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.
0. Point the CLI at a registry
Section titled “0. Point the CLI at a registry”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:
export MEMSTEAD_REGISTRY=https://registry.example.comInstalling a local .mem file needs no registry at all.
1. Give the mem its card text
Section titled “1. Give the mem its card text”The registry renders each mem with a one-line description embedded in the archive. Set it before publishing (an empty string clears it):
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.
2. Dry-run first
Section titled “2. Dry-run first”--dry-run assembles and resolves everything — mem, version, scope, archive size — but POSTs nothing and mutates nothing:
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.
3. Publish
Section titled “3. Publish”Same command, without the flag:
memstead publish --mem recipesOn 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/recipesYou 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.
4. The install line
Section titled “4. The install line”That’s what you put in your README. Anyone pulls your mem into their own workspace with:
memstead install github:alice/recipesThe 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.
Ship an update
Section titled “Ship an update”Bump the version and publish in one step (--version persists the bump to the mem config, like npm version + npm publish):
memstead publish --mem recipes --version 0.2.0The 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--registrynorMEMSTEAD_REGISTRYis 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 answerGET /api/auth/configwith 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-runmemstead login.WORKSPACE_NOT_INITIALISED— you ran publish outside any workspace:no workspace found from <cwd> or any ancestor (missing .memstead/workspace.toml).cdinto the workspace, pass--workspace <path>, or supply a pre-built archive path.INVALID_INPUT—--versionwithout--mem(the bump needs to know which mem to re-version), or--version/--redact-anchorscombined with a pre-built archive path (its content is already baked in — assemble with--memor the bare shape instead).INVALID_VERSION—--versionthat 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 (--scopeoverrides are reserved for registry admins; normal publishes never need it).
Provenance is redacted at export
Section titled “Provenance is redacted at export”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.
Check records travel with the archive
Section titled “Check records travel with the archive”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:
memstead publish --mem my-notes --redact-anchorsEvery 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:
grainreveals the medium shape,at_versionmay carry a commit SHA or ETag,sourceis the name you chose for the source, andhashlets 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.
Publishing a pre-built archive
Section titled “Publishing a pre-built archive”If you already have a .mem file — e.g. from memstead export --format mem -o my.mem — publish the bytes directly:
memstead publish my.memContent-shaping flags (--version, --redact-anchors) refuse on this
shape — the bytes are already baked. Re-export, or use an assembling shape.
Where next
Section titled “Where next”- The registry you publish to documents its own side of these commands (routes, limits, terms).
- The Glossary defines mem, archive, and mount precisely.