Archive format

A Gem serializes to an archive with two parts: a manifest (gem.json) and a lock (gem.lock). This split is what lets Gems compose — merging reconciles manifests and re-resolves a single lock, rather than diffing opaque blobs. This page specifies the on-disk format produced by src/gem/archive.ts.

ARCHIVE_FORMAT_VERSION is currently 1.

File layout

writeGemArchive(gem, opts?) produces a flat map of POSIX paths → string content:

skills/<name>/SKILL.md       # skill body (markdown)
mcp/<name>.json              # { transport, config, source?, secretRefs? }
hooks/<name>.json            # { event, matcher?, config, source?, secretRefs? }
instructions/<name>.md       # instructions body (markdown)
checks/<name>.json           # one file per embedded GemCheck
gem.json                     # manifest
gem.lock                     # lock

Artifact names are run through safePathSegment() so they're safe as filenames; path collisions are detected and surfaced as skipped rather than silently overwritten.

The manifest — gem.json

The human-meaningful declaration of what the Gem contains:

interface GemManifest {
  formatVersion: number;
  name: string;
  version: string;
  createdFrom: string;
  artifacts: ManifestArtifactEntry[];   // { type, name, path, description?, source? }
  requiredSecrets: SecretRequirement[]; // { name, artifact, location } — names only
  checks: ManifestCheckEntry[];         // { name, path }
  dependencies?: string[];              // registry refs this Gem builds on
}

The manifest is an index: it lists what's in the archive and where, the declared secret surface (by name), and any registry dependencies. It never contains secret values.

The lock — gem.lock

The resolved, pinned detail that makes a build verifiable:

interface GemLock {
  formatVersion: number;
  files: Record<string, string>; // path -> "sha256:<hex>" for every file except the lock
  gemDigest: string;             // "sha256:<hex>"
  signature: string | null;      // reserved for future signing
}

Digest computation

computeLock(files):

  1. Hashes each file's content as sha256:<hex>. The manifest is hashed via a canonical stable JSON serialization so logically-equal manifests hash identically regardless of key order.
  2. Derives gemDigest deterministically from the sorted file paths and their hashes.

Because hashing is order-independent and tar packing uses sorted paths with a fixed mtime, the same Gem always produces the same archive bytes and the same digest — important for the registry's immutability check (a re-publish of an existing version with a different digest is rejected).

Reading and verifying

Serialization

The file tree is format-neutral; two serializers turn it into bytes:

Serializer Functions Use
Directory writeArchiveDir / readArchiveDir Local workspaces and testbeds. readArchiveDir skips dot-prefixed entries (e.g. .targets/) and normalizes paths to POSIX.
Tar.gz packTar / unpackTar Transport, download, and registry storage. Deterministic: sorted paths, fixed mtime 0, POSIX ustar.