Skip to content

Publish a mem

A mem is the packaged unit of sharing: a whole typed model — entities, relationships, and the schema they conform to — sealed into one .mem archive. Publishing puts that archive on a Memstead registry under your GitHub handle, where anyone who uses the same registry can install it with one command. This guide walks the first publish end to end.

You need a workspace with a mem worth sharing (see Getting started), a GitHub account, and the base URL of a registry. No registry signup: authentication is GitHub Device Flow, triggered automatically on first use, with the OAuth settings read from the registry itself.

The CLI ships with no default registry. Every registry command (publish, install <scope>/<name>, login, logout, unpublish) takes the registry from --registry <URL> or, when the flag is absent, from the MEMSTEAD_REGISTRY environment variable; with neither it refuses with REGISTRY_NOT_CONFIGURED. The examples below assume the variable is set:

Terminal window
export MEMSTEAD_REGISTRY=https://registry.example.com

Installing a local .mem file needs no registry at all.

The registry renders each mem with a one-line description embedded in the archive. Set it before publishing (an empty string clears it):

Terminal window
memstead mem set-description recipes "Worked example: a tiny cookbook mem"

Versions come from the mem config too — seeded as 0.1.0 at init. Both setters (mem set-description, mem set-version) echo the old and new values back as they persist them, and the dry run in the next step shows the version that would ship.

--dry-run assembles and resolves everything — mem, version, scope, archive size — but POSTs nothing and mutates nothing:

Terminal window
memstead publish --mem recipes --dry-run
# Dry run — would publish
- Mem: `recipes`
- Version: `0.1.0`
- Scope: derived from your GitHub login
- Archive: 3153 bytes
- Registry: https://registry.example.com
Nothing was published and nothing was changed.

--mem <name> names which mem to export-and-publish; it works in every workspace shape. (A single-mem folder workspace can also run bare memstead publish, which wraps up the surrounding folder.) The scope is not chosen by you — the registry derives it from your GitHub login at upload time, so the dry run can only tell you it will.

Same command, without the flag:

Terminal window
memstead publish --mem recipes

On first use this starts GitHub Device Flow: the CLI reads the registry’s OAuth client id from <registry>/api/auth/config, prints a code, opens the verification page, and stores the resulting token in ~/.config/memstead/credentials — subsequent publishes are silent. On success:

# Published github:alice/recipes v0.1.0
- URL: https://registry.example.com/v/github:alice/recipes

You can also log in ahead of time with memstead login, or run non-interactively by setting MEMSTEAD_TOKEN (a GitHub token) — CI has no TTY for the device flow.

That’s what you put in your README. Anyone pulls your mem into their own workspace with:

Terminal window
memstead install github:alice/recipes

The installed mem mounts read-only: its entities and schema structure are readable and linkable, but the engine treats non-first-party content as untrusted input — its schema’s instruction prose is withheld and every read surface tags the content’s origin.

The workspace’s tracked mount roster records the install by what was installed, the archive’s content-addressed name (recipes-<content key>.mem), not by where this machine keeps its archive cache. A clone of the workspace on another machine therefore names the same mem; until it is installed there too, that machine holds it out with ARCHIVE_NOT_INSTALLED, and the message names the install command. Installing the same archive there brings it into service without changing the roster, and an archive of the same name with different content is never mounted in its place.

Bump the version and publish in one step (--version persists the bump to the mem config, like npm version + npm publish):

Terminal window
memstead publish --mem recipes --version 0.2.0

The registry serves the highest published version as current. Publishing an older version succeeds — it’s retained and resolvable — but the output notes that current stays where it was.

To take a mem down: memstead unpublish github:<handle>/recipes (permitted to the original uploader). The same <scope>/<name> becomes immediately re-publishable.

The refusals a first publisher actually hits

Section titled “The refusals a first publisher actually hits”

Every refusal carries a typed code (add --json and branch on .code):

  • REGISTRY_NOT_CONFIGURED: neither --registry nor MEMSTEAD_REGISTRY is set. A dry run still previews and reports the registry as not configured.
  • REGISTRY_AUTH_CONFIG_UNAVAILABLE: login needed the device flow, but the registry did not answer GET /api/auth/config with its OAuth settings (unreachable, not a Memstead registry, or no GitHub login configured).
  • NOT_AUTHENTICATED — no token and no TTY for the device flow: not logged in and stdin is not a TTY — set MEMSTEAD_TOKEN or run `memstead login` first. Also the shape a 401 from the registry maps to (expired/revoked token): re-run memstead login.
  • WORKSPACE_NOT_INITIALISED — you ran publish outside any workspace: no workspace found from <cwd> or any ancestor (missing .memstead/workspace.toml). cd into the workspace, pass --workspace <path>, or supply a pre-built archive path.
  • INVALID_INPUT — --version without --mem (the bump needs to know which mem to re-version), or --version / --redact-anchors combined with a pre-built archive path (its content is already baked in — assemble with --mem or the bare shape instead).
  • INVALID_VERSION — --version that isn’t a semver.
  • REGISTRY_VALIDATION_FAILED — the registry rejected the archive’s content (a 400); the message carries the validation variant, the offending path inside the archive, and the detail.
  • ARCHIVE_TOO_LARGE — the archive exceeds the 2 MiB compressed publisher cap (the unit the validator enforces). Slim the mem or split it.
  • RATE_LIMITED — too many publishes in a window; the message says how many seconds to wait.
  • FORBIDDEN — you tried to publish into a scope that isn’t yours (--scope overrides are reserved for registry admins; normal publishes never need it).

