feat: add core and space migration packages#71
Conversation
| , slug text not null unique check (slug ~ '^[a-z0-9]{12}$') | ||
| , name citext not null | ||
| , shard_id int not null references core.shard (id) | ||
| , language text not null default 'english' check (language ~ '^[a-z_]+$') |
There was a problem hiding this comment.
I would not put language support in now
There was a problem hiding this comment.
one way or the other you have to configure pg_textsearch indexes with a language. The database migrations for the spaces are already wired to configure pg_textsearch languages.
| return; | ||
| } | ||
|
|
||
| const sorted1 = [...incrementals].sort((a, b) => |
There was a problem hiding this comment.
maybe we error if they werent already sorted. They should be, right?
There was a problem hiding this comment.
they should be sorted. I'm indifferent on whether we error
| */ | ||
| alter table {{schema}}.memory add constraint temporal_bounds_convention check | ||
| ( | ||
| temporal is null |
There was a problem hiding this comment.
probably dont want empty one either (force null instead)
There was a problem hiding this comment.
i don't follow. the current logic is intended to allow 3 options
- null
- a point in time: start = end inclusive-inclusive
- a range: start < end inclusive-exclusive
I don't think an empty tstzrange is possible with this constraint unless i'm missing something.
| @@ -0,0 +1,163 @@ | |||
| import { info, reportError, span } from "@pydantic/logfire-node"; | |||
There was a problem hiding this comment.
we need to improve function reuse from a bunch of these migration files. Right now we have a lot of code duplication. Maybe a common migration utility lib?
There was a problem hiding this comment.
Maybe. I care far more about code locality than DRY. If there is actual, felt pain, I guess we could put identical pieces into a common utility lib, but I wouldn't try to make a common "database migrator" process. As soon as they start to differ, you have to decide whether to parameterize the common doohickey to support both ways of working (which makes it harder to reason about and change), or pull it back apart.
If it's just little utility functions, then I guess I don't have a problem with it.
| @@ -0,0 +1,505 @@ | |||
| import { createHash } from "node:crypto"; | |||
…ng, postgres.js pilot)
Add integration test suites for the new core/space migration system, run against a real ghost (TigerData) Postgres via TEST_DATABASE_URL. Tests isolate per-schema (core_test_<rand>, me_<slug>) so they run concurrently and parallel-safe across files; bun run test:db runs core + space.
Template the core migration SQL with {{schema}} so tests provision throwaway, isolated cores and never touch a real control plane; production still defaults to 'core' (exposed as CORE_SCHEMA). migrateCore now takes an optional schema.
Pilot the Bun.SQL -> postgres.js driver swap on the migrate path. Bun.SQL fails to return a pooled connection after a query/transaction error (oven-sh/bun#22395, present in 1.3.13 and 1.3.14), which hangs the suites and is a latent production hazard for the long-lived engine/accounts pools. Both postgres.js and pg fix it; postgres.js is a near-drop-in. Converted core/space migrate + bootstrap + scripts/migrate-db.ts + test-utils; verified on local and ghost.
Docs: CLAUDE.md gains a db-integration-test section and the Bun.SQL -> postgres.js migration recipe; TODO.md tracks test-utils/migration-runner consolidation and the core/space packaging question.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real-DB regression for the max(access) + group by tree_path at the end of agent_tree_access. An agent with a broad `foo` grant plus a redundant `foo.bar` grant, clamped against an owner that grants only `foo.bar`, makes both arms of the inner union emit `foo.bar` at different access levels; the max collapses them to the single effective row. Without it the function returns `foo.bar` twice (verified the assertion fails in that case). Also document the --timeout 30000 needed to run a single integration file directly against ghost (bun's 5s default overruns the migrating beforeAll and surfaces as a misleading hook-timeout). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hard-interrupted integration runs (SIGKILL, OOM, a timed-out beforeAll) can leave throwaway schemas behind. Add scripts/clean-test-schemas.ts and run it as a pre-step in `test:db`. Safety is by construction — the sweeper only matches names impossible in production: `core_test_*` (prod control plane is the bare `core`) and `metest_*`. To make the latter safe, test spaces now provision under a `metest_<slug>` prefix instead of the production `me_<slug>`, via a new optional `schema` override on migrateSpace (mirrors migrateCore; defaults to slugToSchema(slug), so production is unchanged). `metest_` also avoids the `me_` engine-schema prefix. Pointed at a real database the sweeper is a no-op. It is age-gated (drops only schemas older than 60 min) so a concurrent `test:db` sharing the database is safe; `test:db:clean:all` forces a full reset. Verified on ghost: drops stale core_test_/metest_, skips fresh ones, and never touches a production-shaped me_<slug> even with --all. Update the two space tests that re-invoke migrateSpace on an existing space to pass the schema override (as the core tests already do), so they target the test schema rather than provisioning a stray me_<slug>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-locate the control plane (core schema) and data plane (per-space me_<slug> schemas) in a single @memory.build/database package, kept as separate core/ and space/ modules. The team runs them in one database/deployment and pgdog sharding of spaces is off the table for now; the per-slug schema model and the set-local-pgdog.shard code stay in space/ so re-splitting later is cheap.
- packages/{core,space} -> packages/database/{core,space} (git mv, history preserved); single package.json/tsconfig; index.ts re-exports both modules. - Rewire scripts/migrate-db.ts + scripts/package.json + root test:db to @memory.build/database. - ISql<{}> -> ISql (clears the noBannedTypes lint warnings). - TODO.md records the decision; the test-utils/migration-runner consolidations become internal modules.
Verified: typecheck + lint clean; 752 unit tests pass; db integration on ghost (core 18, space 19) pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The core and space test-utils duplicated ~110 lines of generic, driver-level helpers (connect, resolveTestDatabaseUrl, expectReject, and schema introspection). Move them once into packages/database/migrate/test-utils.ts; core/ and space/ test-utils now `export *` from it and keep only their provisioning (TestCore/TestSpace, randomCoreSchema/randomSlug/testSchema). Test files are unchanged. Verified: typecheck + lint clean; 761 unit tests pass; ghost db integration (core 18, space 19) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
migrateCore, migrateSpace, and bootstrapSpaceDatabase duplicated nearly all of the migration machinery. Move it once into packages/database/migrate/kit.ts: advisory locking, session timeouts, extension/Postgres-version preconditions, schema checks (exists/ownership/valid-name), {{…}} templating, SQL-file execution with error-location logging, and the incremental-once/idempotent-always runSchemaMigrations runner. The kit is parameterized by a `label` (drives span/attribute/log names, so telemetry keys are unchanged) and `dir`; the three entry points are now thin orchestrators holding only their schema-specific bits.
bootstrap's advisory lock moves from a hardcoded single-key id to the shared two-key derived lock (internal; bootstrap is its only user).
Verified: typecheck + lint clean; ghost db integration (core 18, space 19) pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New packages/database/auth migration unit, a sibling to core/ and space/ using the shared kit.ts runner. Provisions an `auth` schema with better-auth-shaped tables: users, accounts, sessions, device_authorization, and verifications. - Login-only OAuth: the accounts token/password columns are kept for better-auth shape parity but left nullable and never written, so there is no token-encryption subsystem. - sessions store token_hash (sha256), not a raw token — a deliberate divergence from better-auth's plaintext lookup so a DB read yields no usable bearer tokens. - Requires only citext (not the engine ltree/vector/pg_textsearch extensions), so the schema can live in a pgvector-less database. Verified: typecheck, lint, and 20/20 integration tests against local PG18. Against remote testing_me the connection-storm tests time out the same way the existing core suite does (a shared, pre-existing latency issue, not this unit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With three integration suites now (core, space, auth), running all of them in parallel (-P 4) saturates the small ghost test instance — concurrent pools plus simultaneous HNSW/BM25 index builds peg it, surfacing as CONNECT_TIMEOUT and 30s statement timeouts. -P 2 keeps two suites in flight, within capacity. Verified: full test:db green on ghost (core 18, space 19, auth 20). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spaces are co-located in one database (pgdog distribution is off the table for now), so migrateSpace and bootstrapSpaceDatabase no longer accept a shardId or emit `set local pgdog.shard`. Pure removal — no caller passed shardId, and the space suite stays green (19/19). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mport` Add an e2e scenario that writes a real Claude transcript, captures it via the real `me claude hook --event stop` (binary → server → Postgres), then runs `me claude import` over the same file and asserts the row count is unchanged, and re-running the hook is also a no-op. Exercises the actual server-side dedup (search_memory watermark + create_memory unique constraint), complementing the unit-level cross-idempotency tests. Uses two user turns so the importer doesn't skip it as trivial; a stdin-piping `meStdin` helper feeds the hook event JSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`memory.create` and `memory.batchCreate` now require a non-empty `tree` at the protocol layer, so callers must choose `share` vs `~` deliberately instead of silently defaulting to the shared root. The MCP `me_memory_create` tool and the `me memory create` CLI command enforce and document this (most memories under `share`, private ones under `~`). The file importers remain the one place that defaults a tree-less record: `me memory import` and the `me_memory_import` MCP tool fall back to `share`. SHARE_NAMESPACE moves to @memory.build/protocol as the single source of truth (the wire default); @memory.build/database re-exports it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring docs/ in line with the multiplayer-branch redesign (engine/org/role -> principals/spaces). Rewrite the access-control and TypeScript-client guides, replace the "Engines" concept with "Spaces", and switch the MCP and getting-started guides to the session-token + X-Me-Space model. Retire the org/role/owner/engine/user/grant/invitation CLI references and add me-space, me-group, me-access, and me-agent. Update the docs-site nav to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Dockerfile still copied packages/accounts (renamed to auth) and was missing the database and e2e workspaces, so bun install --frozen-lockfile failed in the dev build. Also drop the runtime copy of client, which is not a transitive dependency of the server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dev deploy injects the legacy two-DB env (ACCOUNTS_DATABASE_URL / ENGINE_DATABASE_URL); the single-DB code requires DATABASE_URL and crashes on boot. Fall back to ENGINE_DATABASE_URL so multiplayer can deploy to dev before the tiger-agents-deploy helm values are migrated to the single-DB env contract. Remove once the deploy config sets DATABASE_URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`me claude install` now installs the Memory Engine plugin (hooks + slash commands + MCP) by driving Claude Code's native plugin CLI for you — `marketplace add` (idempotent) + `plugin install`, passing the resolved server/space/api_key through --config. `--mcp-only` falls back to the previous behavior (register just the `me` MCP server). `--dev` installs from the local checkout's .claude-plugin/marketplace.json (clean swap, since the local and published marketplaces share the "memory-engine" name) so captures exercise your working tree. Docs updated across README, getting-started, mcp-integration, agents.txt, and the me-claude CLI reference to reflect the full-plugin default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
memory.search always cast the tree filter to ::ltree, so a documented lquery (`foo.*`) or ltxtquery (`a & b`) threw `invalid ltree` — surfaced to callers as a generic Internal error. Only a bare path worked. Add classifyTreeFilter() in space/path.ts: it normalizes (~/slashes) then classifies the result — bare path -> ltree containment, contains `&` -> ltxtquery, else -> lquery — and the search handler binds the matching SQL parameter (@> / ~ / @). The store already accepted all three; only the handler was collapsing everything to ltree. Tests: classifier unit cases in path.test.ts, plus end-to-end lquery wildcard and ltxtquery cases in memory.integration.test.ts that exercise the real SQL and would have caught the original crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document installing the local `me` binary and the `me claude install --dev` flow against the dev server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an e2e test asserting that `me claude import` backfills Claude Code work that predates the capture hook: a transcript is written to disk with no hook firing, the hook then captures a separate live session, and a subsequent import pulls in the pre-install transcript without duplicating the live capture. Also adds a countBySession() helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`me create` now requires an explicit tree, so the e2e create/search tests that relied on the old default-to-share behavior were failing. Pass an explicit `--tree share`, then add `test:e2e` to the `check` script so the suite is actually exercised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a first-class `me claude init` subcommand that, for now, backfills existing Claude Code sessions (a default-option import). It's a deliberate seam for further setup steps to be added later, not an alias of `import`. Exports `runAgentImport` from the import command so init reuses the exact auth/option/render path. Covered by an e2e test asserting init backfills a transcript from the default ~/.claude/projects source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a second step to `me claude init`: after backfilling sessions, it writes (or updates, idempotently via marker comments) a managed block in the project's CLAUDE.md pointing the agent at where this project's memories live in Memory Engine — the `share/projects/<slug>` tree, derived with the same SlugRegistry the importer uses, plus how to search them. Targets the git repo root's CLAUDE.md when in a repo, else the cwd's. The e2e test now also asserts the pointer is written and that re-running init leaves exactly one managed block; the `me` helper gained an optional cwd so init runs in an isolated project dir. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AUDE.md The init-written CLAUDE.md block now uses canonical dot-separated ltree paths (share.projects.<slug>) rather than slash form, and instructs the agent to always consult project memories first when exploring the codebase or starting a task. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructures `me claude init` around a step registry (INIT_STEPS). In an interactive terminal it presents a multiselect of all steps, each pre-checked, so the user can deselect any; non-interactively it runs every step except those turned off by a generated --skip-<step> flag (--skip-transcript-import, --skip-claude-md). Adding a step is now a single INIT_STEPS entry — it gets both a skip flag and a multiselect row for free. e2e: the `me` helper-driven (piped, non-interactive) tests assert each --skip flag suppresses exactly its step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spell out in the multiselect prompt that all steps are selected by default and how to toggle (arrows move, space toggles, enter confirms), since the toggle interaction isn't self-evident. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the parenthetical guidance on the multiselect prompt line in dim ANSI so the main "Setup steps to run" text stands out, and drop the per-option hint rows (and the now-unused InitStep.hint field) for plain, uncluttered step labels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oring create_memory now does `on conflict (id) do nothing` and returns null for a duplicate explicit id; memory.batchCreate returns inserted ids only and memory.create maps the null to a clean CONFLICT error (previously an unmapped unique_violation surfaced as "Internal error" and failed the whole batch). This makes the long-documented import contract real: chunk.ts, the MCP import tool's idempotentHint, and docs/cli/me-memory.md all promised ON CONFLICT skip semantics the SQL never had. The session importers never tripped it (they pre-diff existing state via search), but any path that re-submits explicit ids — `me memory import` re-imports, or a session whose >1000 already-imported messages overflow the capped dedup lookup — failed its whole chunk with "Internal error". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructure imports under one umbrella group — `me import memories|claude|codex|opencode|git` — so each new source is one subcommand instead of a new top-level command group. The old spellings stay registered as aliases (`me memory import`, `me claude|codex|opencode import`); the bare `me import <file>` alias is gone (the group owns the top-level name, and the group help points old muscle memory at `me import memories`). New `me import git [repo]`: one memory per commit — message plus a capped changed-file list — under `<tree-root>.<slug>.git_history`, with the commit date as temporal and a deterministic UUIDv7 keyed by (tree, sha) so re-imports are server-side no-op skips. Re-runs are incremental: one search finds the newest imported sha and, when it is an ancestor of the target rev, only `<sha>..<rev>` is walked (force-pushes fall back to the full walk, which the deterministic ids make safe). The walk is one streamed `git log --numstat` with NUL-delimited fields, parsed incrementally in constant memory. Body-less merge boilerplate is skipped; merges with a body (PR merges) are kept. `me claude init` gains an "Import git commit history" step (--skip-git-import; soft-skips outside a git repo), and the managed CLAUDE.md block now names the git_history node next to agent_sessions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spaces were migrated only once, at provision time — so a deploy that changed the idempotent space SQL (the function bodies in space/migrate/idempotent/*.sql) never reached existing spaces: boot migrated core + auth only, and migrateSpace (built as the standalone re-migrate path) had no callers in the server. The on-conflict fix in create_memory, for example, was undeployable to any space that already existed. startServer now enumerates core.space after the core/auth migrations and runs migrateSpace for each (mirroring provisionSpace's defaults). Re-running is cheap — incrementals are version-tracked no-ops, idempotent files are re-applied — and concurrent replica boots are serialized by the per-schema advisory lock. Every space is attempted so the logs name each broken one, then any failure aborts boot. The boot integration test now provisions a space whose create_memory has been tampered with before startServer runs and asserts the sweep restores the real definition. Also rewrites DEVELOPMENT.md's stale "Adding a migration" section (retired engine/accounts paths) to describe the current auth/core/space layout and the boot-time sweep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s capped lookup
create_memory gains `_replace_if_meta_differs text` and now does
insert / update-if-stale / skip atomically: on a duplicate explicit id it
replaces the row (tree/meta/temporal/content) when the stored meta value
for the caller-named key differs from the new record's, else skips.
Returns (id, inserted) — xmax = 0 distinguishes a fresh insert from a
replace; zero rows = skipped. The replace arm additionally requires write
access on the existing row's tree (silently skipped without it, so one
inaccessible row can't fail a batch). The old 6-arg signature is dropped
first — a new defaulted arg would create an ambiguous overload, and the
return type changed; the boot-time space sweep delivers the new function
to existing spaces. Embedding columns are untouched: the content-gated
update triggers mean a meta-only replace neither invalidates nor
re-enqueues the embedding.
memory.batchCreate threads the key (`replaceIfMetaDiffers`) and returns
`{ids, updatedIds}`; the session importers pass "importer_version", so a
version bump re-renders previously-imported messages server-side, batched.
That deletes the client-side reconcile in the importers: writeSession's
existing-state pre-fetch (`fetchExistingMessageVersions`) was one search
capped at 1000 rows — sessions past the cap silently re-submitted their
older messages every import with vanishing counts, and the guard meant to
catch it was dead code (search `total` is the page length). Now every
planned message goes through the upsert and inserted/updated/skipped come
from the batch response, exact at any session size; the hook keeps its
high-water search purely as a bandwidth optimization. Also raises the
CLI's memory-client timeout to 120s: 1000-row chunks are processed
row-by-row server-side and can legitimately exceed 30s.
Verified end-to-end against a real 27-session backfill including a
>1000-message session: 3269 inserted + 11984 cross-file duplicates
skipped, zero failures; re-import all-skip with honest counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
memory.batchCreate looped create_memory per row inside a transaction: a 1000-memory chunk was 1000 server↔database round trips, which is what made bulk imports slow (and was the reason the CLI client timeout had to be raised). New batch_create_memory takes the chunk as parallel arrays (uuid[]/ltree[]/text[]/tstzrange[] + one jsonb array for metas — a jsonb[] parameter double-encodes elements into string scalars, hence the single jsonb array aligned by ordinality via sql.json) and runs one unnest-driven INSERT ... ON CONFLICT DO UPDATE with identical per-row semantics. create_memory becomes a one-row wrapper over batch_create_memory, so the conflict rule (insert / replace-if-meta-differs / skip) lives in exactly one place; it is defined after the batch function since SQL-language bodies are validated at creation. Two deliberate semantic notes, both documented: the target-tree access check is all-or-nothing up front, and an explicit id repeated WITHIN a batch collapses to its first occurrence (a single INSERT cannot touch the same row twice; the importers already dedupe client-side). The handler drops its per-row loop and withTransaction (one statement is atomic by itself). Whale smoke (27 sessions, ~15.4K messages, local server against the remote ghost db): 345s → 55s per pass, zero failures, identical insert/skip classification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ject Init is per-project setup — the CLAUDE.md pointer and the git history import are already scoped to the repo — but its transcript step swept ALL of ~/.claude/projects, importing every project on the machine. The step now passes the repo root (or cwd outside a repo) as the project filter, so only sessions recorded in this project are backfilled; `me import claude` remains the machine-wide sweep. The temp-cwd filter is disabled for this scoped import: it exists to keep throwaway sessions out of bulk sweeps, but with the scope pinned to the directory the user is standing in it would only veto projects that happen to live under a temp dir. The e2e init tests now record their sessions in the init project's own directory (resolved via realpath — macOS tmpdir is a symlink and the filter compares against the resolved process.cwd()), and 8b additionally asserts a foreign project's session is NOT swept up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first thing init printed was the transcript importer's progress spinner — raw scan counters and a .jsonl filename with no context. The init runner now prints each selected step's label (clack.log.step) before running it, so every step's output reads under a header: ◇ Import this project's Claude Code sessions ⠹ [8964 scanned, 7 processed] · … Text output only — --json/--yaml stay clean for parsing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A --project filter only applied per-session AFTER parsing: discovery still walked and parsed every transcript under ~/.claude/projects, so a scoped `me claude init` showed (and paid for) a machine-wide scan — 105k files on a busy machine for a 30-file project. Claude Code names each per-project directory after the session cwd with every non-alphanumeric character replaced by `-`, so discovery now skips directories that can't match the filter (exact encoded name or `<encoded>-` prefix for descendant cwds). The encoding is lossy (`-` and `/` collide), so this is a prune only — kept files still pass the exact per-session cwd filter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After a successful `me login` with an active space, print a "Next step" note telling the user to run `me claude init` at the root of a software development project. Skipped when no space is active — the existing create/select-a-space hints are the right next step there — and absent from --json/--yaml output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Init wrote a CLAUDE.md pointing agents at the me_memory_search MCP tool and backfilled history — but without `me claude install` no MCP server or capture hooks exist, so init alone produced a half-working setup. A new first step runs the same install as `me claude install` (full plugin, user scope, login-session auth). Steps gain an optional `available` gate: a step resolving false is omitted entirely — no multiselect row, not in the non-interactive baseline. The install step hides itself when the `claude` binary is absent or `claude plugin list --json` already shows memory-engine@memory-engine (unparseable output counts as not installed — a wrong guess costs an idempotent re-install offer, never a missed one). The probe is skipped for steps opted out via their --skip flag in non-interactive runs, so `init --skip-plugin-install` never spawns `claude`. Also fixes the init e2e fixtures to mirror Claude Code's real on-disk layout: transcripts now live in encoded-cwd directory names (encodeProjectDir), which the project-scoped import has pruned by since 4dae311 — the literal "init-proj"/"skip-proj" fixture dirs were never scanned, a break that commit missed by not re-running e2e. The e2e init invocations pass --skip-plugin-install so a dev machine's claude never attempts a real marketplace install against the test HOME. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clone → ./bun install + install:local → login against the dev server → `me claude install --dev`. Login precedes the plugin install (it needs the session and stored server URL), --dev runs from inside the repo, and the note explains that a dev-installed plugin keeps `me claude init` from offering the published one over it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI runs every integration file in ONE bun process (`find … | xargs bun test`), so start.integration.test.ts's module-scope `process.env.SPACE_SCHEMA_PREFIX = "metest_"` leaked into every suite that ran after it: their provisionUser created metest_<slug> schemas while the tests query the hardcoded me_<slug> — 20 handler tests failing with `schema "me_…" does not exist`. Local runs were green because `./bun run check` passes --parallel, which isolates files per process. The prefix is now set in beforeAll and restored in afterAll (the slug.test.ts save/restore pattern); the e2e suite keeps its module-scope assignment since it runs in its own invocation. Verified with the exact CI command shape: all 17 integration files in one process, 225 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's bare `find … | xargs bun test` ran every test file in one process while local runs use --parallel (per-file isolation) — the divergence that let a module-scope env leak pass locally and break CI (cd05343). CI now calls package.json scripts so the command shape has one source of truth: the integration job reuses the existing test:db (whose schema cleaner is a no-op against the fresh CI container), and a new test:unit mirrors it for non-integration files. Both carry --parallel, matching the local process model. Deliberately NOT `./bun run check`: check is the dev loop — its `lint --write` would auto-fix violations in the runner instead of failing, and it merges everything into one job, putting the Docker Postgres build on the critical path of every lint error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No description provided.