Hidden survives a silent re-save, and six context builders never read it

a-memory

A four-tier SQLite memory whose visibility flag survives automatic re-saves and filters search, while the context builders read every fact unfiltered.

LicenceMIT
Size84,391 lines of Python in 617 files, 41,776 of them under tests/; the L4 store, core/memory.py, is 531 lines
Activity967 commits on main by 6 author names, 22 June – 14 October 2026; version 1.12.0
Tests2,014 test functions in 302 test files; none run for this reading

Carries 4 of 7 rubric mechanisms. Most systems here carry none or one (43%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

a-memory is a four-tier local agent memory in plain SQLite, reached through an MCP server, whose long-term facts carry a visibility flag that search excludes in SQL and that an automatic re-save cannot reset. The weak side is everything that reads around search: the session-start and context builders read facts through get_all, which carries no visibility predicate, so the invariant written beside the search query — "C8: private facts never leave the store via recall" — holds for that query and not for what reaches the prompt.

It describes itself as "4-tier agent memory with hybrid search and a real knowledge graph — all in plain SQLite files. Zero cloud. Zero external APIs." The tiers are an L1 reflex buffer persisted per layer and user, an L2 session store, L3 episodes and L4 core facts, with an L0 journal that captures raw input before distillation.

Visibility is four states, and an absent argument keeps the stored one. A fact is visible, pinned, private or hidden, validated on write (core/memory.py:99-101). A save that passes no visibility re-reads the stored flag before updating (:106-118), under a comment that records the bug and its date:

"a 'hidden' row re-saved with the same canonical key would otherwise be back to 'visible' — F1 sanitation, 2026-09-12"

The writers that never pass a visibility are the ones that matter: the distiller and the consolidation sweep re-upsert by canonical key, and the rule keeps their output under an operator's flag. An explicit argument overrides it. The MCP write tool takes visibility and forwards it (mcp_server/tools/memory.py:37, :86, :89), and test_resave_preserves_operator_visibility asserts that visibility="visible" clears a hidden flag (tests/test_lifecycle/test_c8_tails.py:127-129). The quarantine holds against automatic writes; the agent can lift it.

Search filters, and the context builders do not. search selects visibility NOT IN ('private','hidden') (core/memory.py:414), the superseded-pair lookup inside it does the same (:457), and get_pinned selects only pinned (:353). get_all (:333-347) selects every row for the layer and user by importance. Six builders read it and render key=value:

  • features/inject.py:165-170, the rehydrate block after a compaction
  • features/inject.py:221-228, the important-facts block
  • features/recall.py:62-64, the recall protocol's marker axis
  • features/smart_context.py:51-56, the smart-context candidate list
  • mcp_server/tools/ops.py:97-98, the memory_context tool
  • mcp_server/tools/ops.py:244-245, memory_context_inject, which also writes the text to a CONTEXT.md file (:267-278)

The second has a filter that does nothing. It keeps facts where getattr(f, "visibility", "visible") == "visible", and CoreEntry (core/memory.py:34-44), built by _row_to_entry (:498-510), has no visibility field. The default answers every time, so pinned, private and hidden facts above the importance threshold all pass. A private fact the search tests prove absent from recall is injected at session start.

Scope is carried by the handle. MemoryManager.get_layer(layer_type, user_id) builds a MemoryLayer whose EpisodicMemory and CoreMemory are constructed with that layer (core/__init__.py:38-54, :122-132), and every read on those two classes begins WHERE layer=? AND user_id=?. The MCP tools take layer and user_id as arguments and resolve a handle from them, so the binding is a property of the Python API rather than of the agent surface. One cache sits in front of it and drops the layer. MemoryLayer.recall keys its results recall:{user_id}:{query}:{limit} (:81) in a MemoryCache that AppContext hands to every handle (mcp_server/context.py:22-23). A recall on the agent layer within 300 seconds of the same query on the user layer returns the user layer's facts.

Every CoreMemory mutation appends a before-and-after row, and the ledger is never rewritten. _record_history writes "one A2.2 ledger row with full before/after row JSON" from save and delete (core/memory.py:121, :129, :368), and no production statement updates or deletes core_memory_history. The ledger's reach stops at those two methods. The 15-minute forgetting loop archives and deletes rows directly (lifecycle/forgetting.py:194-219, :597-600, called from mcp_server/lifespan.py:61), as do forget(scope='recent'), branch deletion and the importance and kind updates. The row snapshot also omits visibility (core/memory.py:208-220), so hiding a key leaves no ledger trace. Both the ledger and the interval chain beside it sit under bare exception handlers that log a warning and let the save commit.

2. Mental Model

A fact is a key and a value in one layer for one user, with an importance, a kind, a source and a visibility. A fact is believed when it is stored and visible or pinned. It stops being believed by deletion, by archiving, or by being marked private or hidden, which hides it from search but not from the context builders.

A layer is user or agent. A handle carries one layer and one user; the MCP surface lets the caller name both.

Hidden is a flag a later automatic write inherits and an explicit write can clear.

A ledger row is what CoreMemory.save or delete changed, with both images and who triggered it. Writes that bypass those two methods leave none.

A contradiction produces two rows, not an overwrite: the distiller marks the earlier scope: earlier and writes the new clause under key::vN with scope: later (lifecycle/distiller.py:413-456), and search hides the earlier one when its later version exists.

Diagram — the layer and user are bound into the handle; a save with no visibility keeps the stored flag; search filters visibility in SQL, the context builders read get_all and do not
Diagram source
%% caption: the layer and user are bound into the handle; a save with no visibility keeps the stored flag; search filters visibility in SQL, the context builders read get_all and do not
flowchart TB
    MCP["MCP tool call<br/>(layer, user_id, key, value, visibility)"] --> H["MemoryManager.get_layer(layer, user_id)<br/>→ MemoryLayer handle"]
    AUTO["distiller · consolidation sweep<br/>(no visibility argument)"] --> SAVE
    H --> SAVE["CoreMemory.save"]
    SAVE --> EX{"key exists?"}
    EX -->|"yes, visibility is None"| KEEP["re-read stored flag<br/>— hidden stays hidden<br/>(F1 sanitation, 2026-09-12)"]
    EX -->|"yes, explicit visibility"| OVR["explicit value wins<br/>— visibility='visible' un-hides"]
    EX -->|"no"| INS["insert, default 'visible'"]
    KEEP & OVR & INS --> LEDG[("core_memory_history<br/>before / after JSON · triggered_by<br/>— visibility not in the snapshot")]
    LOOP["15-minute forgetting loop<br/>archive · decay · dedup"] -->|"DELETE / UPDATE core_memory<br/>no ledger row"| CM[("core_memory")]
    SAVE --> CM
    CM --> R{"reads"}
    R -->|"search · pair lookup"| F1["WHERE layer=? AND user_id=?<br/>AND visibility NOT IN<br/>('private','hidden')"]
    R -->|"get_pinned"| F2["visibility='pinned'"]
    R -->|"get_all"| F3["WHERE layer=? AND user_id=?<br/>— no visibility predicate"]
    F3 --> CTX["inject blocks · recall protocol ·<br/>smart context · memory_context ·<br/>memory_context_inject → CONTEXT.md"]
    F1 --> RC["MemoryLayer.recall<br/>cache key recall:user:query:limit<br/>— no layer"]

3. Architecture

Area Role
core/__init__.py MemoryManager, MemoryLayer, the recall cache and the handle binding
core/memory.py CoreMemory: save, the visibility rules, the ledger, the interval chain, search
core/episodic.py L3 episodes, layer-scoped with (layer, user_id) indexes
core/session.py, core/reflex.py L2 sessions and the L1 buffer persisted per layer and user
shared/l0.py The L0 capture journal that the distiller and replay read
lifecycle/distiller.py, lifecycle/consolidation.py Extraction into L4 and episode promotion
lifecycle/forgetting.py Decay, archiving into archived_memories, restore
features/inject.py, features/recall.py, features/smart_context.py The context builders
features/staging.py Staged mutation proposals and their decide path
features/history.py, features/versioning.py Ledger reads, rollback, snapshots
rag/, graph/, wiki/ Hybrid retrieval, the epistemic graph and wiki pages
mcp_server/ Tool registry, the per-process AppContext, the background loop
alembic/ Schema migrations

Deployment and ergonomics

Everything runs in one Python process over SQLite files in one data directory (MCP_MEMORY_DATA_DIR, default ~/.mcp-ariel-memory). No API key is required to store or search. Embeddings and the spaCy NER model are optional extras, and the privacy gate falls back to a regex tier without the model. The store is ordinary SQLite and repairable by hand, and several scripts under scripts/ exist to do exactly that. Several processes may serve one base; the background loop takes a per-base maintenance lock so only one runs housekeeping (mcp_server/lifespan.py:46-58).

4. Essential Implementation Paths

Write. memory_remember (mcp_server/tools/memory.py:29-94) strips secrets, checks the rate limit and an importance gate, captures an L0 row and closes it as routed_direct, then calls MemoryLayer.remember (core/__init__.py:56-78) and CoreMemory.save (core/memory.py:80-133). The save path validates visibility, finds the row by (layer, user_id, key), preserves or overrides visibility, updates or inserts, appends a ledger row and maintains the interval chain, then commits once.

Extraction. distill_and_route (lifecycle/distiller.py) classifies clauses, applies the dialogic guard, a novelty gate against same-key rows including hidden ones (:390-405), a semantic duplicate check and a conflict check, and saves with created_at set to the source message time (:444-460).

Retrieval. memory_recall consults a layer-keyed tool cache (mcp_server/tools/memory.py:124), then MemoryLayer.recall (core/__init__.py:80-96), which consults its own layer-less cache and calls CoreMemory.search and EpisodicMemory.search.

Context assembly. build_inject_blocks (features/inject.py:74-252) runs at session start from the session_started hook (hooks/user_hooks.py:202-210) and from an HTTP endpoint that takes the layer from the request body (mcp_server/endpoints/hooks.py:58-77). build_smart_context, recall_protocol, memory_context and memory_context_inject are the other builders.

Forget. The forget primitive (mcp_server/tools/primitives/forget.py) takes exact, fuzzy, recent, episode_ids or undo. The first, second and fourth copy rows into a shadow archive before deleting, with a dry run that writes nothing. recent purges with no archive and no preview. undo restores from the archive through ForgettingSystem.restore_entries.

5. Memory Data Model

core_memory holds layer, user_id, key, value, importance, memory_kind, expires_at, source, metadata, visibility, created_at and updated_at, unique on (layer, user_id, key) (core/memory.py:56-71). source is a documented provenance contract — user_explicit, staging_promotion, episode_promotion or manual — which distinguishes a fact a person stated from one promoted out of an episode.

created_at and updated_at are separable on insert: created_at takes the caller's value, which the distiller, replay and import set to the source message's timestamp, and updated_at is the write instant (:189-197). That is the time a thing was said and the time it was stored. Neither is when the fact held in the world.

core_memory_temporal keeps an interval per key with valid_from set to the write instant (:270-287). get_at_time and get_intervals read it (:299-326), and no production code calls either. A backfilled fact can carry a created_at months earlier than its interval's valid_from.

core_memory_history stores old_value, new_value, both importances, a 16-character commit_hash, triggered_by, created_at and the two row images as JSON (:241-258). The image is built by _row_snapshot (:208-220) from key, value, importance, kind, expiry, source and metadata. Visibility is not in it.

Archived rows go to archived_memories with eight columns. A restore reports the destination columns the archive could not fill, read from the schema rather than a hand-kept list (lifecycle/forgetting.py:78-100).

6. Retrieval Mechanics

CoreMemory.search (core/memory.py:387-487) splits the query on whitespace, selects rows matching any token in key or value with the visibility predicate, over-fetches ten times the limit and ranks by matched-token count, then importance. Contradiction pairs are resolved at read time: an earlier row is hidden when its later row is on the page or exists under the same key or key::vN, and a private later version does not hide a public earlier one. Expired facts stay in results with an [EXPIRED] prefix.

The multi-source retriever behind dream fuses RAG pages, wiki, episodes, core facts, graph nodes and entity matches. Its episodic and core sources call the same layer-bound classes, and its graph and entity reads filter epi_nodes on layer and user_id (rag/multi_source.py).

The builders read differently from search. build_inject_blocks adds RAG hits for the current text, the last day of L1, recent diff_gap episodes, the scratchpad, a rehydrate block after a compaction, pending proposals, disclosure triggers, the important block and the pinned block. Each is capped at 400 characters and charged against a token budget. The scratchpad is read with the layer fixed to user (features/inject.py:141), and proposals are listed by user alone, so an agent-layer injection shows the user layer's scratchpad and both layers' proposals. Query-independent blocks go first, ahead of a <cache:break> marker, so a provider's prompt cache can hit the prefix.

7. Write Mechanics

One save path handles both update and insert, keyed on (layer, user_id, key). There is no append-only fact table; the ledger is the record of what a key held. The distiller is the main automatic writer, gated by a dialogic guard, a same-key novelty gate, a semantic duplicate check and the contradiction split. The hourly sweep drains staged dream output into proposals and promotes high-weight episodes.

Staged proposals (features/staging.py) are the review lane. propose absorbs a duplicate pending proposal and, for seven days, one whose key was already rejected or expired (:42-63). Decided rows are purged after fourteen days (:127-146). decide applies or rejects, reached through the memory_proposals MCP tool with approve=true|false (mcp_server/tools/ops.py:848), and auto_apply_pending applies session_close and dream proposals that pass the promotion gate (features/staging.py:155-191).

Two paths put a hidden fact back as visible. memory_history(action='rollback') of a delete reinserts the old image through save with no visibility, which on an absent key inserts visible (features/versioning.py:120-156). And forget(scope='undo') restores from the archive with an INSERT that leaves visibility to its default (lifecycle/forgetting.py:504-539).

Operational cost

Writes are synchronous SQLite statements with no model call on the explicit path. The distiller runs in hooks and replay, so extraction does not block the MCP write. The 15-minute loop reads every core_memory row for decay (lifecycle/forgetting.py:158-192), and a nightly pass enriches the graph. Injection is bounded by a token budget of 2,000 by default and a 400-character per-block cap.

8. Agent Integration

The MCP server registers a flat tool set — memory_remember, memory_recall, memory_context, memory_context_inject, memory_history, memory_proposals and others — and a primitive set of think, dream, forget, evolve, project, memory_hook and wake_up. Every memory tool takes layer and user_id as arguments. The agent can therefore write and read either layer for any user id, set or clear visibility, decide its own staged proposals, and roll back any ledger row by id.

memory_history(action='get') returns a ledger row with both JSON images by history_id alone (mcp_server/tools/ops.py:1492-1496, features/history.py:47-55), and rollback writes into the row's own layer and user (features/versioning.py:120-128). Neither checks the layer and user_id the call named. Ledger rows carry private and hidden values like any other, since the table records no visibility.

9. Reliability, Safety, and Trust

The visibility decision is written per query. Three L4 queries carry it in SQL; get_all, get and the two interval reads do not, and the six builders that reach a prompt all use get_all. The Python-side check at features/inject.py:222 reads an attribute the entry object does not have. The tests assert the exclusion on search and nowhere else, which is why it held there. Putting the predicate in one admission function every read path calls, as MuninnDB does, would make a builder that forgot it fail visibly.

The ledger's guarantee is narrower than its docstring. It is append-only and complete for CoreMemory.save and delete. The forgetting loop's archive deletes, delete_older_than, branch deletion, deduplicate_core, importance decay, importance_scheduler, the quality and replay importance updates, and kind reclassification all write core_memory directly. The archive path records a memory_transitions row instead; the others record nothing in the memory store.

Failures are dropped, not counted. _record_history "[d]egrades to a warning so memory writes never fail on history", and _record_temporal is "advisory, never fails a save". Both run inside the save's transaction under except Exception, log at warning level and let the save commit. If the interval INSERT fails after the UPDATE that closed the previous interval, the key commits with no open interval, and nothing reconciles it.

The recall cache crosses layers. One MemoryCache with a 300-second TTL backs every handle, and the recall key has no layer. The project fixed the same class of defect in its multi-source retriever on 6 October 2026 (d8ec04fe…, issue #78), where the agent-layer instance had answered with user-layer rows. That commit's message explains that every unit test built the retriever explicitly, so the construction site was never exercised.

10. Tests, Evals, and Benchmarks

2,014 test functions in 302 test files, with an eval/ harness, hypothesis property tests and chaos tests. I ran none of them.

The negative cases are specific. test_visibility_pinned_and_private (tests/test_lifecycle/test_c8_tails.py:73-90) saves a pinned and a private fact and asserts the private one's search returns [] while the pinned one is found. test_hidden_first_class_excluded_from_search (:133-149) asserts a hidden fact is absent, the same key made visible is found, and re-hiding removes it. test_episodic_and_core_sources_follow_the_instance_layer (tests/test_rag/test_prod_wire.py:169-193) writes one token into both layers and asserts each layer's search returns only its own rows, and test_e2e_dream_respects_layer_isolation (tests/test_e2e_pipeline.py:158-170) asserts a user-layer dream finds nothing from the agent layer.

The gaps are adjacent to those cases. No test drives a private or hidden fact through build_inject_blocks, build_smart_context, recall_protocol or the two context tools; test_inject_pinned_block (:152-186) asserts only that a pinned fact is present. No test exercises MemoryLayer.recall across layers with a shared cache. test_resave_preserves_operator_visibility (:104-129) asserts the explicit override as intended behaviour.

11. For Your Own Build

Steal

Decide what a missing argument means and write it down. visibility=None meaning "keep the stored flag" rather than "use the default" is what stops an automatic re-upsert from resurrecting a suppressed key, and the dated comment is what will stop it regressing.

Pair every exclusion test with a positive control in the same store. The hidden-fact test makes the key visible and asserts it is found before hiding it again, so a filter that excluded everything would fail.

Resolve contradictions into two rows with a read-time rule. scope: earlier and scope: later with a contradicts reference keep both values and let search show the current one.

Avoid

Writing the privacy predicate beside one query. Six builders that render facts into a prompt read through a method without it, and a seventh path filters on a field the object does not carry.

A cache key that omits a dimension the query below it uses. The layer is in the SQL and in the tool-level cache, and missing from the one between them.

A ledger documented as the mutation record when half the mutators bypass it, and a row image that leaves out the trust field.

Fit

This suits one operator running a local agent who wants facts, episodes, a graph and a review queue in SQLite with no service to stand up. Walk away if a private fact must never reach a prompt, if the agent must not be able to clear a quarantine, or if the mutation ledger must be complete.

12. Open Questions

Whether get_all's callers are meant to honour private. The one that tries reads an absent attribute, which reads as intent; the distiller's same-key select includes hidden rows deliberately and says why.

Whether the interval chain will get a reader. get_at_time and get_intervals are tested and uncalled.

Whether memory_history is meant to be agent-facing as it stands. Its get and rollback take a bare history_id, and mcp_server/annotations.py:74-76 marks the tool read-only although rollback and snapshot_restore write.

Appendix: File Index

Path What to read it for
core/memory.py:80-133 The save path, the visibility rule and the ledger and interval calls
core/memory.py:222-261 The ledger append and its bare exception handler
core/memory.py:333-357 get_all with no visibility predicate, get_pinned with one
core/memory.py:387-487 Search: the visibility predicate, the C8 comment, the contradiction pair rule
core/__init__.py:38-132 The handle, the layer-less recall cache key
features/inject.py:74-252 The session-start assembler and the no-op visibility check
mcp_server/tools/ops.py:79-297 memory_context and memory_context_inject, the CONTEXT.md write
mcp_server/tools/ops.py:1466-1520 memory_history: get and rollback by id
lifecycle/forgetting.py:158-219, :504-539, :597-600 Decay, archive deletes outside the ledger, restore without visibility
lifecycle/distiller.py:390-460 The novelty gate over hidden rows, the contradiction split, created_at
features/staging.py:32-262 Proposals, the seven-day key guard, decide
tests/test_lifecycle/test_c8_tails.py:73-186 The visibility tests, positive and negative
tests/test_rag/test_prod_wire.py:157-219 The layer-isolation tests added with the retriever fix

Recorded searches

Run at the tree root of the pinned checkout with /usr/bin/git grep; its extended regex has no \b, so the patterns below avoid it.

  • git grep -n -E '\.get_all\(' -- '*.py' ':!tests' — ten hits: the six builders, two dashboard reads, features/branches.py:81, and core/__init__.py:106 in get_context, which has no caller.
  • git grep -n -E 'visibility' -- '*.py' ':!tests' ':!core/memory.py' ':!alembic' — reads at features/inject.py:222 (the no-op), lifecycle/distiller.py:398 and mcp_server/tools/wiki.py:122; writes through core/__init__.py:77 and mcp_server/tools/memory.py:86, :89.
  • git grep -n -E '(UPDATE|DELETE FROM|DROP TABLE( IF EXISTS)?) core_memory_history' -- '*.py' — the downgrade migration only.
  • git grep -n -E 'DELETE FROM core_memory[ "]' -- '*.py' ':!tests' — core/memory.py:371, :380, features/branches.py:157, features/compression.py:32, lifecycle/forgetting.py:600, :609, two purge scripts and one migration; only :371 is preceded by a ledger append.
  • git grep -n -E 'get_at_time\(|get_intervals\(|get_context\(' -- '*.py' ':!tests' — definitions only.
  • git grep -n -E 'recall:\{' -- core — core/__init__.py:81, no layer in the key.
  • git grep -n -F 'read_entries(user_id, "user")' -- features/inject.py — :141.
  • git grep -n -i -E 'tombstone' -- '*.py' ':!tests' — features/staging.py:42, :128 and a docstring in lifecycle/consolidation.py:193; a seven-day guard keyed on a proposal's key, not a value.
  • git grep -n -E 'assert hits == \[\]' -- tests/test_lifecycle/test_c8_tails.py — :87, :142, :149.
  • git grep -n -i -E 'arxiv|bibtex|@article|@misc|doi\.org' -- README.md docs — no match; no paper.
  • git grep -n -i -E 'anthropic|machine learning|not (be )?used (for|to)' -- LICENSE NOTICE — no match; the MIT text carries no rider.

History

2026-10-15 — 598e2ab1… — 110 commits on. negative_eval gained, earned at the first pin: the C8 search tests asserted private and hidden facts absent (section 10). Also wrong then: the inject.py:222 filter reads a field CoreEntry lacks, so six builders are unfiltered, not five; the key lookup carries no predicate; an explicit visibility lets the agent un-hide; the ledger covers only CoreMemory.save and delete (section 1). The recall cache key omits the layer at both pins (section 9). Overtaken: created_at carries source time, the multi-source retriever binds its layer (d8ec04f), forget gained dry run and undo. Screened from a full clone: ten files, two auto-run surfaces, one build-time execution point, pyproject.toml and uv.lock inside the cooldown, AGENTS.md, CLAUDE.md and .cursorrules read as data. Nothing was installed, built or run.

2026-09-19 — 32edbb91… — trust_state re-tested against the narrowed line. core/memory.py and core/__init__.py are byte-identical to the previous pin, so what follows is a correction to this report rather than drift. The mark stands: visibility is a validated four-value state, three L4 queries carry it in SQL — search (:407), the key lookup (:450) and the pinned-injection read (:348) — and a re-save with no visibility re-reads the stored flag, so a hidden key stays hidden. The report said private and hidden are excluded from both read paths. There are more than two, and the predicate is written per query rather than once.

get_all (:328-334) selects every row for the layer and user ordered by importance and takes no visibility clause. features/inject.py:222 adds the test in Python; five production callers do not — features/inject.py:165-170 (the compaction block, twenty lines from the one that does), features/smart_context.py:51-56, features/recall.py:62-64, core/__init__.py:106-108 and mcp_server/tools/ops.py:93-94, which writes the facts string into a CONTEXT.md snapshot. Each renders key=value, so a private or hidden fact above the importance threshold reaches a prompt or a file through any of them — which the comment at core/memory.py:406 says cannot happen: C8: private facts never leave the store via recall. It is true of the query it is written beside. The distiller is the one deliberate exception and says so: hidden rows stay in its novelty-gate select (lifecycle/distiller.py:244-250) so a paraphrase aimed at a quarantined key is suppressed too, and it compares against the value without emitting it.

One open question is now answered: hidden is reachable from the agent-facing tools — visibility is an argument on the MCP write tool (mcp_server/tools/memory.py:37, :78), which makes the quarantine asymmetric in the useful direction, since the re-save rule means the agent can hide a key and not unhide it. Section 1, section 9, the matrix rows and the open questions were rewritten. Screened again first; nothing was installed and no suite was run.

2026-09-16 — 32edbb91… — first reading, at a commit dated 16 September 2026. Screened before opening, from a shallow clone: eight files scanned, two auto-run surfaces, one build-time execution point, no unpinned surfaces and two dependency files inside the seven-day cooldown. Nothing was installed, built or run.