Releases: skymanbp/cc-memory
Release list
v2.11.4 — the eleventh gate: is it written down at all?
v2.11.3 fixed an undocumented design by hand and recorded the class as open: "no gate detects an undocumented design." This release closes it.
The gap
The three existing documentation gates all verify the documentation that already exists:
citation_check— afile.py:LINEcitation still points at its symboldoc_claims— a sentence that counts something matches the treei18n_check— a translation is bound to a hash of its source
None of them asks whether a new public surface produced any documentation at all. That is why v2.11.2's two schema columns appeared 0 times in the specification while all ten gates passed.
tools/doc_coverage.py — gate #11
It enumerates four surfaces from the code and requires the document that owns each one to name every member — in both language siblings, because a Chinese reader following the same specification must not be reading a shorter one.
| surface | enumerated from | must appear in |
|---|---|---|
| schema tables | CREATE TABLE in core/db.py |
docs/ARCHITECTURE.md (+.zh) |
| schema columns | ALTER TABLE … ADD COLUMN |
docs/ARCHITECTURE.md (+.zh) |
| MCP tools | mcp/server.py advertised schemas |
README.md (+.zh) |
| config keys | cc_memory/config.json leaves |
README.md (+.zh) |
38 members, 76 document checks. Columns declared inside the original CREATE TABLE are covered by the table itself being documented; an ALTER is the shape that arrives later — which is exactly when documentation is forgotten.
Falsified against the real history, not a constructed case: falsify --case r11doccoverage reverts the sentence v2.11.3 added by hand, and the gate goes red. Registering it also caught that the first breakage was too small — turns_total appears twice in that document, so removing only its definition left the word present and the case ran GREEN. A substring check is falsified only by removing every occurrence.
Deliberately not checked — measured, not assumed
- Migration keys against
CHANGELOG.md. Measured before scoping: 27 of 29 are absent. Requiring them would be a 27-item red gate whose only remedy is rewriting history entries, and this project holds that a history edited to stay current is not a history. - Whether the prose is correct. This gate answers "is this surface mentioned at all". Whether the described behaviour is right is not mechanical, and a green run should not be read as claiming it.
Also
smoke_test's gate-script list is now derived on both sides — everytools/*.pyis a gate except the two the docs explicitly call "not a gate". It previously spelled outdoc_claims.pyby hand: a hand-kept list of the scripts that check hand-kept lists.CONTRIBUTING.mdgains the rule the gate cannot enforce: a new invariant goes indocs/CONTRACTS.md, not only inCHANGELOG.md— the person about to break it is reading the specification, not the release history.- Every live "ten gates" claim became eleven. Sentences describing what was true at an earlier release keep their original number.
Verification
All eleven gates green on Windows and Linux (3.11, 3.13); falsify --anchors 162/162. Both executables were built and run — the installer reports v2.11.4 and still refuses a typo'd --unistall. Verify with SHA256SUMS.txt.
Full detail: CHANGELOG.md
v2.11.3 — the gates were green and the specification was silent
v2.11.2 changed how directive idleness is measured — a schema migration with a load-bearing rule attached — and all ten gates passed.
Those gates check that a file.py:LINE citation still points at its symbol, that a sentence which counts something matches the tree, and that each translation is bound to a hash of its source. None of them asks whether a new design was written down at all.
Measured after the fact: turns_total / turns_at_touch appeared 3× in CLAUDE.md and 2× in the changelog — and 0× in docs/CONTRACTS.md, docs/ARCHITECTURE.md, commands/cc-mem.md, or either Chinese sibling.
A contract that lives only in a changelog entry is a contract the next change will break, because the person about to break it will be reading the specification.
Changed
docs/CONTRACTS.md§ Plan contract now carries directive idleness as the fourth load-bearing property of a Stop refusal: it isplan_active.turns_total - directives.turns_at_touch, and must never be measured againstturns_since_last_guardian, which/cc-mem plan-checkand every plan replacement zero. Both earlier shapes are recorded there, with why each looked right — plus the rule that the stamp is written insideupsert_directive/set_directive_statusrather than supplied by callers.docs/ARCHITECTURE.md§ Database schema documents both v9 columns in the same "carries X since migration Y" form theprojectsandsessionsrows already use.commands/cc-mem.mdstates what "idle" means for a user: turns since that directive was last written. Re-stating or closing it restarts the clock; running/cc-mem plan-checkdoes not.- Both
.zh.mdsiblings updated; markers regenerated. - The README no longer claims cross-platform support "by construction." That described an intention, not a measurement. It now states what CI actually runs — all ten gates on Windows and Linux (3.11, 3.13) — and says plainly that macOS is unmeasured.
Known limits
- macOS has no CI coverage. It is expected to work (the same POSIX paths the Linux job exercises); that expectation is not evidence, and the documentation now says so rather than rounding it up.
- No gate detects an undocumented design. This release fixed the instance by hand. The class is open, and the honest description is that documentation completeness is still a human responsibility in this repository.
Verification
All ten gates green on Windows and Linux (3.11, 3.13). Both executables were built and run — the installer reports v2.11.3 and still refuses a typo'd --unistall. Verify with SHA256SUMS.txt.
Full detail: CHANGELOG.md
v2.11.2 — the debts v2.11.1 recorded, paid
v2.11.1 closed six defects and then wrote down three things it had not closed. This release closes all three. One of them was not merely deferred — it was a fix that looked complete and was not.
Directive idleness was measured against a counter that resets
v2.11.0 stamped every active directive with the project's turns_since_last_guardian, so a directive recorded ten seconds ago was announced as "no progress for 40 turns" and refused the user's turn.
v2.11.1 replaced that with a "has it been touched since the guardian window opened?" guard. That killed the false positive — and inherited a worse one from the counter it still read. /cc-mem plan-check and every plan replacement zero that counter, so a directive genuinely untouched for 30 turns looked freshly attended to the moment anybody ran a guardian check. The ledger forgave exactly the neglect it exists to surface, and it did so silently: "no directive is idle" is indistinguishable from "the ledger is working."
A resettable counter cannot measure elapsed neglect. The answer is a clock that never resets, not a cleverer comparison against one that does.
Schema v9 adds plan_active.turns_total — incremented alongside the drift counter, reset by nothing — and directives.turns_at_touch. Idleness became turns_total - turns_at_touch: subtraction between two numbers that only increase. Both columns default to 0, so an upgraded database reads every existing directive as touched at turn 0 — as old as the project, the safe direction for a ledger whose job is to notice neglect.
The stamp is read inside upsert_directive's and set_directive_status's own BEGIN IMMEDIATE rather than passed in by callers: every caller would otherwise have to know that idleness is counted in plan turns, and the one that forgot would write a row that could never be seen as idle.
Linux runs all ten gates
The workflow ran the fast subset on Linux behind a comment asserting smoke_test / test_surfaces were Windows-specific. That was an assumption, never a measurement, and it left the single largest unknown in this project unmeasured: whether cc-memory works on Linux at all.
Both suites now run there on 3.11 and 3.13 with python3-tk (the one real dependency they need; everything else is standard library). They pass. The assumption was simply wrong.
Also
- The stray
.pytest_cache/is gone from a project documenting "no pytest". tests/test_directive_enforcement.pyis 53 checks, up from 27 at v2.11.0.- The concurrency gate is now deterministic: re-anchoring
r11directiveraceexposed that it ran GREEN — sixteen racers now connect before athreading.Barrierso they enter the critical section together. A race gate that only sometimes fires is not a gate. - New falsification case
r11resetforgivesproves a guardian check no longer forgives an idle directive. Register 160 → 161,--anchors161/161.
Verification
All ten gates green on Windows and Linux (3.11, 3.13). Both executables were built and run, not header-inspected — the installer reports v2.11.2 and still refuses a typo'd --unistall. Verify the assets with SHA256SUMS.txt.
Full detail: CHANGELOG.md
v2.11.1 — the release that shipped red, and the engine nobody drove
v2.11.0 was tagged while tests/smoke_test.py was failing. The directive ledger added three CLI subcommands and commands/cc-mem.md was never updated. Because main() is one sequential function, that first failing assert also hid the one below it — core/db.py had created 12 tables since v8_directives while three documents still said eleven. Neither was caught, because "run all ten gates" was a sentence in a markdown file rather than an executable.
It is one now: python tests/run_gates.py, plus CI on every push.
A seven-scope disjoint audit with adversarial verification then found that the v2.11.0 enforcement engine — the code that can refuse to end your turn — had zero test coverage of its own.
Fixed — the enforcement path
- The escape budget could never release, trapping the session.
write_markernever raises; it returnsFalse. So_block_attempt'sexcept OSErrorwas dead code and the return value was discarded: on any temp directory the marker layer refuses, nothing persisted, the counter stayed at 1, and the hook refused forever — measured[1,1,1,1,1,1,1,1]over eight consecutive Stops. An unbreakable block is worse than no block is the invariant that line exists to hold, and it did not hold. - A stored directive reached Claude as a live authority marker. The block reason is fed back as a
{"decision": "block"}payload — a higher-authority channel than PROGRESS.md — andrender_block_reasonwas the one renderer in its module that never escaped. Now escaped on the write path and the render path. - A refusal's stdout was not a JSON document, because a status line printed first.
- A cleared plan enforced forever — the hook tested a row that
plan-cleardeliberately keeps as a tombstone. - A just-stated directive was reported idle, because idleness came from the plan's counter rather than the directive's own history.
- Re-stating a directive erased its demand and quote — the single operation the ledger exists for.
- Concurrent directive creates raced. Now
BEGIN IMMEDIATE: 8 concurrent creators → 0 exceptions, 1 row,times_stated = 8.
Fixed — packaging and repository integrity
hooks/_entry.pywas absent from_REQUIRED_PLUGIN_FILES, so/cc-mem statuscertified an install where all six hooks die at import as healthy. Third recurrence, so the requirement is now derived from the hooks' module-level import graph.- A
.gitignoreblanket had taken.github/to zero tracked files, invisibly.
Added
tests/run_gates.py— one command, all ten gates, a summary table, and the failing gate's own output..github/— CI running the full set on Windows and the platform-independent subset on Linux, plus issue/PR templates.CONTRIBUTING.md,SECURITY.md.- Nine falsification cases, each driven RED individually. Two ran GREEN first — the checks were fixed, not the cases. Register 151 → 160.
Changed
- README rewritten: 1228 → ~690 lines. 640 lines of release archaeology moved to the changelog that already held it; replaced by a table of contents, a quickstart, a feature index in eight categories, full CLI/MCP/config reference and a troubleshooting table. Chinese sibling rewritten to match.
build_exe.py→scripts/build_exe.py.- Discoverability: repository topics 0 → 20;
pyproject.tomlkeywords 9 → 24, classifiers 7 → 18.
Assets
cc-memory-installer.exe and cc-memory-dashboard.exe were built and run, not header-inspected — the installer reports v2.11.1 and still refuses a typo'd --unistall. Verify with SHA256SUMS.txt.
Full detail: CHANGELOG.md
v2.11.0 — enforced plan state + directive ledger
Advisory became enforced, because advisory did not work
Every piece of plan machinery in this package was a suggestion. hooks/stop.py
said so in its own comment — "The plan-refiner nudge is advisory" — and
rate-limited that suggestion to once per five turns on top.
What that cost, measured in a real consuming project on 2026-08-15: a
51,237-character raw plan sat unrefined while PLAN.md, plan-status
and the drift guardian all answered from the PREVIOUS plan. The guardian
was faithfully drift-checking against a superseded baseline — the one job it
exists to do, performed against the wrong document.
A full-transcript audit of 416 deduped user messages then found a feature
demanded six separate times with zero implementation, and a pause rule
stated three times that was violated the first time it mattered. Nothing
detected any of it, because nothing was ever forced.
Added
directivestable (schema v8) +directive-list/directive-add/
directive-close. A ledger of what the USER asked for, deliberately
separate from plan steps: a step is a unit of EXECUTION and dies when the
plan is replaced or the step is marked done, while a directive is a unit of
INTENT that outlives every plan. Folding them together is precisely how the
six-times-repeated demand vanished — it was never a step in whichever plan
happened to be active.times_statedaccumulates on ONE row, because
repetition is the importance signal a plan cannot express.directive-closerefuses without--evidence. A directive closed on an
assertion is the exact failure the ledger exists to prevent.- Stop enforcement —
core.plan.blocking_reasons+
hooks/stop.py:_emit_blockemit{"decision": "block", "reason": ...}for
an unrefined plan, a plan that has gone undrift-checked, or an active
directive idle past the threshold. - A guaranteed escape. After
_BLOCK_MAX_CONSECUTIVErefusals of the
same condition set it degrades to a loud advisory; the counter is keyed by
a digest of the condition keys, so fixing one problem never spends the budget
of the next. An unbreakable block is worse than no block. Kill switch:
CC_MEMORY_PLAN_ENFORCE=0.
Scope
Projects with no plan row are never enforced — opting into planning is what
turns it on. Every other project on the machine is untouched.
Removed
_claim_refine_nudge and its two constants. A rate-limited advisory is what let
a plan sit unrefined indefinitely; leaving the helper would keep a second,
unreachable policy in the tree. Its temp-marker prefix stays registered in the
uninstall sweep so older installs are still cleaned.
Verification
tests/test_directive_enforcement.py— 27 checks (ledger / blocking
predicate / kill switch / escape budget)- Live hook drive (stdin payload to stdout): a no-plan project passes, an
unrefined plan blocks and names both the condition and the fix, the kill
switch releases, and five consecutive Stops give
[True, True, True, False, False]— proving the escape budget really lets go - Full suite 9 passed, including the version-consistency gate across all five
declared manifests - Four doc gates green:
doc_claims/citation_check/contracts/
i18n_check
v2.10.1 — the three open items closed
The three items v2.10.0 recorded as open, closed:
- Dashboard logic cores now executed by a gate. The Progress/Plan renderer (cx 54, register-E3 marker escaping included) and the LLM tidy-verdict normaliser (cx 47's heavy half) are pure staticmethods, extracted behaviour-preserving; their Tk callbacks keep only widget plumbing. test_surfaces §8 drives both (13 checks);
falsify --case r10dashrenderun-escapes the renderer on a copy and the suite goes RED — the first draft of that breakage modelled the counterfactual backwards and was rewritten per round 9's lesson 1. The Tk shells hold no logic now; their remaining zero coverage is stated, not hidden. - The contracts registries fail loud about their proxy.
_verify_entry_gate(same pattern as_BACKSTOP_CREATORS): a shared hook gate that stops consulting the opt-out before anchoring now ERRORS both registries instead of listing six hooks as protected.falsify --case r10gateproxy, RED (verified). The v2.10.0 accepted-risk ledger entry is retired. - Codex confirmation verdict: CONFIRMED-CLOSED. The follow-up had been queued behind the first review run, wedged 1h+ after its file reads; killed and re-dispatched fresh, it confirmed the v2.10.0 guard fix closes Q2 with no new defect (three file:line citations).
Falsify registry 149 → 151, anchors 151/151, all nine gates green on this tree; both executables rebuilt from it and RUN through a sandboxed install → hook drive → uninstall plus a dashboard boot check.
v2.10.0 — six ladders walked into one gate
An anti-bloat architecture round, driven by measurement.
A function-level LOC + cyclomatic-complexity sweep against the v2.5.0 baseline (487 → 818 functions, 12,514 → 20,836 function-LOC) answered the question this round was commissioned for: the growth since v2.5 is overwhelmingly mechanism with a measured defect behind every guard — except one real structural duplication, now fixed.
hooks/_entry.pyis the six hooks' entry ladder, once. stdin parse (parse_payload) + the opt-out→anchor gate (resolve_project,is_excludedon the RAW cwd FIRST). Six hand-rolled copies (~350 lines) had shipped a defect on every historical drift — v2.7.0's release theme and v2.9.0's junk-cwd database plant were both single-rung misses. Per-hook policies (coerce vs abort, the NUL check, excluded-branch reactions) stay per-hook.- Pinned four ways: test_surfaces §4's new narrow-exclusion drive (a listed subdirectory inside a live project — behaviourally untested before), §7's in-gate order rule + no-direct-import rule, falsify cases
r10entryorderandr10lograise(both verified RED on reverted copies; anchors 149/149), andtools/contracts.pycountingresolve_projectwith identical 12/12 membership. - Codex (gpt-5.6-sol, read-only) reviewed the diff. Its one confirmed finding — the shared ladder lost post_tool_use's broken-logger guard — is fixed at the one shared site with a raising-logger assertion. The follow-up confirmation turn did not return from the runtime before this release; recorded as such.
- Deliberately NOT refactored, on record (CLAUDE.md §v2.10.0):
pre_compact.main's linear pipeline,db.py's snapshot-verdict cluster, and the dashboard's three cx-47..100 functions (zero executable coverage — refactoring an untested 2.9k-line GUI is the failure mode this round exists to avoid).
All nine release gates green on the final tree; both executables rebuilt from it and RUN through a sandboxed install → hook drive → uninstall, plus a dashboard boot check. SHA256SUMS.txt covers both binaries.
v2.9.0 — two readers, disjoint scopes, and the gates got read too
A dual-perspective review: two independent readers, 18 defects, 18 repros
No audit framework this round. Two reviewers with disjoint file sets and
different angles read the shipped v2.8.0 tree at the same time — a six-scope
fan-out of my own (db/writer · hooks · mcp/cli · ui · core · tools/tests, with
adversarial refutation of the severe findings) and an independent read-only
pass by codex over the whole runtime package. Every finding was reproduced
here before it counted; two were refuted and dropped.
Data you would have lost
archive_obsoletedestroyed an existing supersede link. A loser from an
earlier SUPERSEDE already points at the row it replaced; the write was an
unconditionalsupersedes_id = ?, so chain[2,1]became[2,3]and the
original wording was unreachable — while/cc-mem supersedesstill labelled
the result "newest first". NowCOALESCE.patch_progressbootstrapped across three transactions, so two hooks
first-touching the same project interleaved and a stale "row absent" verdict
replayed the default row over a landed patch. OneBEGIN IMMEDIATEnow; 200
concurrent first-touch pairs lost 0 fields.- MEMORY.md's ordering probe was blind inside one second — the fingerprint
was row counts +MAX(id)+MAX(updated_at)and_now()stamps whole
seconds. Replaced byPRAGMA data_versionon a held connection. merge_near_duplicatesarchived a row on the authority of a row it was
archiving in the same pass (Jaccard is not transitive).- One malformed TodoWrite entry cost the entire compaction — and the
PROGRESS.md handoff with it.
Data that reached the wrong project, or the wrong person
- Four
/cc-memcommands ignored project scope in a database file that
legitimately holds several projects:encoding-check --applyarchived
another project's rows,supersedesprinted another project's memory into
the Claude session,sessions/keywordslisted it. - A reinstall deleted a user's own hook when it shared a matcher group with
ours — rc=0, no warning. The uninstall path had been fixed for exactly this. - The settings.json compare-and-swap was disarmed on a fresh machine, so a
settings.json Claude Code created inside the write window was destroyed. - PLAN.md forged whole document sections from two model-authored slots.
- An empty prompt left the previous turn's request in the marker the Stop
observer ships to Anthropic.
Things that silently stopped working
- PostToolUse discarded any tool event over 512 KiB — the observation row
and the mode-independent live-plan block, with rc=0 and no log line. - A Windows junction defeated both fail-closed link guards (
S_ISLNKis
False for a reparse point), so a junctionedmemory/was written into and
adopted as a project root. - The web viewer could be locked out indefinitely by 16 header-phase
drip-feeders; there is a 10 s absolute header budget now (measured recovery
at t+10.1 s, from none). The shed 503 was also being discarded by a TCP
reset — 30/30 probes receive it now, from 26/30. - MCP answered frames with no
jsonrpcmember as valid Requests. unmatched_criteriajudged CJK criteria on the ASCII bar, so a replaced
Chinese criterion read as "carried" while the steps gate refused the same
pair.
The gates themselves
Five holes, found by pointing the review at them: a .py-only citation regex
(25 citations exempt, 2 already rotten), a doc_claims grammar that one
modifier word defeated, a render_paths probe missing neutralize_document
and aliased imports (6 reported, 7 in the tree), a verify_anchors handler
catching only SystemExit — a BaseException, so naming it is not naming
Exception — and _HOOK_ORDER bound to nothing while being the sole
enumeration behind four hook-wide rules. Plus the third release gate running
outside a sandbox and leaking two project directories per run into the real
%TEMP% (270 found, 42 MB, removed).
Verification
Nine gates green in one run. smoke_test.py gained a §9 block, test_surfaces.py
a §9 section, and the falsification register went 127 → 147 cases — each new
one driven RED individually, including one rewritten after it ran GREEN because
it modelled "the probe always fires" instead of "the probe is blind".
--anchors 147/147. Recorded coverage gaps, including what this round did NOT
close, are tracked in the working tree.
Assets
First release with binaries attached: cc-memory-installer.exe,
cc-memory-dashboard.exe, and SHA256SUMS.txt. Both were built from this
commit and RUN — install → verify → unknown-flag refusal → uninstall → verify,
plus a dashboard start — against a sandboxed HOME before upload.
Full narrative: CHANGELOG.md
v2.8.0 — the audit turned on its own fixes
A correctness release closing a two-round radial audit of the whole tree. Full narrative in CHANGELOG.md §2.8.0 (rounds 3–8).
Highlights
- cc-tree radial audit, two rounds. Round 1: 12 framings over the root → 21 candidates → 18 adjudicated → 15 fixed. Round 2 attacked round 1's own fixes: 27 → 18 survived adversarial refutation → 13 fixed. Every finding was reproduced on the real package before its verdict counted; 9 candidates were killed in refutation and the kills are on record too. The run is NOT convergent under the engine's §6 (novelty 0.667 vs the 0.15 bar) — shipped with that stated, not papered over.
- Two coverage closures worth more than any defect: the plan-anchor position invariant (stated three times in CLAUDE.md, zero executable coverage — nine gates stayed green with it broken) and the anti-patch contract (prose-only) are now computed and falsified:
tools/contracts.pyderives six sets from the tree,test_surfaces§8 drives the anchor through its own hook in every mode. tools/doc_claims.pyscans three surfaces with one grammar — tracked markdown,config.json, and the shipped package's docstrings/comments. Its first sweep of the new two caught three live wrong counts ("the seventh caller" with twelve surfaces consulting the opt-out; "66 call sites" in a file holding 80; "three hooks" importing a module two hooks import).- Measured fixes:
get_recent_sessions557.68 → 4.31 ms at 2 000 sessions; the recencyEXISTS47.41 → 2.75 ms (covering index);memory_topics272 KB/~68k-token dump → bounded with reported truncation; a user question truncated at an unpaired harness tag (77 chars → 34) → stored whole; the SessionStart banner un-eaten by its own escape sweep; the observer watermark durable inprojects.obs_watermark(v7). - Falsification register 41 → 127 cases — each verified RED individually, full sweep 127/127 detected,
--anchors127/127 intact. Nine release gates green in one run; both installer and dashboard exes rebuilt from this tree and actually run through a sandboxed install/uninstall.
Verification
smoke_test · test_plan_carryover (20/20) · test_surfaces §1–§8 · i18n_check (3 in-sync) · citation_check (598/598, 0 stale) · doc_claims (59 bound claims, 0 problems) · compileall · tomllib parse · version-site agreement (five sites, one string).
🤖 Generated with Claude Code
v2.7.0 — guards belong to the candidate set
v2.6.0 attached its safety guards to one rung's inner loop instead of to the candidate set — and every rung that did not inherit them became its own data-integrity defect.
A convergent adversarial debug round (five dimensions, every finding double-verified against the real source) confirmed 45 defects in v2.6.0. The three worst share that single root cause — _is_container had exactly one call site — and all three were reproduced before being fixed:
- the database rung consulted no guard at all, so a
memory/created by one session in a projects folder captured every uninitialised project under it; - the marker rung never container-checked the first marker it found, only the ones it extended onto, so one stray
package.jsonthere did the same to every marker-less directory below it; - neither had any notion of a dependency tree, so a cwd inside
node_modules/left-padanchored on the package and planted a database where the reporter does not look.
The structural fix
_candidates() filters the chain once, before any rung reads it. Containers of projects and dependency internals are simply not candidates anywhere — three patches replaced by one invariant.
_is_container is rewritten with asymmetric triggers: two VCS-root children is always decisive; two merely database-owning children counts only when the directory owns none itself. The first narrowing exempted any directory with a database — which is exactly what a polluted container has — and the reproduction stayed red until that was corrected.
Also fixed, each with its own reproduction
- Monorepos resolve to the workspace, as the docs always claimed. The marker extension no longer requires a contiguous run —
packages/,apps/,crates/,libs/carry no manifest, so v2.6.0 stopped at the package and re-created the stray it exists to prevent. _is_profile_dirnow requiresUsers/hometo sit at the filesystem root. Without it, any in-repousers/directory truncated the chain, so a session in<repo>/users/alice/subreached no rung and planted a stray four levels down — the guard producing the defect it guards.project_rootreally never raises, including for a non-pathcwd: v2.6.0's own handler re-raised, taking the hook to rc=1 with a stderr traceback.user_prompt.pygained the field-type guard the other five hooks already had.- The stray reporter was blind where strays live — it reached one level less than asked and skipped nine directory names including
vendorandnode_modules. It also ran after the missing-database early return, so the stray-only shape printed "No database" and nothing else. - The nested count is active-only and provably cannot write. It counted archived rows while the root's line did not (3725 vs 2607 for one database); and plain
mode=rostill lets SQLite create-wal/-shmsiblings, so the "read-only" report wrote into the directory it only meant to name.immutable=1forbids that. core/roots.pyadded to_REQUIRED_PLUGIN_FILES— every hook imports it at module level, so an install missing it does not degrade; all six die at import whilestatusreported healthy.
Every surface anchors now, not just the hooks
cc-mem --project goes through _anchor_project and /ccm-load resolves before building the scaffold. Until now the hooks refused to create a stray while /cc-mem add from a subdirectory made one — and rung 0, being terminal, then pinned all six hooks to it permanently. Redirections are always printed: an explicit --project is an instruction.
Verified
12 targeted reproductions, each red before and green after. All 20 real databases on the author's machine — including the four legitimately nested, the largest holding 3,725 memories — still resolve to themselves; CodeEraser/cli → CodeEraser; the 37-child projects folder stays itself. tests/test_surfaces.py §7 grew to 23 ladder cases plus a contracts block: _CONTAINER_CHILDREN is now pinned from both sides (it was entirely unpinned — the suite passed with the threshold at 1), and project_root is asserted to return a Path for int, None, list, dict and bytes.
Eight release gates green: smoke, carryover (20), surfaces, i18n (3 in-sync), citations (616, 0 stale), compileall, pyproject parse, version agreement across all five sites.
Minor, not patch: resolution changes for the same input in a monorepo, inside a dependency tree, and under a projects folder.
Full detail: CHANGELOG.md