A .mem archive carries a provenance record for every entity in .memstead/provenance.json: the latest authoring rationale where a mutation recorded a note, and an explicit no-rationale record otherwise. Those notes are written inside your workspace and can name what a public archive must not: internal plan paths, a legacy domain, an absolute user path, a credential. memstead export --format mem redacts every such span to [redacted:<class>] before the archive is written and never strips the record: the row keeps its timestamp, kind and actor, and the rest of the sentence stays readable. The classes are the same seven the engine’s leak scan enforces, held equal by a test, and the export report counts what it redacted per class (redactions in the JSON envelope). Entity bodies are not rewritten: a private string in a body is yours to fix, and the leak scan keeps flagging it.

A .mem archive exported from a workspace whose check ledger holds records for the mem carries them in .memstead/checks.json: per entity, the latest record per kind (verification, conformance, and each foreign x-<name> kind) with the verdict, the method note, the checker’s declared identity handle exactly as recorded, the actor and role, the entity’s content hash at check time, the timestamp and, where a rename carried the record, its origin. A mem with no record, and a workspace with no ledger, export no member and the same bytes as before the member existed. The method note passes the same per-class redaction the provenance member applies; the identity handle is copied verbatim, never mapped to a person. The archive format number is unchanged: an archive sealed before the member existed reads as carrying no check records, and an engine from before the member installs an archive that carries one and ignores it.

An archive mount derives its check state from that member and from nothing else: memstead entity <id> --provenance, the MCP entity read with include_provenance, and memstead health --include checks compare each sealed record’s hash against the entity as sealed, so a record sealed over a later edit reads check_stale, never fresh. A workspace ledger beside the mount is not consulted for it, and an archive without the member reads never_checked with the reason stated. The read-only refusal stands: nothing records a check on an archive mount.

The author≠checker independence reading travels too. An archive records no history at the engine seam, so the mount cannot derive who executed the work; the export derives the reading per sealed record from the source mem’s provenance (confirmed_independent, self_checked or unconfirmable) and seals it beside the record. The mount serves that reading in the checks axis and in the transition gates, so a plan whose criteria were confirmed independently stays satisfied on the archive. A member sealed before the reading existed reads unconfirmable.

Publishing over a private source: --redact-anchors

Section titled “Publishing over a private source: --redact-anchors”

A mem built from a source carries provenance anchors — durable records tying each entity to the artifacts it describes. They travel in the published archive by design: a consumer reads how strongly each entity claims fidelity to its source. But every anchor also names the source’s file paths or URLs. If your source is private, publish with redaction:

Terminal window
memstead publish --mem my-notes --redact-anchors

Every artifact reference in the packaged sidecar — the artifact field and each derived_from entry — becomes the fixed sentinel [redacted], while the trust metadata survives: provenance class, at_version, grain, hash, hash stability, and your source name. A consumer still sees the trust grade of every entity; they no longer see which files it came from. Reverse artifact lookup (memstead anchors --artifact <path>) on an installed redacted mem finds nothing — by design.

Two things to know:

  • Your workspace is untouched. Redaction happens on the staged copy at publish time; the local sidecar keeps its real references, and local anchors, verify, and drift behaviour are unchanged.
  • Redaction removes identity, not existence. The kept fields still disclose something: grain reveals the medium shape, at_version may carry a commit SHA or ETag, source is the name you chose for the source, and hash lets someone confirm guessed content against the prepared form. A redacted package is not free of source-related information — it just names no artifact.

Without the flag, published anchors ride untouched — that honest default is unchanged.

If you already have a .mem file — e.g. from memstead export --format mem -o my.mem — publish the bytes directly:

Terminal window
memstead publish my.mem

Content-shaping flags (--version, --redact-anchors) refuse on this shape — the bytes are already baked. Re-export, or use an assembling shape.

  • The registry you publish to documents its own side of these commands (routes, limits, terms).
  • The Glossary defines mem, archive, and mount precisely.