All notable changes to engram will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
The MCP tool surface is committed-stable for the v1.x lifetime per the API stability commitment in 02-TECHNICAL_DESIGN.md.
- Team-vault pre-receive hook: committer identity now resolves the GPG
primary-key fingerprint from VALIDSIG (subkey-signed pushes were
universally rejected before);
captured_byis now required on every pushed thought, and revoked or never-enrolled keys are refused. Covered by a real-GPG integration suite (ephemeral keyrings, subkey-signed commits). sync.signed_pull_requiredis now enforced on every pull path (coordinator rebase gate,explicit_pull,engram sync --pull, startup pull) via a trusted-keys verify-commit gate that fails closed on a missing/empty allow-list. The control was previously documented but inert, andverify_commit's VALIDSIG parsing could never match real gpg output.
engram reindex --fullis now index-only: it no longer rewrites the markdown source of truth, preservesupdated_at/captured_by/legacy_created_at, and no longer creates duplicate files after a body edit drifted the slug. Capture also persistslegacy_created_atinto frontmatter so the SoT carries everything a rebuild needs.- Sync coordinator surfaces
git commitfailures (re-enqueues the batch, transitions to manual-resolution) instead of silently reporting success and dropping captures from the pipeline. engram servenow loads each team-write vault's policy + members and the operator GPG identity, so team-write captures over MCP work.- Multi-statement SQLite index writes run inside real transactions;
under autocommit the previous
with conn:blocks could half-commit (thought row marked ok with no embedding row). engram doctornow runs the daemon-mode and team-vault check families (stale sockets, socket perms, enrollment, orphan quarantine, routing collisions) that previously had no callers.engram move-thoughtperforms the relocation (was a stub exiting 0);engram daemon logs --tail 0prints nothing instead of the whole log, and negative--tailvalues are rejected.
-
engram team-vault rotate-member-key <old-fp> <new-fp>(steward-only key rotation per ADR 007 Q6). Members serialization now goes through one canonical writer that round-tripssuperseded_byand force-quotes fingerprints. -
engram consolidate- report-then-action vault curation. Four detection passes (exact-duplicate keep-newest, near-duplicate clustering with LLM-distilled merge proposals, age-only stale candidates, LLM-judged contradiction candidates) produce a reviewable per-machine report;--applyexecutes merge proposals only, archiving originals body-untouched under<vault>/archive/and curating the SQLite index. Apply is a daemon-stopped one-shot holding the vault lock for its full run, journaled and resumable, with per-proposal fingerprint re-verification and one git commit per run. Merged thoughts carry provenance frontmatter (consolidated_from,consolidated_range,source: engram-consolidate) and inherit the most restrictive member portability. Stale and contradiction findings are report-only. Team-write vaults, read-only vaults, and cloud-synced paths refuse--apply. Seedocs/CONSOLIDATION.md+ ADR 009. Roadmap renumbering: the former "Phase 6 - Enterprise Scaffolding" and "Phase 7 - Enterprise Polish" shift to Phase 7 and Phase 8; Phase 6 is consolidation. -
engram consolidate --exclude-prefix(defaultSession Summary). Log-like prefixes skip the similarity passes (near-dup clustering + contradiction judging) by default - they cluster on shared template structure, not shared meaning, so merging them destroys history. Exact-duplicate and staleness passes still cover them; the exclusion is counted in the report and CLI summary; an explicit--prefixscope overrides it.
engram consolidatenear-dup threshold default raised 0.90 -> 0.93. First real-vault evidence: true duplicates scored 0.94-0.99; pairs at 0.90-0.92 needed human judgment and now flow to the contradiction judge (report-only) instead of merge proposals. Tune with--threshold.
-
Markdown rewrites preserve all non-serializer-owned frontmatter fields. Previously
update_metadata/update_body/ reindex re-capture silently droppedlegacy_created_at(and would have dropped the new consolidation provenance fields) because write-side extras preservation only kept UNKNOWN fields. The serializer now owns an explicit field set; everything else round-trips verbatim. -
Startup probes no longer warn on intentionally local-only vaults or on vaults that use
.engram/identity.localfor commit identity. Two refinements insrc/engram/sync/startup_probes.py: (1)probe_vault_identitynow returns early when the vault has no configured git remote — with no push path there is no cross-vault contamination risk to guard against, so the probe skips rather than emitting a FAIL/WARN onactual_url=None. (2)probe_user_identitynow consults.engram/identity.localbefore warning about an unsetgit config --local user.{email,name}. When identity.local provides bothuser_emailanduser_name, the sync coordinator already uses those values when committing, so the warning is redundant. Net effect:engram doctorandengram servestartup are quieter on two valid configurations they previously over-reported on. -
engram daemon stopcleans stale state file + socket when the daemon was already dead. The dead-PID branch insrc/engram/cli/daemon.py(ProcessLookupErrorfromos.kill(pid, SIGTERM)) previously echoeddaemon for <vault> was already stopped (pid N)and returned, leaving<vault>/.indexes/engram.state.jsonand<vault>/.indexes/engram.sockon disk. The nextengram daemon startthen saw stale artifacts and got confused. The dead-PID branch now unlinks both before returning so the nextstartsees a clean slate. -
engram daemon stopexits promptly with active proxy connections. On macOS, when the daemon had multiple idle UDS connections registered with its kqueue selector, an externally deliveredSIGTERMwas deferred until an unrelated event (typically a proxy keepalive ~60s later) woke the selector — leading to the observed 60s+engram daemon stophang. Two-part fix inDaemonServer(src/engram/daemon/server.py): (1) a periodic 1-second wakeup pump (_signal_wakeup_pump) guarantees the selector returns regularly so pending Python signal handlers fire during the eval loop's pending-signals check; (2)_install_async_signal_handlersnow installs bothloop.add_signal_handler(the canonical asyncio path that wires up the wakeup-fd) AND a synchronoussignal.signalfallback that schedulesrequest_shutdownviacall_soon_threadsafe— the fallback fires reliably from the pending-signals check even when the wakeup-fd dispatch path is unreliable. Time-to-stop with 4 active connections drops from ~60s to <1s. Regression test:test_engram_daemon_sigterm_with_active_connections_exits_promptly. -
Daemon readiness pipe survives
exec; defensive guard against stale-fd writes. Latent bug exposed by the MCP-aware proxy: every MCP tool call against a proxy-spawned daemon returneddisk I/O errorfrom SQLite. Root cause: in_spawn_daemon_process(src/engram/daemon/client.py), the proxy creates the readiness pipe viaos.pipe()and passes the write-end's fd number to the daemon via--readiness-fd N. Since PEP 446 (Python 3.4+),os.pipe()fds are non-inheritable by default — the kernel closes them atexec. The daemon received a stale fd number; SQLite later openedengram.dbat that (now-free) fd; the daemon's subsequentos.write(N, b"ready\n")corrupted the first 6 bytes of the db file andos.close(N)closed SQLite's main-db descriptor from the OS's view. SQLite continued to think fd N was open, and every subsequent query failed with EBADF surfaced assqlite3.OperationalError("disk I/O error"). Fix (primary): callos.set_inheritable(wfd, True)in the child branch beforeexecvpe. Fix (defensive): inDaemonServer.serve_forever,os.fstatthe readiness_fd and verifystat.S_ISFIFObefore writing; on non-pipe, log a warning and skip — preventing the same class of bug from ever clobbering a reused fd again. Regression tests cover both the inheritability contract and the defensive skip path. -
engram serveproxy is now MCP-aware enough to survive daemon restarts without losing the MCP session. Follow-up to the reconnect fix below. Even after the proxy reconnects to a freshly-restarted daemon, the new daemon has no record of the originalinitializehandshake; the very next client request (typicallytools/list) was rejected with JSON-RPC-32602 Invalid params("server not initialized"). Fix: a new_FrameSnooperobserves client→server frames as a tee — the byte stream is not modified — and caches the latestinitializerequest andnotifications/initializednotification. On successful reconnect,_replay_mcp_sessionre-sends both to the new daemon and swallows the duplicateinitializeresponse (Claude already received the original; a second response with the same id would confuse its JSON-RPC client). Failure modes degrade gracefully: snooper never raises, replay timeout is bounded at 2s with a warning, and if noinitializewas ever seen the replay is a no-op. Also de-flaked the existingtest_run_proxy_loop_returns_1_when_reconnect_exhauststest, which was racing the proxy's first reconnect attempt (jitter up to 2s) against a wall-clockasyncio.sleep(0.2); now closes the listener synchronously inside the connection handler so the close is causally ordered. -
engram serveproxy now reconnects across daemon restarts. Prior behavior: when the daemon died mid-session (crash, restart, idle shutdown), the proxy's byte shuffler observed UDS EOF and exited cleanly with rc=0. Claude Code's MCP client saw its server vanish and dropped all engram tools from the registry - tools didn't come back without restarting Claude Code. The_reconnect_with_backoffhelper existed inengram.daemon.client(matching the docstring contract for mid-session reconnection) butrun_proxy_loopnever called it. Fix:_shuffle_bytesnow returns a_ShuffleExitenum signalling whether stdin (Claude closed) or the socket (daemon died) closed first;run_proxy_loopreconnects with backoff on the socket case and exits cleanly on the stdin case. The stdin-closed path preserves the existing "drain remaining socket response before exit" behavior via UDS half-close + 2s drain budget. Caveat: MCP session state (initialize / tools-list) is held by the daemon; if Claude Code's MCP client doesn't re-initialize on transport hiccup, some session-state-dependent behavior may still need manual recovery after a daemon restart. The proxy-side fix is the prerequisite for any further session-state work.
engram doctordisk_usagecheck removed. The check was added in the SQLite resilience pass on the theory that high disk usage predicts SQLite EIO. In practice it fires on macOS at 93%+ used even when Finder shows hundreds of GB "Available" - because macOS Finder includes purgeable space (Time Machine local snapshots + caches APFS can free on demand) that the doctor'sshutil.disk_usage.freedoes not. Operators reasonably read the warning as a false positive and lose trust inengram doctorat large. The actual SQLite EIO defense lives in the retry-with-backoff on transientSQLITE_IOERRplus thePRAGMA busy_timeout=5000, both shipped in the same pass and unchanged. If a future incident shows the retry path was insufficient, we can revisit with a smarter purgeable-aware check; until then noise > signal.engram daemon start(without--detach) now logs a one-line hint to stderr noting that the terminal will block until Ctrl-C and pointing at--detachfor background operation. Operators reasonably mistake the foreground-blocking behavior for a hang (the CLI process literally transitions into the daemon process; it does not fork). The hint is suppressed when the daemon is being spawned via the proxy's internal dance (detected by the presence of--readiness-fd).engram daemon stopno longer hangs when a proxy is attached. The daemon'sserve_foreveraccept loop usedasync with self._server:whose__aexit__callsServer.wait_closed()and blocks until every accepted connection handler finishes. Handlers were awaiting MCP frames from still-open proxy streams (e.g. an active Claude Code session), so they could not return on their own; the daemon had to burn the fullshutdown_drain_secondsbudget before the cancel fallback fired - and in pathological cases (long-uptime daemon, proxy stuck mid-RPC) the total drain exceeded the CLI's 60s timeout, forcing--force(SIGKILL). Fix: cancel in-flight handler tasks immediately when the shutdown event fires, BEFORE exiting the listener context.wait_closed()then returns promptly and_drain_and_exitruns the budgeted cleanup. Measured graceful shutdown on a personal vault with an attached proxy dropped from ~5s (drain-budget-exhausted) / 60s+ (timeout-and-force) to ~4s end-to-end including the CLI's 0.2s pid-alive polling.
delete_thoughtMCP tool with a mandatory two-call confirmation contract. Callers passconfirm: boolexplicitly (no default). The first call (confirm=False) returns metadata + a ~200-char body preview without modifying the vault; the second call (confirm=True) removes the markdown file, the SQLite row, and the embedding row, and enqueues a git commit via the sync coordinator. Unknown ids returndeleted: falsewith a"Not found"message (not an error). Tool count rises from seven to eight (six core + two LLM).engram delete <id>CLI with a typed-string (delete) confirmation gate.--dry-runprints the preview without modifying the vault;--yesskips the prompt (intended for CI / scripts that have already validated the id).ThoughtNotFoundErrorinengram.errors(error_codethought_not_found) — raised byVaultStorage.delete()when the id is absent. The MCP and CLI handlers shield callers from the exception and surface a user-visible "Not found" message instead.engram.utils.lock.serve_lock_metadata(vault_path)helper that returns the per-vault serve-lock metadata dict (orNoneif not held), without acquiring the underlyingflock. Used by one-shot CLI mutating commands to detect a running daemon before opening a second SQLite connection.CaptureOutput.index_statefield ("ok"|"failed") on the MCPcapture_thoughtresponse. Surfaces when the markdown was written but the SQLite index insert raised so AI clients can warn the operator that the thought won't appear in search/list untilengram reindexruns. Markdown SoT is unchanged. Additive to the wire format with a safe default ("ok") per the v1.x stability commitment - existing clients ignore the field with no behavior change.engram doctororphan_markdowncheck that walks the markdown tree and counts files whose ID has no SQLite row. Surfaces as a WARN with up to 10 example IDs and the remediation hint to runengram reindex. Complements the existingorphan_rowscheck (SQLite-rows-pointing-to-missing-markdown). Catches accumulated drift that escaped the in-the-momentCaptureOutput.index_statesignal (e.g. vaults imported from another tool, or vaults that ran on an older engram before the field existed).PRAGMA busy_timeout=5000on every SQLite connection. Rides out brief contention (concurrent writer / WAL checkpoint) before raisingSQLITE_BUSY. Five seconds covers typical contention; persistent failures still surface as exceptions to the caller.- Retry-with-backoff on transient SQLite errors in
insert_thought. Three attempts total with exponential backoff (100ms -> 200ms). Retries onSQLITE_BUSY(5),SQLITE_LOCKED(6), and theSQLITE_IOERRfamily (10) via Python 3.11'ssqlite_errorcodeattribute. Logic errors (SQLITE_CONSTRAINT,SQLITE_CORRUPT, etc.) propagate immediately - retrying cannot help. Each retry emits a WARN log line so the operator can see contention bursts in the daemon log. (added then removed in the same release - see the Removed section above for the rollback rationale).engram doctordisk_usagecheck
VaultStorage.delete(thought_id, *, source="api")now returns the deletedThought(previouslybool) so callers can emit a confirmation that includes prefix + portability without a separate lookup. The method emits a structured INFO log line (thought_deleted id=... prefix=... portability=... fingerprint=... vault=... source=...) and forwards the deleted thought to_post_capture_syncso the sync coordinator stages the git removal. Existing callers that ignored the return value (CLI move-thought) continue to work; callers that relied on the boolean falsy return for "not found" must catchThoughtNotFoundErrorinstead.engram deleteandengram reindexnow refuse (exit 2) when the per-vault serve lock is held. Both commands open a fresh SQLite connection and walk the index; running concurrently with the daemon can wedge the daemon's WAL handle and silently drop in-flight capture rows. The error message names the lock path + PID and directs the operator to stop the serve loop first.engram syncandengram bundlealready had equivalent refusals; this brings the remaining two mutating one-shot CLIs into line.engram syncandengram bundlemigrated to the sharedserve_lock_metadatahelper (no behavior change). Replaces the two bespoke per-CLI implementations of the lock check with a single source of truth.engram bundle's refusal message now also shows the holder PID for parity with the other three (sync,delete,reindex); exit code (2) and lock-not-held behavior unchanged.VaultStorage.capture()accepts an optionalon_index_failurecallback (Callable[[Thought, sqlite3.Error], None] | None, defaultNone). When supplied, the callback fires if the SQLite insert raises - the capture still returns the Thought, the markdown is still on disk, and the historical log-and-continue behavior is preserved when no callback is registered. Used by the MCPcapture_thoughthandler to populate the newindex_statefield; internal callers (move_thought,reindex, migration, tests) keep their existing semantics with zero signature changes. Callback exceptions are caught and logged so they cannot mask the original capture outcome.
The big change in v0.5.0: N concurrent Claude Code sessions can now
attach to the same engram vault simultaneously. Pre-Phase-5,
engram serve held the per-vault advisory lock for its lifetime;
the second concurrent session against the same vault failed with
LockError. Phase 5 introduces a per-vault daemon process and
makes engram serve a thin proxy that auto-spawns the daemon on
first invocation. Existing MCP configurations need no edits.
- Daemon subpackage (
src/engram/daemon/): per-vault UDS daemon, proxy client, fastmcp dispatch shim, spawn helpers, state file, log rotation handler. engram daemonsubcommand group with 4 subcommands (start,stop,status,logs).engram daemon status --jsonprovides a machine-readable status payload with the not-running shape per spec Amendment 7.engram daemon logs --followtails the daemon's log file with inode-reopen semantics for rotation handling.DaemonConfigPydantic model with 14 tunable fields (idle-shutdown, spawn-timeout, WAL-recovery grace, drain budgets, frame-size cap, log rotation knobs, content redaction). Full reference indocs/DAEMON_MODE.md.- 5 new daemon error classes:
DaemonError,DaemonSpawnError,DaemonConnectionError,DaemonNotRunningError,PeerCredRejectError— each with a stableerror_codeso MCP clients can route consistently. - 6 new doctor check codes:
daemon_running,daemon_socket_permissions,daemon_socket_stale,daemon_log_rotation_healthy,daemon_uptime_excessive,daemon_socket_path_too_long— surfaced viasrc/engram/diagnostics/daemon_checks.py. - Peer-credential check (
SO_PEERCREDon Linux,getpeereidon macOS) on every accepted UDS connection; belt-and-suspenders on top of the socket's 0o600 perms. - Hermetic CLI smoke (
tests/test_phase5_cli_smoke.py): 8 new subprocess-based smokes against the installed binary, including a fulldaemon start --detach→status→stopround-trip. - ADR 008 (
docs/adr/008-daemon-mode.md) captures the per-vault topology + auto-spawn + UDS rationale plus alternatives considered. - Operator + migration guide (
docs/DAEMON_MODE.md) documents the upgrade procedure, daemon lifecycle, status output, troubleshooting, fullDaemonConfigreference, and the v0.4.x downgrade procedure.
user_config_vault_name_mismatchdoctor check (src/engram/diagnostics/phase3_checks.py): WARNs when thename:field in~/.config/engram/config.yamldoes not match thevault_name:in the vault's ownengram.config.yaml. This mismatch previously surfaced as an opaqueVaultError: primary vault already mountedat daemon startup with no pointer to the fix.engram doctornow names the mismatch and shows the corrective action.ALL_PHASE_3_CHECK_CODESgrows from 22 to 23 codes.
engram servedefault behavior: now runs in proxy mode (auto-spawns a per-vault daemon and shuffles bytes between stdin/stdout and the daemon's UDS). The pre-Phase-5 single-process stdio path is preserved bit-for-bit behind a new--no-daemonflag.VaultLock.__init__now acceptsinstall_signal_handlers: bool = True. Daemon use cases passFalseso the daemon can own its own SIGTERM/SIGINT handler (spec Amendment 1).ServeRuntime.embeddertype loosened fromFastEmbedProvidertoobjectto match engram's existing duck-typed embedder convention (mirrorsserve_multivault).cli/serve.pyfactored:_init_serve_runtime(async)ServeRuntimedataclass +ServeInitErrorare now the shared entry betweenserve --no-daemonandengram daemon start.
DaemonConfig.idle_shutdown_secondsdefault changed from3600to30. The daemon now exits ~30 seconds after the last Claude session closes, matching the expected behavior that the daemon only runs while Claude is running. Users who want the old always-warm behavior can setdaemon.idle_shutdown_seconds: 3600(or any value, including0for never-auto-shutdown) in their vault'sengram.config.yaml._attach_daemon_log_handler(cli/daemon.py): now callsos.setsid()after attaching the rotating log handler, placing the daemon in a new process group. Also redirects fd 1 and fd 2 to/dev/null. Without this, SIGTERM sent to the spawning proxy's PGID on Claude Code session close also killed the daemon; and writes to the inherited stdout after the proxy exited triggeredBrokenPipeError→SystemExit(1)in the rich console handler.DaemonServer._drain_and_exit(daemon/server.py): idle- shutdown timer is now cancelled as the first step of the drain sequence (step 0) before the listener close, preventing asyncio from logging "Task was destroyed but it is pending!" during loop teardown.VaultStorage.close(storage/facade.py): issuesPRAGMA wal_checkpoint(TRUNCATE)before closing the SQLite connection. A crash that left a stale WAL/SHM behind causedSQLITE_IOERRon every tool call in the next daemon startup.
No MCP config edits are required. The engram serve command your
client invokes today runs in proxy mode by default in v0.5.0 and
auto-spawns the daemon on first use. See docs/DAEMON_MODE.md for
the full upgrade procedure with verification steps.
If you ever need to downgrade:
engram daemon stopfirst — v0.4.x does not know about daemon mode andengram servewill fail withLockErroruntil the v0.5.0 daemon is gone.- Remove any
daemon:block fromengram.config.yaml— v0.4.x's Pydantic model isextra="forbid"and refuses configs with the block present.
(Closes risk M6 in the design spec.)
The pre-2026-05-12 roadmap's "Phase 5 — Enterprise Scaffolding" and
"Phase 6 — Enterprise Polish" are renumbered to Phase 6 and Phase 7
respectively. The new Phase 5 is daemon mode. The roadmap files in
docs/superpowers/specs/2026-05-04-engram/ (gitignored local
artifact) reflect the renumber.
Design spec lives locally at
docs/superpowers/specs/2026-05-12-engram-daemon-mode-design.md
(gitignored under docs/superpowers/). The execution plan
docs/PHASE_5_PLAN.md is committed for traceability.
engram summarize <thought-id>CLI — wraps thesummarize_thoughtMCP handler so operators can invoke single-thought summarization from a terminal (per01-PRODUCT_SPEC.mdF11). Honors the same provider resolution + portability gate + cost cap as the MCP tool.engram synthesize "<query>"CLI — wraps thesynthesize_thoughtsMCP handler with--k,--vault-filter,--include-sensitive,--include-friend-vaults, and--jsonflags.engram doctor --print-hashesmaintainer flag — recomputes the cached FastEmbed model file hashes in manifest-ready format. Used after a model upgrade to refreshengram/embedding/model_hashes.py.- FastEmbed model integrity verification is now active. The
BAAI/bge-small-en-v1.5hash manifest is populated; mismatched files raiseEmbeddingErrorand refuse to load. Empty manifests (for unpinned models) keep the original trust-on-first-use behavior with a single WARNING log. FastEmbedProvider.list_cached_files()+_resolve_snapshot_dirhelper — handles HuggingFace'smodels--<org>--<repo>/snapshots/<sha>/cache layout so verification + hash printing find the actual files.
team-writerole forVaultMount.rolejoiningprimaryandread-only. N team-write vaults may be mounted alongside the singleton primary; team-write requiresremote_url(refused at config-load withteam_write_requires_remoteif absent). Canonicalvault_id = sha256(remote_url)[:16]derived at validation time.- Per-prefix routing rules +
auto_routeopt-in flag in~/.config/engram/config.yaml. Captures matching aRoutingRuleprefix auto-route to the rule's target vault unless an explicitvault:arg orblock/sensitiveportability overrides per the pinned invariants 1+2. - Capture-time gate composition
(
engram.team.capture_gate.gate_team_capture): the read-only-role refusal + member-enrollment assert + policy refuse-or-pass +captured_bystamping client-side gate. - Sender attribution:
Thought.captured_by+Frontmatter.captured_byfields. Set automatically when capturing into ateam-writevault; None for personal captures and Phase 1+2+3 thoughts (backwards compatible). SQLite migration adds the column with NULL default. - GPG identity wrapper (
engram.team.identity.GpgIdentity):gpg --list-secret-keys --with-colonsdiscovery + subkey-to-primary resolution.assert_member_enrolledrefuses unenrolled fingerprints withTeamMemberNotEnrolled. - Persistent push queue
(
engram.team.push_queue.PersistentPushQueue): durable per-vault queue at<vault>/.engram/push-queue.local. Survives engram restart; partial-line tolerance from SIGKILL mid-write; orphan-on-auth-failure to<personal>/.engram/orphans/<id>.tar.gz; disk-full at enqueue raisesPushQueuePersistenceFailed(capture refusal upstream). - Routing dispatcher (
engram.team.routing.resolve_target_vault): 5-rule precedence (block-veto, sensitive-fall-through, explicit-arg- wins, auto-route-match, fallback-primary) + multi-prefix tie-break (first prefix wins) + rule priority tie-breaker + ambiguity refusal withRoutingRuleAmbiguous. engram team-vault setupCLI: bootstraps a team vault by writing 5 canonical files (engram.config.yaml,.engram/team-policy.yaml,.engram/members.yaml,.gitignore,.engram/setup_completesentinel). Idempotent + resume-after-partial.engram team-vault enroll-keyCLI: discovers operator's GPG primary fingerprint and prints next-steps.engram team-vault add-memberCLI: steward-only enrollment helper. Writes line-level-merge-friendly YAML.engram team-vault revoke-keyCLI: steward-only revocation. Historical thoughts under the revoked key remain attributable; new captures refuse withteam_member_not_enrolled.- Server-side
pre-receivehook bundle (engram/team/server_hooks/pre_receive.py): stdlib-only Python 3.10+ script. Refuses.indexes/paths,blockportability,attribution_committer_mismatch, disallowed prefixes, force-push, and non-steward mutation of policy/members. Lists ALL violations on rejection (not just the first). Hand-rolled restricted YAML parser (no PyYAML dep). - Phase 4 doctor codes (9 new):
multiple_team_write_vaults_ok,team_member_not_enrolled,team_pending_pushes,team_membership_revoked,team_policy_violation_quarantined,serve_config_stale,routing_rule_priority_collision,team_vault_embedding_mismatch,git_branch_drifted.ALL_PHASE_4_CHECK_CODESsuperset has 32 codes total (updated from 31 whenuser_config_vault_name_mismatchwas added to Phase 3 in v0.5.0). - 11 new errors:
TeamMemberNotEnrolled,TeamPolicyViolation,RoutingRuleAmbiguous,RoutingTargetNotMounted,BlockThoughtInTeamVaultDisallowed,TeamVaultEmbeddingMismatch,TeamMembershipRevoked,AttributionCommitterMismatch,TeamWriteRequiresRemote,TeamVaultAlreadyInitialized,PushQueuePersistenceFailed. - CaptureInputMetadata.vault field (additive). Phase 1+2+3 clients omitting it see Phase 3 semantics unchanged (pinned invariant 6).
- ADR 007 - Team Brain documenting D1-D9 design decisions.
- TEAM_BRAIN_GUIDE.md operator setup walkthrough (GPG bootstrap, setup, hook install per platform, member add, revocation, disaster recovery).
- MULTI_VAULT_SETUP.md updated with Phase 4 role taxonomy table.
- Two-layer enforcement boundary (pinned invariant 4): client-side is canonical for capture-time policies (block routing, member enrollment); server-side hook is canonical for push-time policies (allowlists, attribution integrity, force-push refusal, steward-only mutation). The two layers compose - a single bypass doesn't breach the boundary.
- GPG-fingerprint-bound attribution: replaces free-form
default_userfor team-vault sender id. Refuses captures whosecaptured_bydoesn't match the local GPG primary fingerprint (client-side) and refuses pushes whosecaptured_bydoesn't match the GPG-signed git committer fingerprint (server-side). - Steward-only mutation of
team-policy.yaml+members.yaml: enforced by the pre-receive hook reading the OLD tree's stewards list and refusing pushes from non-stewards. .indexes/push refusal: defense-in-depth; canonical.gitignorePLUS server-side hook refusal (P4-H1 mitigation).- Force-push refusal:
denyNonFastForwardsPLUS server-side hook check (P4-H3 mitigation).
- VaultRegistry (
engram.multivault.registry): in-process resolver mapping vaultname-> openVaultStorage+ role (primary/read-only) + optional sync coordinator. Mount-time realpath collision check (canonical enforcement point per ADR 006 D1). At-most-one-primary invariant + read-only-vault hard-refusal flag plumbing. - Cross-vault aggregator (
engram.multivault.aggregator):aggregate_searchwith portability push-down at the per-vault Filter (blockNEVER returned regardless of any flag); per-vault floor (default 3); ATTACH-vs-SEQUENTIAL mode at the 11-vault threshold;degraded_vaultslist for per-vault timeout. Embedding-model compatibility check on every cross-vault invocation. - Defense-in-depth portability gate
(
engram.multivault.portability):assert_no_block_in_resultsstrip_block_thoughts+split_portabilities.
- Bundle export / import
(
engram.bundle.{format,exporter,importer}): on-disk format ismanifest.json+thoughts/<rel>.mdin a streaming tar.gz. Per-file 1 MB / per-bundle 4 GB caps; manifest written LAST; importer enforces NFC + path-traversal refusal + YAML safe-loadportability=blockfilter + id-collision pre-flight (atomic)bundle_id-chain cycle detection.
engram export/engram importCLI (engram.cli.bundle): default portability filter isportableonly;--portabilityis repeatable; refuses while serve holds the per-vault lock;--allow-read-onlyopt-in.- LLM provider abstraction (
engram.llm): five adapters (Anthropic / OpenAI / Ollama / llama.cpp / OpenAI-compatible) + MockProvider for tests +resolve_providerper-thought portability gate.base_urlvalidated against~/.config/engram/trusted-llm-urls.yaml. LLMBudget(engram.llm.budget): per-day cost cap persisted to<primary>/.indexes/llm_usage.json;truncate_to_budgetpreserves per-vault floor.- Citation post-validator (
engram.llm.citations): strips hallucinated UUID-shaped citations from LLM responses. summarize_thought+synthesize_thoughtsMCP tools (engram.mcp.llm_tools): per-thought summary + cross-vault RAG with default-off friend-vault inclusion (B-4 fix), anti-injection delimiter wrap + system prompt, citation post-validation.- Multi-vault server (
engram.mcp.server.build_multivault_server): registry-routed 5 stable tools + 2 additive Phase 3 LLM tools. - Multi-vault serve startup
(
engram.cli.serve_multivault): testable orchestrator for the Step 17 ordering. - Phase 3 doctor extensions (
engram.diagnostics.phase3_checks): eight new checks corresponding to the eight new Phase 3 codes. - Phase 3 errors (
engram.errors):VaultReadOnlyError,VaultPathCollision,DuplicateVaultName,EmbeddingModelMismatch,BundleImportError,BundleCycleDetected,BlockThoughtLLMDisallowed,LLMProviderError. - Phase 3 config:
AggregatorConfig, newLLMConfigfields,UserConfig._check_one_primary_vaultvalidator,EffectiveConfig.aggregator+.vaults. - Documentation:
docs/adr/006-multi-vault-and-llm.md,docs/PHASE_3_CODE_COMPLETE.md,docs/MULTI_VAULT_SETUP.md,docs/FRIEND_SHARE_GUIDE.md,docs/LLM_FEATURES.md.
EffectiveConfignow carriesaggregatorandvaultsfields populated by the loader fromUserConfig.VaultStorageacceptsread_only_role: bool; every public write entry-point gates on this flag.engram.diagnostics.check_codesexposesALL_PHASE_3_CHECK_CODES(14 Phase 2 + 8 Phase 3 = 22 codes).- Pre-existing fingerprint property test fixed: hypothesis can generate mixed line-ending content; test now normalizes to LF-only first, then exercises the three transforms.
- Per-thought portability gate at the LLM layer (
blockalways refuses;sensitiverequires local provider). - Friend-vault content excluded from LLM RAG by default
(
include_friend_vaults=False). - Anti-injection delimiter wrap + system prompt + citation post-validator.
- Bundle import gate: path-traversal, oversize, YAML safe-load, block filter, id-collision atomicity, cycle detection.
- Read-only vault hard refusal at storage write boundary.
- LLM
base_urltrust file (separate from main config).
- Sync coordinator state machine (
engram.sync.coordinator) with 10 explicit states (IDLE,DEBOUNCING,COMMITTING,COMMITTED_NOT_PUSHED,FETCHING,PUSHING,PAUSED_FOR_MIGRATION,AUTH_REQUIRED,MANUAL_RESOLUTION_REQUIRED,DISABLED). Allowed transitions encoded inALLOWED_TRANSITIONS; disallowed transitions raiseSyncError. Owns asyncio queue + lock + ring buffer of last 256 events. Debounce window (default 60s) coalesces rapid captures; max-deferral ceiling (default 300s) flushes long bursts. - Typed async git wrapper (
engram.sync.gitops) over the Phase 1run_githelper withGitErrorClassclassification (AUTH, NETWORK_TRANSIENT, NETWORK_PERMANENT, NON_FAST_FORWARD, CONFLICT, LOCK_HELD, UNKNOWN). Functions:is_inside_work_tree,current_branch,remote_url,default_remote_branch,status_porcelain(porcelain v1 -z),ahead_behind_count,commit_paths,fetch,pull_rebase,push(withforce_with_lease+set_upstream),verify_commit,git_version. - Conflict marker scanner (
engram.sync.gitops.conflict_marker_scan): whole-file walker requiring BOTH<<<<<<<AND>>>>>>>markers; the lone hunk separator is a markdown horizontal-rule and does NOT trigger. - Per-vault identity check (
engram.sync.identity) reading.engram/identity.local(machine-local; gitignored). Defends against R-H3 cross-vault contamination by refusing to push when the resolvedoriginURL does not matchexpected_remote_pattern. - R-M9 reflog gate: before any pull-rebase, the coordinator
captures the previous
origin/<branch>SHA, fetches, then asserts reachability viagit merge-base --is-ancestor. If unreachable (force-push detected upstream), refuses to auto-rebase and transitions toMANUAL_RESOLUTION_REQUIRED.--force-with-leaseis the only force semantics ever invoked. - MigrationLock (
engram.utils.lock.MigrationLock): separate flock fromVaultLock;MigrationLock.is_held()cross-process probe. Coordinator parks inPAUSED_FOR_MIGRATIONwhile migration is running, resumes on release. - 14 startup probes (
engram.sync.startup_probes) mapping 1:1 to doctor check codes: git version floor, autocrlf drift, LFS drift on*.md, branch alignment, submodules underthoughts_dir, remote default-branch match, gitignore required entries (.indexes/+*.sqlite*), cloud-sync under.git/, GPG agent reachable, vault identity remote match (R-H3), per-vault user identity (R-M14), working-tree-dirty at startup (R-M12), read-only role contradicts auto-push, signed commits required. Per-cycle re-runs of probes 7 + 11 catch mid-session admin changes. engram clone-vault <url> <local_path>(Step 14 / R-H1):git clone --no-checkout-> delete.git/hooks/->git checkoutso a malicious post-checkout hook in the remote never executes. Writes a starter.engram/identity.localtemplate.engram syncsubcommand with--pull/--push/--first-push/--resume(default = pull-then-push). Refuses to run while the per-vault flock is held byengram serve.engram sync compactquarterly maintenance:git gc --autogc.reflogExpire=30.days.ago(L2/L3 mitigation).
engram servestartup ordering (Step 17): startup probes before lock; conflict-marker scan after lock; coordinator built + attached toVaultStorage; drain on shutdown.- 14 doctor sync checks (
engram.diagnostics.doctor.run_sync_diagnostics) reusing the probe logic. Non-git vaults emit OK rows for every Phase 2 code rather than failing checks that do not apply. - ADR 005 documenting the state machine + cross-vault contamination guard + force semantics + conflict-marker handling.
docs/MULTI_MACHINE_SETUP.md: operator-facing setup guide.docs/PHASE_2_CODE_COMPLETE.md: Phase 2 exit-criteria validation paralleling the Phase 1 doc.
engram.config.SyncConfigextended with 11 new Pydantic-validated Phase 2 fields:role(primary|read-only),disabled,debounce_window_seconds,max_deferral_seconds,push_retry_count,push_retry_backoff_seconds,push_timeout_seconds,allow_unsigned,use_no_verify,signed_pull_required,expected_remote_pattern.engram.storage.facade.VaultStoragegains an optional_sync_coordinatorattribute andset_sync_coordinator()injection point._post_capture_syncforwardsthought.file_pathtocoordinator.enqueuewhen attached; unit tests stay hermetic by leaving the coordinator unset.
- R-H1:
clone-vaultdeletes hooks BEFORE checkout. Verified by test that plants a maliciouspost-checkouthook in the bare source and asserts the sentinel file is never written. - R-H3:
vault_identity_remote_matchprobe refuses pushes to a remote URL that does not match the per-vault identity pattern. - R-H6:
conflict_markers_presentdoctor + startup check refuses to serve a vault containing literal merge markers. - R-H7:
cloud_sync_under_dotgitprobe FAILs vaults whose.git/resolves under a known consumer cloud-sync provider. - R-M9: reflog gate refuses to auto-rebase across an upstream history rewrite; the operator must intervene manually.
- Initial project scaffold per
10-CODE_QUALITY.md:pyproject.toml(PEP 621),rufflint + format config,mypystrict mode,pytestconfig with coverage,pre-commithooks, GitHub Actions CI matrix (Python 3.11 + 3.12 across macOS + Ubuntu), Apache-2.0 license,README.md,CONTRIBUTING.md, this changelog. - Phase 1 implementation plan (
docs/PHASE_1_PLAN.md) authored viasuperpowers:deep-planwith critique pass; 21 ordered steps across 8 layers. - Layer 0 foundations:
engram.logging- structlog config writing only to stderr; secret-shaped keys (api_key, token, password, x-brain-key, etc.) redacted before any renderer runs; text or JSON output.engram.errors-EngramErrorbase + 7 typed subclasses, each with a stableerror_codefor MCP error mapping.engram.utils.atomic_write- durable atomic file writes for the markdown SoT layer; tempfile in same directory as destination,F_FULLFSYNCon macOS, parent-directory fsync after rename, mode 0600.engram.utils.fingerprint- canonical body fingerprint per02-TECHNICAL_DESIGN.md: SHA-256 over normalized body (line-ending normalization, trailing-whitespace strip per line, trailing-blank-line strip, UTF-8 encode).engram.utils.file_naming-{prefix-dir}/{YYYYMMDDHHMMSS}-{slug}-{shortuuid12}.mdderivation; slug fallback tothought; UUID-v7 last-12-hex tail; path traversal + RTL-override character rejection.engram.utils.run_command- safe subprocess wrapper enforcingshell=False;run_githelper pre-stages the four non-interactive env vars (GIT_TERMINAL_PROMPT=0,GIT_MERGE_AUTOEDIT=no,GIT_ASKPASS=true,GIT_LFS_SKIP_SMUDGE=1) per02-TECHNICAL_DESIGN.mdFlow C.
- 109 tests covering all of the above plus property-based (hypothesis) tests for fingerprint stability, atomic-write byte/text round-trip, and filename uniqueness across 2000-capture batches.
engram.utils.lock- per-vault advisory lock at<vault>/.indexes/engram.lockusingfcntl.flock(LOCK_EX | LOCK_NB)so the kernel arbitrates between concurrent processes attempting to serve the same vault. Diagnostic JSON metadata (pid, hostname, acquired_at, version) for "who holds the lock" reporting; cross-host vs same-host detection;--forceoverride unlinks and retries once. Stale locks self-recover via flock semantics on FD close. atexit + SIGTERM/SIGINT cleanup hooks; signal handlers restored on release. Concurrent-process test uses subprocess.Popen so flock is exercised across kernel-arbitrated FDs.engram.models- Pydantic v2 boundary types:Frontmattermodel with strict validation (canonical + custom prefixes accepted; path-traversal, NULL, RTL-override unicode rejected; portability Literal-typed; tz-aware datetime enforced; 64-hex-char fingerprint pattern;schema_versiondefaults to 1 per NFR5;extra="allow"so unknown future fields round-trip).CANONICAL_PREFIXESconstant (15 values) andDEFAULT_PORTABILITY_BY_PREFIX(Domain and Artifact default tosensitiveper BYOC).ThoughtandThoughtWithSimilarityruntime objects withvaultandlegacy_idfields present from v1.0 for forward compat (R29).CaptureInput,CaptureOutput,SearchInput,SearchOutput,ListInput,ListOutput,StatsOutput,FetchInput,FetchOutput,Filter,PortabilityCounts,SortOption- one-to-one MCP API contract per02-TECHNICAL_DESIGN.md.
- 192 tests total at this checkpoint (109 from layer-0 + 13 lock + 70 models), coverage 94.15%.
engram.config- five-layer config loader + Pydantic models:- Models:
SyncConfig,LLMConfig,VaultMount,UserConfig,VaultConfig,EffectiveConfig. Thellm:block is reserved per02-TECHNICAL_DESIGN.mdOptional LLM-Mediated Features; Phase 1 parses but ignores it at runtime. load_config()implements the full precedence (defaults -> per-user YAML -> per-vault YAML ->ENGRAM_*env -> CLI flags) via two-pass load: Pass 1 resolves the vault path from per-user config +--config/--vaultflags; Pass 2 loads the per-vault YAML at the resolved path.resolve_default_user()priority: CLI > env > per-user YAML > devkit~/.config/devkit/identity.jsongithub_username>$USER> literalengram-user. Devkit identity.json is a soft dependency; absence, malformed JSON, and missing field all fall through silently.ensure_user_config_dir()creates~/.config/engram/with mode 0700 per06-SECURITY.mdBoundary B1.- Fatal errors with clear messages for: no vault configured, vault directory
does not exist,
--configfile does not exist, novaults:list, no primary vault marked, requested--vaultnot in list.
- Models:
- 239 tests total now (47 new config tests), coverage 94.22%.
engram.storage.sqlite- SQLite + sqlite-vec connection factory and schema:- Three spec-defined tables (
thoughts,thought_embeddingsvirtual,migrations) plus a Phase 1engram_settingsKV table that records embedding model name, embedding dimension, and sqlite-vec version (Risk R22 mitigation; uses settings rows instead of an undocumented schema table). - sqlite-vec extension probe + load via
sqlite_vec.load(conn). Detects when Python's stdlib sqlite3 lacks loadable-extension support and raises a clearIndexErrorwith a remediation pointer to uv-managed Python. - Dimension and model-name mismatch detection on reopen: changing the
embedding model raises
IndexErrordirecting the user toengram reindex --full --model <new>. - Schema version tracked via
PRAGMA user_version. WAL journal mode. Database file mode 0600 on POSIX.
- Three spec-defined tables (
engram.storage.sqlite_queries- parameterized query helpers:insert_thought(atomic row + optional embedding insert),get_thought_row,list_thoughts(returns rows + truetotal_countpre-pagination),search_thoughts_by_vector(sqlite-vec ANN with metadata filter; returns rows + cosine similarity in [0, 1]; pending rows excluded),update_thought_metadata,update_thought_body,delete_thought,upsert_embedding,mark_embedding_status,list_thoughts_with_status(for doctor --repair),get_stats,record_migration_start/record_migration_complete,iter_all_thought_paths.- Tag filtering uses
json_eachrather thanLIKE '%"x"%"so adversarial tag names don't false-match substrings (Risk R24). - All SQL is parameterized; injection attempts via filter values land as literal strings.
- 62 new tests across the SQLite layer; total 301; coverage 92.69%.
engram.storage.markdown- markdown SoT layer:read_thought()/write_thought()/split_frontmatter()/FrontmatterDrift/DriftReason. Two-parse design (PyYAML safe_load for Pydantic-validated read, ruamel.yaml round-trip for write-side preservation of unknown extras) so forward-compat fields survive write+read cycles (Risk R19).- Frontmatter Schema Drift Handling per
02-TECHNICAL_DESIGN.md: missing schema_version defaults to 1 (NFR5 exception); missing required field surfacesMISSING_REQUIRED_FIELDdrift and the file is not indexed; non-UTF-8 surfacesNOT_UTF8; YAML parse error surfacesYAML_PARSE_ERROR; unknown prefix value surfacesUNKNOWN_PREFIXbut the file IS indexed; unknown extra fields surfaceUNKNOWN_EXTRA_FIELD(info-level) and round-trip preserved. - A4 explicit test: body containing literal
---mid-document round-trips intact. - Force-quote
id,fingerprint,created_at,updated_aton write to avoid YAML scalar misinterpretation (e.g., all-hex-digit fingerprints could parse as int). - Atomic write via
engram.utils.atomic_write; mode 0600. - Hypothesis property test for write+read body round-trip (handles CRLF normalization + trailing-newline append per the writer contract).
engram.storage.facade-VaultStorageclass composing markdown SoT + SQLite:capture()implements the Flow A atomicity contract: markdown write must succeed before SQLite insert; embedding optional (omitted -> embedding_status'pending'); SQLite txn wraps row + embedding insert; git sync hook stubbed for Phase 2+._post_capture_sync()is the placeholder.parse_prefix_from_content()extracts[Word]from leading body content; falls back toNote. Multi-word prefixes ([Action Item],[Session Summary]) are returned intact.- BYOC default-portability:
DomainandArtifactdefault tosensitive; other prefixes default toportable. Explicit override at capture time. get_by_id(),list_thoughts(),search(),update_metadata(),update_body(),delete(),stats(),repair_pending_embeddings().- Q1 default applied: content >1 MB raises
VaultError; >100 KB warns.
- 55 new tests (23 markdown + 32 facade); total 356; coverage 90.51%.
engram.embedding- lazy-loaded FastEmbed provider:EmbeddingProviderruntime-checkable Protocol so the storage layer never has to import the concrete provider.FastEmbedProviderboots cold (~2s budget forinitialize) and lazy-loadsBAAI/bge-small-en-v1.5on firstembed()under athreading.Lockso the first capture or search after cold start absorbs the 2-3s model-load cost once per process. Dimension verified against the model card on first use.aembed()async wrapper executes the sync embed viaasyncio.to_threadso the MCP event loop stays unblocked under concurrent tool calls.
engram.storage.reindex- drift-aware reindex pipeline:- Four modes:
INCREMENTAL(re-embed drifted bodies + insert new files),FULL(re-embed everything from markdown - used after embedding model swap),REPAIR(onlyembedding_status='pending'rows),REMOVE_ORPHANS(snapshot-guarded SQLite-only cleanup of rows whose markdown disappeared). - Snapshot-timestamp guard (R11): orphan removal compares against the walk start time so concurrent captures during reindex are not deleted.
- Four modes:
engram.diagnostics.doctor- 9-check health pass:- Vault directories exist, sqlite-vec is loadable, embedding settings agree
with the
engram_settingsrow, embedding model is downloaded, index + markdown counts agree, no orphan rows, no orphan tempfiles, no pending embeddings (--repairreconciles). CheckStatusis one of OK / WARN / FAIL; the report'sexit_codeproperty maps to 0 / 1 / 2 so CI consumers can branch on it directly.
- Vault directories exist, sqlite-vec is loadable, embedding settings agree
with the
engram.cli- Typer-driven console entrypoints registered asengram:engram init <vault>scaffolds<vault>/thoughts/,<vault>/.indexes/,<vault>/.engram/config.yamlwith mode 0700 directories + 0600 config.engram doctorruns the 9-check pass and exits 0/1/2 per status.engram reindexexposes the four modes;--repairand--fullaccept explicit model overrides.engram serveacquires theVaultLock, builds the FastMCP server, and blocks on stdio. Cloud-sync paths (Dropbox, iCloud, Google Drive) emit a structured WARN per Q10 default.
engram.mcp- FastMCP-wired tool surface:- Five
@mcp.toolhandlers (capture_thought, search_thoughts, list_thoughts, thought_stats, fetch) - one-to-one with the Open Brain MCP surface so existing prompts and skills work unchanged. - capture: embedding-failure is non-fatal (sets
embedding_status='pending', structured WARN logged); content >1 MB rejected withVaultError. - fetch: returns null (NOT error) for unknown id so the MCP client can distinguish "no row" from "tool failure".
- Five
engram.migration.open_brain- one-shot Open Brain -> engram migration:- Six steps per
04-MIGRATION.md: connect/probe (verifies the OB endpoint honorssort=created_at_asc), enumerate (paginatedlist_thoughts), transform (UUID-v7, prefix parsing, fingerprint, idempotency triple(fingerprint, source, created_at)), write (markdown + SQLite + embedding under Flow A), validate (random-sample byte-level fetch round-trip), report (migration-report.json+ audit-trail row). - F1 empty corpus, F3 idempotent rerun via triple match, F5 future
created_atpreserved aslegacy_created_at, F6 empty body skipped- error logged, F8 dry-run reads but writes nothing, F10
--limitcaps at N, F12--prefer-legacy-id-matchin-place update for actively-edited sources.
- error logged, F8 dry-run reads but writes nothing, F10
- CLI:
engram migrate-from-open-brainwith--url/--key(env-var-preferred per ps-aux safety),--config,--vault,--dry-run,--limit,--prefer-legacy-id-match,--confirm-supabase-snapshot-taken(refuses non-dry-run without it),--report-path.
- Six steps per
- Test infrastructure:
tests/fixtures/corpus.py- deterministic synthetic corpus generator covering all 15 canonical prefixes, all 3 portability values, and strictly-increasingcreated_at. Same generator scales to 10K for the benchmark below.tests/properties/test_invariants.py- hypothesis tests for capture/fetch round-trip, fingerprint stability under whitespace + line-ending normalization,searchtop-k upper bound, and incremental reindex idempotency.bench/search_10k.py- NFR1 measurement harness over 10K synthetic thoughts with a deterministic stub embedder. Local p95 ~37ms (target: <100ms). Exits non-zero when the threshold is exceeded so CI can wire it directly.
- ADRs at
docs/adr/: 001-storage-recipe (markdown + SQLite + sqlite-vec), 002-mcp-tool-surface (five-tool surface, frozen for v1.x), 003-sync-model (git CLI, no library, Phase 2+), 004-embedding-model (BAAI/bge-small-en-v1.5 via FastEmbed). - Final test count for the Phase 1 cut: 433 tests across 18 modules (config, diagnostics, embedding, mcp, migration, models, storage, utils, fixtures, properties).
[Unreleased]: https://github.com/kpachhai/engram/compare/...HEAD