@unknown-creatives/substrate
v1.3.0
Published
The distribution init CLI — `substrate init` wires Substrate's AI tooling (skills, the Bloom agent, instruction files) into a consuming project across the 14-platform wiring matrix (Claude Code, Cursor, Windsurf, Copilot, Gemini, Codex, …), and records se
Readme
@unknown-creatives/substrate
The distribution init CLI — substrate init wires Substrate's AI tooling
(skills, the Bloom agent, instruction files) into a consuming project across
the 14-platform wiring matrix (Claude Code, Cursor, Windsurf, Copilot, Gemini,
Codex, …), and records selections in .substrate/state.yaml for --refresh.
It also owns catalog entry scaffolding through substrate add, deterministic
substrate upgrade engine swaps, and exact-byte local engine artifact assembly
and verification through substrate artifact assemble. setup --apply and
upgrade cross one delivery seam: both consume the signed engine bundle and
share a single verification implementation
(distribution architecture v2).
These commands sit inside a delivery flow: for the end-to-end supervised engagement path — handoff set, onboarding, client-side verification, and upgrades — see the engagement delivery runbook.
Running the CLI
This package is published to npm. The package is
@unknown-creatives/substrate; a bare npx substrate still installs an
unrelated third-party package — always use the scoped name:
npx @unknown-creatives/substrate init --platform claude-codeThe bare substrate <cmd> spellings below assume that invocation. The
published version always equals the engine release version (single-version
coupling), so npx @unknown-creatives/substrate@<v> means "the CLI released
alongside engine <v>".
CLI dependencies vs engine runtime dependencies: the published package is
self-contained — it declares and installs its own dependencies (js-yaml,
and esbuild for artifact assemble). The engine's runtime dependencies
(tsx, yaml, semver, and the typescript-5 alias for migration/audit
analysis, UCS-1105) are not CLI dependencies and are intentionally not
in this package: generation runs from a Substrate checkout or a vendored
engine, never from the CLI install. (Historical note: before publishing, the
bin had to run from a checkout that had had npm ci, else it failed with
ERR_MODULE_NOT_FOUND — the published package has no such requirement.)
Working on Substrate itself, run the bin from your checkout after npm ci:
node <substrate-checkout>/packages/cli/bin/substrate-init.js init --platform claude-codeA substrate install bootstrap that would perform the engine-vendoring copy
itself remains deferred to the npm-engine packaging option (P4·h)
(see docs/getting-started.md §0).
In the vendored client layout, substrate init exits non-zero when its client
bootstrap hits blockers — a kernel/platforms/types content root it could
not create because the vendored engine source is missing (missing-source) or
the symlink and copy fallback both failed (copy-failed). It then prints
Init completed with N blocker(s) instead of
Done! Substrate is ready in this project., so scripted or CI onboarding can
gate on the exit code. The contract binds only when the engine the bootstrap
inspects is the engine init detected: the legacy layouts (npm install,
clone-subdir) may print the same content-root warnings but keep their
warning-only exit 0. Unresolvable engine runtime dependencies (tsx,
js-yaml, yaml, semver, typescript-5) stay a non-blocking warning:
running generation from the Substrate checkout is a supported layout that
never resolves them in the client (getting-started §0). The printed install
command spells typescript-5 in its npm alias form
("typescript-5@npm:typescript@^5") — it pins the TS 5.x compiler API the
migration and current-state analyzers need regardless of the client's own
TypeScript major.
Setup planning and transactional apply
substrate setup --plan is the non-mutating front door for self-serve setup. It
inspects a supported Vite + React consumer through client-visible signals
(package.json, one vite.config.*, index.html, the React application entry,
the application tsconfig, and one lockfile family) and prints a canonical JSON
plan to stdout:
substrate setup --plan \
--engine-artifact ../substrate-engine-0.39.0.bundle \
--brand acme \
--component surface \
--component button \
--target css \
--target swift \
--target compose \
> /tmp/substrate-setup-plan.json
substrate setup --apply /tmp/substrate-setup-plan.json \
--signing-public-key \
../substrate-release-signing-public-key.pem--dry-run is an alias for --plan. Both modes only read the consumer
repository. Keep a serialized plan outside the consumer repository: the plan
binds the repository's real path and exact file snapshot, so adding the plan
itself inside that tree correctly makes the original snapshot stale.
Apply accepts only that plan and a trusted Ed25519 public key. It rechecks the
repository snapshot, re-derives the complete plan, verifies the retained bundle
bytes against their attestation, checksum, signature, and public key, and stages
the engine plus ownership records before committing any consumer-visible
resource. A stale, edited, or foreign plan fails without mutation. Pre-commit
failure removes staging and leaves the consumer byte-equivalent to its starting
state; an exact rerun reports retained without rewriting files.
Apply is also the reconciliation boundary for an existing declaration. It
copies the current generated root into staging, runs the complete declared
project generation there, hash-validates every generated path declared by the
new manifest.gen.json, and compares that manifest with the previous generated
ownership record. Unmanifested passthrough files are checked separately and
must remain byte-identical. A removed brand, component, or target deletes every
path owned by the previous manifest, including a locally edited copy;
unmanifested files are copied through unchanged, and a newly generated path
that would collide with an unowned file fails the transaction. Generation or
pre-commit failure cannot publish a partial prune or advance ownership.
The apply JSON reports generated artifacts separately from committed
transaction resources. artifacts.targets.css, .swift, and .compose each
list created, updated, removed, and unchanged client-relative paths; the
ownership-manifest report independently identifies whether
manifest.gen.json was created, updated, or unchanged. Repeating the same
declaration or contraction therefore produces an all-unchanged artifact report
and no committed resource changes.
The local engine input is the
substrate-engine-<version>.bundle file emitted by substrate artifact
assemble, together with its .attestation.json, .bundle.sha256, and
.bundle.sig sidecars. Planning reads the bundle's declared member metadata to
enumerate future destinations, but labels those declarations unverified:
provider verification of the retained bundle bytes is mandatory before apply
can extract anything. It never accepts an unpacked directory as a second
delivery format.
The JSON identifies the vite-react host adapter, operational root remaps and
integration points, inferred or existing substrate/project.yaml, detected AI
hosts, and the future acquisition, ownership, projection, host-integration,
generation, and verification phases. Every planned resource includes its future
operation and reason. Generated target projections carry only target-neutral
brand/component/target selections; engine adapters own their concrete output
topology and the generation phase owns manifest.gen.json.
The client-owned declaration has four keys:
version: 1
brands:
- acme
components:
- button
- surface
targets:
- css
- swift
- composeTargets are output selections, not AI hosts. AI-host detection remains a
separate operational concern destined for .substrate/state.yaml; delivery
provenance remains in .substrate/manifest.yaml; generated topology and
freshness remain in substrate/generated/manifest.gen.json.
The installed engine resolves this declaration into one platform-neutral declared project graph before target descriptor construction. CSS, Swift, and Compose consume target selections from the same graph, so brand identity, component dependency closure, system inputs, and provenance resolve once while adapter syntax remains separate.
When substrate/project.yaml already exists, plan mode reads and validates it
instead of accepting selection flags as a second authority. Unsupported or
ambiguous host shapes fail with a resolution before any edit. The supported
Vite seam directly imports defineConfig from vite, exports one object
literal through that binding, and declares exactly one top-level plugins
array. Transactional apply verifies and stages the sealed engine, durable
project state, generated CSS projection, and deterministic Vite/TypeScript/
React integration before committing them. It then runs the consumer-owned
scripts.build, whose supported contract performs both the TypeScript check
and Vite production build; setup reports success only when that command
passes. Repeated setup retains the managed integration byte-for-byte.
Unsupported, ambiguous, or symlinked host layouts fail before any client file
is edited.
The plan never schedules the existing client-owned project declaration for
rewrite, and apply preserves its exact bytes. An engine root whose exact file
set and hashes match the unverified bundle declaration is retained only when
its delivery manifest also owns the same engine root, version, and member hash
set; any unowned or divergent engine root is rejected with a direction to use
substrate upgrade. Verification is mandatory before apply relies on either
case.
Onboarding P0 release gate
Run the complete release-level onboarding acceptance gate from the Substrate repository root:
npm run verify:onboarding:p0The gate requires Node 22, npm registry access for disposable installs, a real
swiftc, and a Java-backed kotlinc. Missing native toolchains are blocking
failures; there is no local or CI skip mode.
The command packs and installs @unknown-creatives/substrate, assembles the allowlisted
engine bundle through that installed public command, and invokes onboarding
only through the documented substrate setup --plan / --apply interface.
Disposable consumers have registry-installed Vite, React, and TypeScript
dependencies and no source-checkout aliases or links. The gate builds the real
Vite application, compiles minimal Swift and Kotlin consumers against the
shipped kernels and projected output, proves an unchanged rerun, and contracts
brands, components, and targets from generated-manifest ownership while
preserving an unmanifested client file. It also proves byte restoration and no
success output for artifact tampering, invalid declaration, generation,
Swift/Compose compilation, and consumer-build failures.
The pull-request checks job and the release workflow run this exact command.
P0 is not green, and a release cannot be cut, unless every black-box contract
passes with all three CSS, Swift, and Compose targets enabled.
Toolchain aliases
substrate init also sets up the supported @substrate/* import aliases for the
project's toolchain (UCS-703). The alias list is sourced at runtime from the
src/aliases.ts policy SSOT (UCS-702) — the CLI never hardcodes a second list,
so a contract change can't drift from what init writes.
- Auto-write (default): the two public aliases (
@substrate/engine,@substrate/generated/*) are written intotsconfig.jsoncompilerOptions.paths(the safe structured surface). The write is idempotent and preserves existing options and unrelated paths.baseUrlis never added — TypeScript 7 removed the option (TS5102), and the./-prefixed targets resolve relative to the tsconfig without it. - Report for bundlers: Vite, Vitest, Webpack, and Next configs are code and
unsafe to rewrite, so when one is detected
initprints the exact alias block to add by hand instead of editing it. - Report-only (
--report-aliases): print every alias block (tsconfig + detected bundlers) without writing anything.
--dry-run previews the alias step without writing. Targets resolve against any
roots: remapping in .substrate/state.yaml.
Catalog add
substrate add is the P4·c v0 catalog path. It resolves a path-derived entry id
from a local catalog, validates the author-facing entry.yaml, copies
scaffoldable files into the client content root with provenance headers, records
the fetch in .substrate/manifest.yaml, and runs the caller-provided generate
command. V0 deliberately has no customer permission, tier, or delivery-token
model.
substrate add components/button \
--catalog ./catalog-staging \
--generate-command "npm run generate"Inside the Substrate repo, the same public command scaffolds entries from
catalog-staging/ into a client-shaped target for dogfooding.
Local engine artifact assembly
substrate artifact assemble is the P0 local engine delivery path. It compiles
the generator and its runtime packages into a standalone Node compiler
entrypoint, independently bundles the browser-safe runtime entrypoint, adds
only sealed engine data from the explicit allowlist, and writes one
deterministic artifact bundle plus verification metadata:
substrate artifact assemble \
--source-root . \
--output-directory dist/local-engine \
--version 0.39.0 \
--signing-private-key env:SUBSTRATE_RELEASE_SIGNING_PRIVATE_KEYFor version 0.39.0, the output set is:
substrate-engine-0.39.0.bundle— the exact bytes delivered and consumedsubstrate-engine-0.39.0.attestation.json— version, byte length, checksum, signature, member hashes, and compiler/browser-runtime input provenancesubstrate-engine-0.39.0.bundle.sha256substrate-engine-0.39.0.bundle.sig
The final-member allowlist contains compiler.mjs, the browser
runtime.mjs plus its public TypeScript declarations, a minimal artifact
package.json, sealed system/property YAML, the chart and text-role defaults
required by client CSS projection, the public alias policy, and static web
cascade CSS. It also contains the exact Swift/Kotlin kernel sources and
SwiftUI/Compose API stubs required to compile generated native projections
during client setup. Those native validation inputs have their own canonical
provenance list and must match bundle members byte-for-byte. The runtime is
compiled separately from the Node compiler so the disposable application
imports one browser-safe public entrypoint rather than engine source internals.
generated/, src/brands/, src/components/, and client-owned
substrate.config.yaml are not members. Compiler source and package inputs are
also enumerated by exact path in checked-in compiler and browser-runtime
allowlists; both exact path sets are recorded in artifact provenance. Assembly
fails if either build graph reaches a newly imported test, demo, generated
registry, source, or package file until that input is explicitly reviewed and
added.
Setup can use the local provider seam without trusting a mutable path:
import {
createLocalEngineArtifactProvider,
inspectEngineArtifact,
} from '@unknown-creatives/substrate/engine-artifact';
const declaration = inspectEngineArtifact({
artifactPath: './incoming/substrate-engine-0.39.0.bundle',
attestationPath: './incoming/substrate-engine-0.39.0.attestation.json',
});
// declaration.verification === 'unverified'
// declaration exposes version, entrypoint, and immutable memberPaths for planning.
const provider = createLocalEngineArtifactProvider({
artifactPath: './incoming/substrate-engine-0.39.0.bundle',
attestationPath: './incoming/substrate-engine-0.39.0.attestation.json',
checksumPath: './incoming/substrate-engine-0.39.0.bundle.sha256',
signaturePath: './incoming/substrate-engine-0.39.0.bundle.sig',
publicKey,
});
const artifact = await provider.acquire();
// artifact.verification === 'verified'
artifact.extractTo(stagingDirectory);inspectEngineArtifact() and provider.inspect() parse only declared,
read-only planning metadata and never extract or confer trust. Setup may use
their immutable member paths to build a plan, but it must not mutate a client
from an unverified declaration. acquire() reads the bundle once, then
verifies the byte length, SHA-256 checksum sidecar, Ed25519 signature sidecar,
matching attestation, version, attested provenance, member set, member hashes,
and safe paths. readBytes() returns a copy of those retained verified bytes and
extractTo() materializes from the retained bundle, so replacing the local
file after verification cannot change what gets staged. Extraction requires an
empty staging directory. The artifact test executes the extracted
compiler.mjs against client-owned brand intent, then asserts that the compiler
plans and emits a generated registry without node_modules or a source checkout.
The release workflow uses this same exact-byte assembler for engine delivery.
Both substrate setup --apply and substrate upgrade acquire the engine
through this one provider, so the artifact that onboards a client is the
artifact that upgrades them (UCS-1118). The existing directory-based catalog
signer remains separate.
Deterministic engine upgrade
substrate upgrade is the P3·d updater v1: no git merge, no semantic merge,
and no silent overwrite of manifest-tracked engine edits.
Upgrade takes the same signed .bundle and sidecars that setup --apply
consumes — one delivery contract across onboarding and updates. Verification
is not reimplemented here: upgrade calls
createLocalEngineArtifactProvider().acquire(), the single implementation
described above, so byte length, checksum, signature, attestation, member
hashes, and safe member paths are checked identically on both paths. The engine
is then extracted from the retained verified bytes, never copied from a mutable
directory. The command:
- Acquires and verifies the signed engine bundle through the shared provider
before touching the current engine, and refuses a bundle whose declared
version disagrees with
--engine-version. - Confirms the current
engine.fileshash set in.substrate/manifest.yamlstill matches the vendored engine on disk. If it does not, the command stops before swapping and requires an explicit three-door decision. - Replaces the resolved engine root wholesale with the verified artifact.
- Runs versioned config-schema codemods from the new engine registry.
- Runs caller-provided regenerate and verify commands, then refreshes the manifest engine version/hash set while preserving catalog fetch records.
- Writes Markdown and JSON reports under
.substrate/reports/and prints the same renderer's summary to the terminal.
substrate upgrade \
--engine-artifact ./incoming/substrate-engine-0.37.0.bundle \
--signing-public-key ./uc-release-public-key.pem \
--engine-version 0.37.0 \
--regenerate-command "npm run generate" \
--verify-command "npm run generate:check && npm run test:e2e:engine" \
--modified-engine refuse \
--report-id 2026-06-13-client-upgradeThe .attestation.json, .bundle.sha256, and .bundle.sig sidecars are
resolved next to the bundle, exactly as setup resolves them, so a client
passes the release handoff files unchanged.
Migrating from the directory contract
--engine-artifact no longer accepts an unpacked engine directory; passing one
fails with a migration message before any engine mutation. --artifact-sha256
and --artifact-signature are likewise retired — the bundle carries its own
checksum and signature sidecars, verified against the attestation, so those
values are no longer passed by hand. If you only have a source checkout, produce
a bundle with substrate artifact assemble and pass that.
The signature payload is deterministic and covers artifact kind, target version, and checksum:
substrate-artifact-signature-v1
kind=engine
version=0.37.0
sha256=<sha256>--signing-public-key accepts a PEM string, a file path, or env:NAME. The
release workflow assembles the signed engine bundle through the exact-byte path
above and signs catalog attestations with
SUBSTRATE_RELEASE_SIGNING_PRIVATE_KEY; clients receive the public key through
the engagement handoff and the updater refuses unsigned or tampered artifacts
before the engine swap.
Release signing public key
The current release signing public key is recorded in
docs/release/substrate-release-signing-public-key.pem and distributed with
signed release assets as substrate-release-signing-public-key.pem. Its
SHA-256 fingerprint over the SPKI DER public key is:
b61d2c945cd9fad091d9ec2fbd1c5e4ec1fa6097e294b00803744aa472363a6eRelease handoffs should include the public key, this fingerprint, and the
matching *.attestation.json, *.sha256, and *.sig files for the delivered
engine or catalog artifact. The private key remains only in owner-controlled
storage and the GitHub Actions secret named
SUBSTRATE_RELEASE_SIGNING_PRIVATE_KEY; it must never be committed, attached
to a release, or pasted into handoff notes. If the key is rotated, publish the
new public key and fingerprint before signing the first release with it, then
call out the rotation in release notes and engagement handoff material.
When the current engine has local edits, --modified-engine refuse (the
default) prints the modified paths and the three available doors without
touching disk: discard replaces the edited engine with the new artifact,
freeze exits successfully while staying on the current version, and
engage-uc exits successfully with an upstream/white-glove handoff prompt.
Only discard may overwrite engine edits, and it must be selected explicitly.
Engine artifacts may ship upgrade codemods in
codemods/registry.yaml. This framework is specific to deterministic engine
upgrades and is deliberately separate from the guided src/migration/ domain.
Entries run in target-version order during the post-swap-pre-regenerate phase:
version: 1
codemods:
- id: config-schema-002
from: 0.1.0
to: 0.2.0
phase: post-swap-pre-regenerate
command: node substrate/engine/codemods/config-schema-002.jsCodemod commands run from the client project root with
SUBSTRATE_PROJECT_ROOT, SUBSTRATE_ENGINE_ROOT, SUBSTRATE_CONTENT_ROOT,
SUBSTRATE_GENERATED_ROOT, and SUBSTRATE_CODEMOD_* environment variables.
The phase field defaults to post-swap-pre-regenerate; the explicit field is
kept so a future post-regenerate phase can be added without changing the
registry shape.
The report lists refreshed pristine engine files, modified delivered catalog
files, path-collision counts, and knowledge/reference overlay conflicts. Overlay
conflicts cover same-path overlays plus conservative semantic duplicates:
Markdown/frontmatter titles provide the semantic key, token overlap provides a
deterministic confidence score, and each finding carries a stable id plus a
cleanup recommendation for Bloom. Its JSON sidecar is the durable input for the
later substrate adopt flow (P3·f).
Bloom reconciliation
substrate adopt consumes an upgrade report JSON sidecar and turns flagged
findings into stable, per-finding actions. Listing mode never mutates disk:
substrate adopt --report .substrate/reports/upgrade-2026-06-13-client-upgrade.jsonApply mode requires one explicit finding id. The first implementation only performs deterministic file-level moves: file path collisions copy the engine file into the client path, while exact byte-identical knowledge/reference overlays are removed because the engine now carries the same content. Shadow overlays, semantic duplicates, directory collisions, and modified delivered catalog files remain manual/Bloom-reconciliation plans.
substrate adopt \
--report .substrate/reports/upgrade-2026-06-13-client-upgrade.json \
--finding path-collision:0 \
--applyEvery applied finding writes a rollback record under .substrate/adopt/ before
changing files and prints the rollback id:
substrate adopt --rollback adopt-2026-06-13t18.20.00.000z-12345-path-collision-0A wrong merge is any applied finding that fails validation or user review after adoption. The rollback record restores the exact previous file bytes (or absence) for that finding, giving the client a reversible default while larger semantic choices stay in manual Bloom review.
Substrate location detection
detectSubstrate() (bin/lib/detect.js) probes in priority order; the
resolved path is always the directory containing skills/ and agents/:
| Probe | Model | Path |
|---|---|---|
| $SUBSTRATE_PATH set | manual | env var value (must stay inside the project) |
| substrate/engine/skills/substrate-config/ | client | substrate/engine |
| engine/skills/substrate-config/ | client | engine (substrate root remapped onto the repo root) |
| node_modules/@unknown-ui/substrate/skills/substrate-config/ | install (legacy) | node_modules/@unknown-ui/substrate |
| substrate/skills/substrate-config/ | clone-subdir (legacy) | substrate |
| skills/substrate-config/ | clone-root | . |
The client model is the v2 client layout (spec §4): a vendored engine at
{substrate root}/engine/, the sealed unit UC replaces wholesale. It is
probed before every legacy model so a v2 client repo is never misdetected as
clone-subdir and a leftover npm install never shadows the vendored engine.
The npm install model is superseded
The npm-symlink install model (npm install @unknown-ui/substrate +
symlinks into node_modules/) is formally superseded by vendored
delivery — decision D10 of the
2026-06-12 distribution architecture v2
(§10). Detection of node_modules/@unknown-ui/substrate stays so existing
installs keep working, but no new capability targets it.
To be precise about what is and isn't published: what was superseded is
engine-in-node_modules; what is published today is the CLI only
(@unknown-creatives/substrate — the bin/ + assets/ surface of this
package). The engine is never published; it arrives through the signed
bundle (supervised engagement) or a vendored checkout, and P4·h's
engine-packaging question is unchanged by CLI publishing.
The packaging research behind it (npm files manifest, symlink-through-
node_modules resolution, update flow) is preserved in the
2026-05-18 companion spec
and re-enters as the npm-engine packaging option (P4·h) if self-serve
productization wants it. That trigger — not general demand — is what would
revive the model.
