Skip to content

Releases: skymanbp/cc-memory

v2.11.4 — the eleventh gate: is it written down at all?

Choose a tag to compare

@skymanbp skymanbp released this 17 Aug 04:47

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 — a file.py:LINE citation still points at its symbol
  • doc_claims — a sentence that counts something matches the tree
  • i18n_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 smallturns_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 — every tools/*.py is a gate except the two the docs explicitly call "not a gate". It previously spelled out doc_claims.py by hand: a hand-kept list of the scripts that check hand-kept lists.
  • CONTRIBUTING.md gains the rule the gate cannot enforce: a new invariant goes in docs/CONTRACTS.md, not only in CHANGELOG.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

Choose a tag to compare

@skymanbp skymanbp released this 17 Aug 04:27

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 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 is plan_active.turns_total - directives.turns_at_touch, and must never be measured against turns_since_last_guardian, which /cc-mem plan-check and every plan replacement zero. Both earlier shapes are recorded there, with why each looked right — plus the rule that the stamp is written inside upsert_directive / set_directive_status rather than supplied by callers.
  • docs/ARCHITECTURE.md § Database schema documents both v9 columns in the same "carries X since migration Y" form the projects and sessions rows already use.
  • commands/cc-mem.md states 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-check does not.
  • Both .zh.md siblings 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

Choose a tag to compare

@skymanbp skymanbp released this 17 Aug 03:51

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.py is 53 checks, up from 27 at v2.11.0.
  • The concurrency gate is now deterministic: re-anchoring r11directiverace exposed that it ran GREEN — sixteen racers now connect before a threading.Barrier so they enter the critical section together. A race gate that only sometimes fires is not a gate.
  • New falsification case r11resetforgives proves a guardian check no longer forgives an idle directive. Register 160 → 161, --anchors 161/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

Choose a tag to compare

@skymanbp skymanbp released this 17 Aug 02:41

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_marker never raises; it returns False. So _block_attempt's except OSError was 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 — and render_block_reason was 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-clear deliberately 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.py was absent from _REQUIRED_PLUGIN_FILES, so /cc-mem status certified 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 .gitignore blanket 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.pyscripts/build_exe.py.
  • Discoverability: repository topics 0 → 20; pyproject.toml keywords 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

Choose a tag to compare

@skymanbp skymanbp released this 16 Aug 03:16

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

  • directives table (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_stated accumulates on ONE row, because
    repetition is the importance signal a plan cannot express.
  • directive-close refuses without --evidence. A directive closed on an
    assertion is the exact failure the ledger exists to prevent.
  • Stop enforcementcore.plan.blocking_reasons +
    hooks/stop.py:_emit_block emit {"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_CONSECUTIVE refusals 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

Choose a tag to compare

@skymanbp skymanbp released this 10 Aug 06:25

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 r10dashrender un-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

Choose a tag to compare

@skymanbp skymanbp released this 10 Aug 05:50

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.py is the six hooks' entry ladder, once. stdin parse (parse_payload) + the opt-out→anchor gate (resolve_project, is_excluded on 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 r10entryorder and r10lograise (both verified RED on reverted copies; anchors 149/149), and tools/contracts.py counting resolve_project with 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

Choose a tag to compare

@skymanbp skymanbp released this 10 Aug 03:50

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_obsolete destroyed an existing supersede link. A loser from an
    earlier SUPERSEDE already points at the row it replaced; the write was an
    unconditional supersedes_id = ?, so chain [2,1] became [2,3] and the
    original wording was unreachable — while /cc-mem supersedes still labelled
    the result "newest first". Now COALESCE.
  • patch_progress bootstrapped 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. One BEGIN IMMEDIATE now; 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 by PRAGMA data_version on a held connection.
  • merge_near_duplicates archived 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-mem commands ignored project scope in a database file that
    legitimately holds several projects: encoding-check --apply archived
    another project's rows, supersedes printed another project's memory into
    the Claude session, sessions / keywords listed 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_ISLNK is
    False for a reparse point), so a junctioned memory/ 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 jsonrpc member as valid Requests.
  • unmatched_criteria judged 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

Choose a tag to compare

@skymanbp skymanbp released this 10 Aug 00:03

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.py derives six sets from the tree, test_surfaces §8 drives the anchor through its own hook in every mode.
  • tools/doc_claims.py scans 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_sessions 557.68 → 4.31 ms at 2 000 sessions; the recency EXISTS 47.41 → 2.75 ms (covering index); memory_topics 272 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 in projects.obs_watermark (v7).
  • Falsification register 41 → 127 cases — each verified RED individually, full sweep 127/127 detected, --anchors 127/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

Choose a tag to compare

@skymanbp skymanbp released this 08 Aug 02:57

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.json there 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-pad anchored 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_dir now requires Users/home to sit at the filesystem root. Without it, any in-repo users/ directory truncated the chain, so a session in <repo>/users/alice/sub reached no rung and planted a stray four levels down — the guard producing the defect it guards.
  • project_root really never raises, including for a non-path cwd: v2.6.0's own handler re-raised, taking the hook to rc=1 with a stderr traceback. user_prompt.py gained 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 vendor and node_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=ro still lets SQLite create -wal/-shm siblings, so the "read-only" report wrote into the directory it only meant to name. immutable=1 forbids that.
  • core/roots.py added to _REQUIRED_PLUGIN_FILES — every hook imports it at module level, so an install missing it does not degrade; all six die at import while status reported 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/cliCodeEraser; 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