/*
RFC-0354 §7.1 + RFC-1307: sternsystem.register — add a new Sternsystem to the fleet
registry from a validated werkstatt:intake/v1 bundle: create pin, seed system.md, the
primary-locale business profile and publishable assets deterministically, record the
intake provenance, open the first mission, and trigger materialization.
With --amend: update pin and open amend mission without creating a new registry entry.
- Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.
- Do not reimplement pin, mission.open, or mission.materialize logic — delegate to existing commands.
- RFC-1328: strip envelope-only statementByLocale/localeNeutral before the strict pbp/*@1 parse in prepareIntakeSeed.
- RFC-1307: seed default operator config files at registration
- RFC-1307: seed the platform CSP sink allowlist disposition at registration
- RFC-1325: provisional workers.dev as first-class site host
Single host predicate module in werkstatt-shared; provisional registration derives dev/alt/main workers.dev channels from the account subdomain; first dev deploy auto-protects provisional hosts; provisional main responses carry noindex; dns-aid skips provisional sites; site.domain.adopt cuts over to a custom domain.
- RFC-1325: review r1 — pin-carrying probes, AC-3 write ordering, dns-aid validate parity, hardened host lint
- RFC-1326: single Cloudflare permission registry, token preflight, birth provisioning
REVIEW-CODE-2026, RFC-0354, RFC-0532, RFC-0561, RFC-0902, RFC-1149, RFC-1249, RFC-1293, RFC-1294, RFC-1307
*/
import { isAbsolute, join, resolve } from "node:path";
import { cacheCloneCommit } from "../mission/mission-git-commit.ts";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
KernelFlagValue,
} from "@warpgogol/werkstatt-engine/kernel";
import { StarCatalog, type StarName } from "@warpgogol/werkstatt-shared/ontology/cosmic";
import { intakeEnvelopeSchema } from "@warpgogol/werkstatt-shared/onboarding/intake";
import { pbpSchemaById } from "@warpgogol/pbp/entities";
import { byteHash } from "@warpgogol/werkstatt-shared/fingerprint";
import { resolveIntakeExportSha256, validateIntakeBundle } from "../intake/intake-validate.ts";
import { isSeedableAsset, planSeedFromIntake, type IntakeSeedPlan } from "./seed-from-intake.ts";
import {
discoverSystems,
readSystemConfig,
writeSystemConfig,
hasAppsCollision,
resolveMirrorPath,
resolveCacheClonePath,
readSystemState,
writeSystemState,
} from "./registry-io.ts";
import { runSternsystemPin } from "./sternsystem-pin.ts";
import { OPERATOR_CONFIG_FILES } from "../mission/operator-config-files.ts";
import { identityConfigPath } from "../identity/identity-io.ts";
import { runMissionOpen } from "../mission/mission-open.ts";
import { runMissionAbort } from "../mission/mission-abort.ts";
import { hasTldSuffix } from "@warpgogol/werkstatt-shared/ontology/operations";
import type { WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { failResult } from "@warpgogol/werkstatt-shared/checks/result-helpers";
import {
isBareHostname,
isProvisionalHost,
} from "@warpgogol/werkstatt-shared/middleware/site-host";
import { deriveProvisionalDeployment, type ProvisionalDeployment } from "./provisional-host.ts";
import { resolveSubdomainEnv } from "../subdomain/subdomain-helpers.ts";
import { provisionExternalResources } from "./provision-external-resources.ts";
export interface SternsystemRegisterData {
command: "sternsystem.register";
system: string;
status: "pass" | "fail";
registryEntry?: {
id: string;
cosmicStar: string;
status: "registered" | "active";
registeredAt: string;
};
pinPath?: string;
firstMissionId?: string;
/** RFC-1307: what register seeded from the intake. */
seed?: { intakeId: string; pbpDocuments: number; assets: number };
diagnostics: string[];
}
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
function flagBool(input: KernelCommandInput, key: string): boolean {
const v = input.flags[key];
return typeof v === "boolean" ? v : false;
}
function flagNumber(input: KernelCommandInput, key: string): number | undefined {
const v = input.flags[key];
return typeof v === "number" ? v : typeof v === "string" ? Number(v) : undefined;
}
function makeInput(flags: Record): KernelCommandInput {
return { flags, argv: [] };
}
/**
* Ask the site plugin, through the kernel registry, whether a biome id exists
* and passes its contract. Returns null when it does, else the reason.
*/
async function checkBiomeViaKernel(workspaceRoot: string, biomeId: string): Promise {
const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel");
const result = (await executeKernelCommand({
workspaceRoot,
commandName: "biome.contract.validate",
argv: [`--biome=${biomeId}`],
outputFormat: "json",
})) as { exitCode?: number; data?: { diagnostics?: Array<{ ruleId: string; message: string }> } };
if ((result.exitCode ?? 1) === 0) return null;
return (
result.data?.diagnostics?.map((d) => `${d.ruleId}: ${d.message}`).join("; ") ||
"biome.contract.validate failed"
);
}
/** Seams for tests — production code always uses the kernel-registry check. */
export const registerDeps = {
checkBiome: checkBiomeViaKernel,
openMission: runMissionOpen,
provisionExternal: provisionExternalResources,
};
/**
* RFC-1307 preconditions for a new registration — run before any file write.
* Returns the parsed envelope, the intake.json digest, and the seedable asset
* bytes, or throws with an actionable message.
*/
export async function prepareIntakeSeed(
input: {
id: string;
cosmicStar: string;
intakeDir: string;
biomeId: string;
domain?: string;
},
context: KernelRuntimeContext,
): Promise {
const { io, workspaceRoot } = context;
const bundleDir = isAbsolute(input.intakeDir)
? input.intakeDir
: resolve(workspaceRoot, input.intakeDir);
const exportInfo = await resolveIntakeExportSha256(io);
if (exportInfo === null) {
throw new Error(
"[sternsystem.register] intake.schema.generated.yaml is missing — run onboarding.intake.schema.export first",
);
}
const validation = await validateIntakeBundle(bundleDir, io, {
exportSha256: exportInfo.sha256,
bundleLabel: input.intakeDir,
});
const errors = validation.diagnostics.filter((d) => d.severity === "error");
if (errors.length > 0) {
const listed = errors
.slice(0, 10)
.map((d) => `${d.rule}: ${d.message}`)
.join("; ");
throw new Error(
`[sternsystem.register] intake ${input.intakeDir} fails onboarding.intake.validate (${errors.length} error(s)) — ${listed}`,
);
}
// Warnings are reported but do not block (RFC-1307).
for (const d of validation.diagnostics.filter((d) => d.severity === "warning")) {
context.logger.warn(`[sternsystem.register] intake ${d.rule}: ${d.message}`);
}
const intakeBytes = await io.readFileBytes(join(bundleDir, "intake.json"));
const intake = intakeEnvelopeSchema.parse(JSON.parse(new TextDecoder().decode(intakeBytes)));
if (intake.systemId !== input.id) {
throw new Error(
`[sternsystem.register] intake systemId '${intake.systemId}' differs from --id '${input.id}' — provenance follows the envelope`,
);
}
// Materialization needs a site host (public.infrastructure.generate, astro
// site URL) — refuse before any write instead of failing mid-materialize.
const intakeDomain = intake.classification.domain ?? null;
if (input.domain !== undefined && intakeDomain !== null && input.domain !== intakeDomain) {
throw new Error(
`[sternsystem.register] --domain '${input.domain}' conflicts with the intake's confirmed domain '${intakeDomain}' — the intake wins; drop --domain`,
);
}
if (intakeDomain === null && input.domain === undefined) {
throw new Error(
"[sternsystem.register] the intake carries no confirmed domain — pass --domain (e.g. the provisional ..workers.dev host) until the real domain is decided", // site-host: operator-facing guidance text, not host classification
);
}
if (input.domain !== undefined && !isBareHostname(input.domain)) {
throw new Error(
`[sternsystem.register] --domain '${input.domain}' is not a bare lowercase host name (no scheme, no path)`,
);
}
// RFC-1325: a provisional workers.dev host is a first-class birth host —
// verify the account + worker-name availability and derive the deployment
// block before any write.
const effectiveDomain = intakeDomain ?? input.domain;
let provisionalDeployment: ProvisionalDeployment | undefined;
if (effectiveDomain !== undefined && isProvisionalHost(effectiveDomain)) {
const env = await resolveSubdomainEnv();
provisionalDeployment = await deriveProvisionalDeployment(
input.id,
effectiveDomain,
io,
workspaceRoot,
env,
);
}
// Biome check through the kernel registry — the engine never imports the
// site plugin or hardcodes its catalog path (RFC-1307, RFC-0769).
const biomeError = await registerDeps.checkBiome(workspaceRoot, input.biomeId);
if (biomeError !== null) {
throw new Error(
`[sternsystem.register] --biome '${input.biomeId}' rejected — ${biomeError}. Run biome.from-intake --id ${input.id} --family first.`,
);
}
// DNA-103: every entity parses through its canonical schema before any asset
// bytes are read — a broken entity must outrank a missing asset file.
for (const entity of intake.pbp) {
const schema = pbpSchemaById[entity.schema];
if (!schema) {
throw new Error(
`[sternsystem.register] no canonical PBP schema for ${entity.schema} (${entity.id})`,
);
}
// AMD-005: statementByLocale/localeNeutral are envelope-level — strip
// before the frozen pbp/*@1 strict parse.
const {
statementByLocale: _statementByLocale,
localeNeutral: _localeNeutral,
...canonicalEntity
} = entity as Record;
schema.parse(canonicalEntity);
}
const assetBytes = new Map();
for (const asset of intake.assets.filter(isSeedableAsset)) {
assetBytes.set(asset.id, await io.readFileBytes(join(bundleDir, asset.file)));
}
const plan = planSeedFromIntake({
intake,
intakeSha256: byteHash(intakeBytes),
systemId: input.id,
cosmicStar: input.cosmicStar,
biomeId: input.biomeId,
...(input.domain !== undefined ? { domainFallback: input.domain } : {}),
assetBytes,
});
return provisionalDeployment === undefined ? plan : { ...plan, provisionalDeployment };
}
async function writeSeedFiles(
systemDir: string,
plan: IntakeSeedPlan,
io: WorkspaceIO,
written: string[],
): Promise {
// Refuse to overwrite anything already on disk — a pre-existing file at a
// planned path is not register's to clobber, and rollback must never delete
// it (REVIEW-CODE-2026-10-16-09 N12). Checked before the first write so a
// collision leaves zero partial output.
for (const file of plan.files) {
const target = join(systemDir, ...file.path.split("/"));
if (await io.exists(target)) {
throw new Error(
`[sternsystem.register] seed path already exists: ${file.path} — refusing to overwrite pre-existing content`,
);
}
}
for (const file of plan.files) {
const target = join(systemDir, ...file.path.split("/"));
await io.mkdir(join(target, ".."));
await io.writeFile(target, file.content);
written.push(file.path);
}
}
async function rollbackConfig(workspaceRoot: string, id: string, io: WorkspaceIO): Promise {
const cacheClone = resolveCacheClonePath(workspaceRoot, id);
const configPath = join(cacheClone, "system-config.yaml");
if (await io.exists(configPath)) {
await io.rm(configPath, { recursive: true });
}
}
async function rollbackSystemState(
workspaceRoot: string,
id: string,
io: WorkspaceIO,
): Promise {
const cacheClone = resolveCacheClonePath(workspaceRoot, id);
const statePath = join(cacheClone, "system-state.yaml");
if (await io.exists(statePath)) {
await io.rm(statePath, { recursive: true });
}
}
async function rollbackPin(systemDir: string, io: WorkspaceIO): Promise {
const pinPath = join(systemDir, "system.pin.json");
if (await io.exists(pinPath)) {
await io.rm(pinPath, { recursive: true });
}
}
/** Remove the seeded files register actually wrote + dirs it created; pre-existing content stays. */
async function rollbackSeedFiles(
systemDir: string,
seededPaths: string[],
io: WorkspaceIO,
): Promise {
const parentDirs = new Set();
for (const filePath of seededPaths) {
const target = join(systemDir, ...filePath.split("/"));
if (await io.exists(target)) {
await io.rm(target, { recursive: true });
}
let dir = join(target, "..");
while (dir !== systemDir && dir.startsWith(systemDir)) {
parentDirs.add(dir);
dir = join(dir, "..");
}
}
// Deepest first — a dir that still holds entries is pre-existing and stays.
for (const dir of [...parentDirs].sort((a, b) => b.length - a.length)) {
try {
if ((await io.readdir(dir)).length === 0) {
await io.rm(dir, { recursive: true });
}
} catch {
// Already removed by an earlier prune — cosmetic only.
}
}
}
async function rollbackContent(systemDir: string, io: WorkspaceIO): Promise {
const contentPath = join(systemDir, "src", "content");
if (await io.exists(contentPath)) {
await io.rm(contentPath, { recursive: true });
}
}
/**
* Remove the clone dir only when it is git-less AND empty. Callers invoke this
* solely for dirs register itself created — a pre-existing dir is never
* removed, no matter its contents (REVIEW-CODE-2026-10-16-09 N8). Returns the
* outcome so the caller can log WHY a dir survived; a failed readdir/rm keeps
* the dir and never throws — rollback must not escape the catch handler with
* a secondary error (REVIEW-CODE-2026-10-16-10 O1).
*/
type DirRollbackOutcome = "removed" | "kept-git" | "kept-nonempty" | "kept-error";
async function rollbackSystemDirIfEmpty(
systemDir: string,
io: WorkspaceIO,
): Promise {
if (!(await io.exists(systemDir))) return "removed";
const gitDir = join(systemDir, ".git");
if (await io.exists(gitDir)) return "kept-git";
let entryCount: number;
try {
entryCount = (await io.readdir(systemDir)).length;
} catch {
return "kept-error";
}
if (entryCount !== 0) return "kept-nonempty";
try {
await io.rm(systemDir, { recursive: true });
} catch {
return "kept-error";
}
return "removed";
}
/** Git state captured before register's first write — rollback trusts only this. */
interface PreRegisterGit {
/** The probe ran; refs + sha are trustworthy. False = skip ref surgery entirely. */
captured: boolean;
/** HEAD commit at capture — null only when HEAD was genuinely unborn. */
sha: string | null;
/** refs/heads/* present before register — never touched by rollback. */
refs: string[];
/** The branch register pushes to (origin/HEAD-derived, falls back to "main"). */
branch: string;
/**
* Non-untracked entries in git status at capture — uncommitted tracked or
* staged work register would destroy with reset --hard on rollback. Register
* refuses before the first write (REVIEW-CODE-2026-10-16-09 N12).
*/
dirty: boolean;
/**
* origin's refs/heads/* at capture (sha by refname). Null = ls-remote was
* unreachable/failed — rollback then skips ALL remote surgery rather than
* guess at remote state (REVIEW-CODE-2026-10-16-09 N9).
*/
remoteRefs: Record | null;
}
/**
* A fresh Sternsystem has no cache clone repository yet: initialise it in
* place (`git init -b main`) with `origin` pointing at the local bare mirror
* (mirrors[1]), so the scaffold commit, its push and mission.open's Bordbuch
* commit have a repository to land in. An existing repository is left alone;
* an external-only mirrors[1] (URL) leaves the clone without origin.
* Returns true when register created the repository (rollback removes it).
*/
async function ensureCacheCloneRepo(
systemDir: string,
mirrors: ReadonlyArray<{ path: string; storageType: string }>,
workspaceRoot: string,
io: WorkspaceIO,
): Promise {
if (await io.exists(join(systemDir, ".git"))) return false;
const git = async (args: string[]) => {
const result = await io.exec("git", args, { cwd: systemDir, timeoutMs: 10000 });
if (result.exitCode !== 0) {
throw new Error(
`[sternsystem.register] git ${args.join(" ")} exited ${result.exitCode}: ${result.stderr.slice(0, 200)}`,
);
}
};
await git(["init", "-b", "main"]);
const bare = mirrors[1];
if (bare?.storageType === "bare" && !/^[a-z]+:\/\/|^[^/]+@[^:]+:/i.test(bare.path)) {
await git(["remote", "add", "origin", resolveMirrorPath(workspaceRoot, bare.path)]);
}
return true;
}
/**
* Default content for the required operator config files (RFC-0840). A fresh
* system has none, and workpiece.config.presence.check would fail the first
* mission.validate; register seeds them into its scaffold commit (missions
* track and reconcile later edits). Operators tune them in missions.
*/
export const OPERATOR_CONFIG_DEFAULTS: Readonly<
Record<(typeof OPERATOR_CONFIG_FILES)[number], string>
> = {
".lighthouse-budget-ignore":
"# RFC-0833 LH-12 escape hatch: one JS bundle name per line that is emitted\n# but intentionally not referenced by any page. Seeded empty at registration.\n",
"src/image-delivery.config.yaml":
"# RFC-0830 IMG-DELIVERY escape-hatch overrides.\n# Each entry needs: srcPattern, rules[], reason. Seeded empty at registration.\noverrides: []\n",
};
/**
* Site-level defaults every Astro site needs from its first build: the CSP sink
* allowlist disposition for the platform schema chunk (guarded `new Function`
* capability probe emitted into every site bundle). Dispositions stay per-site
* (csp.sinks.validate never blesses sinks itself); this only seeds the common one.
*/
export const SITE_BIRTH_DEFAULTS: Readonly> = {
"src/csp-sinks.allowlist.yaml": [
"version: 1",
"entries:",
' - filePattern: "_astro/schemas.*.js"',
" disposition: accepted-debt",
" note: \"Platform schema bundle carries a guarded new Function('') capability probe (try/catch + UA check); no user-controlled markup reaches it.\"",
"",
].join("\n"),
};
/** Write missing operator config and site birth defaults; returns the paths register created. */
async function seedOperatorConfigDefaults(systemDir: string, io: WorkspaceIO): Promise {
const created: string[] = [];
const defaults: Array<[string, string]> = [
...OPERATOR_CONFIG_FILES.map((rel): [string, string] => [rel, OPERATOR_CONFIG_DEFAULTS[rel]]),
...Object.entries(SITE_BIRTH_DEFAULTS),
];
for (const [rel, content] of defaults) {
const target = join(systemDir, ...rel.split("/"));
if (await io.exists(target)) continue;
await io.mkdir(join(target, ".."));
await io.writeFile(target, content);
created.push(target);
}
return created;
}
/**
* Probe the clone's git state before register writes: HEAD sha (or confirmed
* unborn), the pre-existing branch refs, and the push branch. An ambiguous
* probe (rev-parse fails without an unborn HEAD, or any exec throw) yields
* captured=false — rollback then skips ref surgery rather than risk deleting
* branches it cannot attribute to register (REVIEW-CODE-2026-10-16-08 N1).
*/
async function capturePreRegisterGit(systemDir: string, io: WorkspaceIO): Promise {
const failed: PreRegisterGit = {
captured: false,
sha: null,
refs: [],
branch: "main",
dirty: false,
remoteRefs: null,
};
if (!(await io.exists(join(systemDir, ".git")))) {
return { ...failed, captured: true };
}
const git = (args: string[]) => io.exec("git", args, { cwd: systemDir, timeoutMs: 10000 });
try {
let branch = "main";
const remoteHead = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
if (remoteHead.exitCode === 0 && remoteHead.stdout.trim()) {
branch = remoteHead.stdout.trim().replace(/^origin\//, "");
}
const refsResult = await git(["for-each-ref", "--format=%(refname)", "refs/heads"]);
if (refsResult.exitCode !== 0) return failed;
const refs = refsResult.stdout
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
// Tracked modifications or staged entries would be destroyed by the
// rollback's reset --hard — register refuses to run at all (N12). Untracked
// (??) entries are safe: reset --hard never touches them and the
// pathspec-scoped add never stages them.
const status = await git(["status", "--porcelain"]);
if (status.exitCode !== 0) return failed;
const dirty = status.stdout
.split("\n")
.some((line) => line.trim() !== "" && !line.startsWith("??"));
const head = await git(["rev-parse", "HEAD"]);
let sha: string | null = null;
if (head.exitCode === 0) {
sha = head.stdout.trim() || null;
} else {
// rev-parse fails for unborn HEAD and for corrupt/detached states alike —
// only a symbolic HEAD pointing at a branch ref proves "unborn".
const sym = await git(["symbolic-ref", "-q", "HEAD"]);
if (!(sym.exitCode === 0 && sym.stdout.trim().startsWith("refs/heads/"))) return failed;
}
// Remote refs at capture — the rollback's remote surgery trusts only this
// snapshot, never possibly-stale remote-tracking refs (N9). An unreachable
// or unconfigured origin yields null → remote steps are skipped with a
// diagnostic rather than guessed.
let remoteRefs: Record | null = null;
const remote = await git(["ls-remote", "origin", "refs/heads/*"]);
if (remote.exitCode === 0) {
remoteRefs = {};
for (const line of remote.stdout.split("\n")) {
const [rsha, rref] = line.split("\t");
const ref = rref?.trim();
if (rsha?.trim() && ref?.startsWith("refs/heads/")) {
remoteRefs[ref] = rsha.trim();
}
}
}
return { captured: true, sha, refs, branch, dirty, remoteRefs };
} catch {
return failed;
}
}
/**
* Rewind the commits register pushed into the cache clone: the writeSystemState
* auto-commit, the scaffold commit and their pushes to the bare remote. Runs
* AFTER the file-level helpers — a reset restores pre-existing tracked files,
* so deleting register's files first and resetting second leaves the clone
* exactly at its pre-register state (REVIEW-CODE-2026-10-16-08 N2). Only refs
* register created are deleted — anything captured pre-register is untouched.
* Failures are reported via diagnostics, never silently swallowed.
*/
async function rollbackGitHistory(
systemDir: string,
pre: PreRegisterGit,
io: WorkspaceIO,
diagnostics: string[],
): Promise {
const gitDir = join(systemDir, ".git");
if (!(await io.exists(gitDir))) return;
if (!pre.captured) {
diagnostics.push(
"rollback: pre-register git probe was unreliable — skipped ref surgery; file-level rollback already ran",
);
return;
}
const git = (args: string[]) => io.exec("git", args, { cwd: systemDir, timeoutMs: 10000 });
// Delete a remote ref: leased push first — the lease is anchored on the sha
// observed in the just-read remote snapshot, so a foreign push landing in the
// window between read and delete fails the lease instead of being removed
// (REVIEW-CODE-2026-10-16-10 O2). When the remote is a local-path mirror
// (the systems-git convention) whose HEAD names the ref, receive-pack's
// denyDeleteCurrent refuses — the operator owns the dir, so verify the ref
// still holds the expected sha and delete in place; a moved ref is foreign
// and survives.
const tryDeleteRemoteRef = async (refname: string, expectedSha: string): Promise => {
const del = await git([
"push",
`--force-with-lease=${refname}:${expectedSha}`,
"origin",
`:${refname}`,
]);
if (del.exitCode === 0) return;
const url = await git(["remote", "get-url", "origin"]);
const configured = url.exitCode === 0 ? url.stdout.trim() : "";
const remotePath = configured.startsWith("file://")
? configured.slice("file://".length)
: configured;
const resolved =
remotePath && !isAbsolute(remotePath) ? join(systemDir, remotePath) : remotePath;
if (resolved && (await io.exists(resolved))) {
const cur = await git(["-C", resolved, "rev-parse", "--verify", refname]);
const curSha = cur.exitCode === 0 ? cur.stdout.trim() : "";
if (curSha !== expectedSha) {
diagnostics.push(
`rollback: remote ${refname} moved since the last observation (${curSha.slice(0, 12) || "absent"}) — left untouched`,
);
return;
}
const direct = await git(["-C", resolved, "update-ref", "-d", refname]);
if (direct.exitCode === 0) return;
diagnostics.push(
`rollback: in-place remote delete of ${refname} failed (non-fatal): ${direct.stderr.slice(0, 160)}`,
);
return;
}
diagnostics.push(
`rollback: remote delete of ${refname} failed (non-fatal): ${del.stderr.slice(0, 160)}`,
);
};
try {
if (pre.sha) {
const reset = await git(["reset", "--hard", pre.sha]);
if (reset.exitCode !== 0) {
diagnostics.push(
`rollback: git reset --hard ${pre.sha} failed: ${reset.stderr.slice(0, 160)}`,
);
}
}
// Rewind the remote branch register pushed to — anchored on the remote
// refs captured pre-register AND the remote's state right now, never on
// possibly-stale remote-tracking refs (REVIEW-CODE-2026-10-16-09 N9).
// Three cases: register created the remote branch → delete it; register
// moved a pre-existing branch → restore the captured sha with a lease
// pinned to the sha just observed; remote untouched → nothing to do.
const remoteKey = `refs/heads/${pre.branch}`;
// Remote state observed right now — read once, reused by the branch surgery
// AND the created-refs cleanup below (REVIEW-10 O2: deletes consult it too).
let nowRefs: Record | null = null;
if (pre.remoteRefs === null) {
diagnostics.push(
"rollback: remote refs were not captured — remote branch state left untouched",
);
} else {
const now = await git(["ls-remote", "origin", "refs/heads/*"]);
if (now.exitCode !== 0) {
diagnostics.push(
`rollback: ls-remote failed — remote state unverifiable, remote left untouched (non-fatal): ${now.stderr.slice(0, 160)}`,
);
} else {
nowRefs = {};
for (const line of now.stdout.split("\n")) {
const [rsha, rref] = line.split("\t");
const ref = rref?.trim();
if (rsha?.trim() && ref?.startsWith("refs/heads/")) {
nowRefs[ref] = rsha.trim();
}
}
const preRemoteSha = pre.remoteRefs[remoteKey];
const nowSha = nowRefs[remoteKey];
if (nowSha !== undefined && preRemoteSha === undefined) {
// Register created this remote branch — delete it, lease-anchored on
// the just-observed sha (O2).
await tryDeleteRemoteRef(remoteKey, nowSha);
} else if (nowSha !== undefined && preRemoteSha !== undefined && nowSha !== preRemoteSha) {
// Register moved a pre-existing remote branch — restore its captured
// sha, lease-asserted on the sha observed moments ago (a concurrent
// foreign push fails the lease instead of being clobbered).
const rew = await git([
"push",
`--force-with-lease=${remoteKey}:${nowSha}`,
"origin",
`${preRemoteSha}:${remoteKey}`,
]);
if (rew.exitCode !== 0) {
diagnostics.push(
`rollback: remote rewind of ${remoteKey} to ${preRemoteSha.slice(0, 12)} failed (non-fatal): ${rew.stderr.slice(0, 160)}`,
);
}
} else if (nowSha === undefined && preRemoteSha !== undefined) {
diagnostics.push(
`rollback: remote branch ${remoteKey} vanished externally during register — left as-is`,
);
}
}
}
// Delete only refs register created — pre-register refs are never touched,
// whether HEAD was unborn or rev-parse was merely confused at capture.
const current = await git(["for-each-ref", "--format=%(refname)", "refs/heads"]);
if (current.exitCode === 0) {
const preRefs = new Set(pre.refs);
const created = current.stdout
.split("\n")
.map((s) => s.trim())
.filter((r) => r !== "" && !preRefs.has(r));
for (const ref of created) {
const del = await git(["update-ref", "-d", ref]);
if (del.exitCode !== 0) {
diagnostics.push(
`rollback: failed to delete register-created ${ref}: ${del.stderr.slice(0, 160)}`,
);
continue;
}
// Remote counterpart: delete only when the pre-snapshot proves the
// remote ref did not exist before register — a same-named remote
// branch predating register is not ours to remove (N9). The delete
// consults the just-read remote state: absent means nothing to remove,
// unverifiable means leave untouched (O2).
if (pre.remoteRefs === null) {
diagnostics.push(
`rollback: remote refs uncaptured — remote ${ref} (if any) left untouched`,
);
continue;
}
if (pre.remoteRefs[ref] !== undefined) continue;
if (nowRefs === null) {
diagnostics.push(
`rollback: remote state unverifiable — remote ${ref} (if any) left untouched`,
);
continue;
}
const createdRemoteSha = nowRefs[ref];
if (createdRemoteSha === undefined) continue;
await tryDeleteRemoteRef(ref, createdRemoteSha);
}
}
if (!pre.sha) {
// Unborn-HEAD path: drop the staged index so file-level rollback output
// stays plain untracked deletions — read-tree --empty has no HEAD
// dependency, unlike git rm --cached.
await git(["read-tree", "--empty"]);
}
} catch {
diagnostics.push(
"rollback: git history rewind aborted mid-way — file-level rollback already ran",
);
}
}
export async function runSternsystemRegister(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot, logger } = context;
const id = flagString(input, "id");
const cosmicStar = flagString(input, "cosmicStar");
const mirrorsFlag = flagString(input, "mirrors");
const platform = flagString(input, "platform");
const owner = flagString(input, "owner");
const isAmend = flagBool(input, "amend");
const amendId = flagNumber(input, "amend-id");
const intakeDir = flagString(input, "intake");
const biomeId = flagString(input, "biome");
const domainFlag = flagString(input, "domain");
if (!id) throw new Error("[sternsystem.register] requires --id ");
// RFC-0902: Reject TLD-suffixed IDs before any file operations.
if (hasTldSuffix(id)) {
throw new Error(
`[sternsystem.register] id '${id}' ends in a TLD suffix — use the business ID without domain TLD (e.g. 'warpgogol' not 'warpgogol-com')`,
);
}
// RFC-1125: identity + signing-key preflight — register either produces a signed
// passport or blocks. An unsigned passport is worse than no registration.
// Gates both register and --amend paths (amend missions close into signed bordbuch
// commits + passport refresh, which need the same identity material).
const identityExists = await context.io.exists(identityConfigPath(workspaceRoot));
const signingKeyConfigured = Boolean(
process.env.SIGNING_PRIVATE_KEY || process.env.SIGNING_PRIVATE_KEY_PATH,
);
if (!identityExists || !signingKeyConfigured) {
const missing = [
!identityExists ? "werkstatt.identity.json" : null,
!signingKeyConfigured ? "signing key (SIGNING_PRIVATE_KEY/SIGNING_PRIVATE_KEY_PATH)" : null,
]
.filter(Boolean)
.join(" and ");
throw new Error(
`[sternsystem.register] Missing ${missing}. Run identity.bootstrap first — it creates werkstatt.identity.json and prints the private key for SIGNING_PRIVATE_KEY.`,
);
}
const diagnostics: string[] = [];
if (isAmend && (intakeDir !== undefined || biomeId !== undefined || domainFlag !== undefined)) {
throw new Error(
"[sternsystem.register] --intake/--biome/--domain are for new registrations; --amend reads onboarding//.input/amend-/",
);
}
if (isAmend) {
const _config = await readSystemConfig(workspaceRoot, id, context.io);
if (owner) {
// owner field not in systemConfigSchema — would need schema extension
// For now, log warning that owner update is not supported in convention-based config
logger.info(`[sternsystem.register] --amend: owner update not yet supported for '${id}'`);
}
const pinInput: Record = { id };
if (platform) pinInput.platform = platform;
const pinResult = await runSternsystemPin(makeInput(pinInput), context);
const pinPath = pinResult.data!.pinPath;
const brief = amendId ? `Amend ${amendId} for ${id}` : `Amend for ${id}`;
const missionResult = await runMissionOpen(makeInput({ system: id, brief }), context);
const missionId = missionResult.data!.missionId;
return {
data: {
command: "sternsystem.register",
system: id,
status: "pass",
pinPath,
firstMissionId: missionId,
diagnostics,
},
exitCode: 0,
summary: `[sternsystem.register] amended '${id}' — mission ${missionId} opened and materialized`,
nextSteps: [
{
action: `Validate the system: pnpm exec werkstatt run sternsystem.validate --id ${id}`,
kind: "optional",
},
],
};
}
if (!cosmicStar) throw new Error("[sternsystem.register] requires --cosmicStar ");
if (!mirrorsFlag)
throw new Error(
"[sternsystem.register] requires --mirrors (first is cache, second is bare, rest are external)",
);
if (!(StarCatalog as readonly string[]).includes(cosmicStar)) {
throw new Error(`[sternsystem.register] cosmicStar '${cosmicStar}' is not in StarCatalog`);
}
if (await hasAppsCollision(workspaceRoot, id, context.io)) {
throw new Error(
`[sternsystem.register] id '${id}' matches existing apps/${id}/ — extract first`,
);
}
// Check for existing system via discovery
const { systems: existingSystems } = await discoverSystems(workspaceRoot, context.io);
if (existingSystems.some((s) => s.id === id)) {
throw new Error(`[sternsystem.register] id '${id}' already exists in ../systems-cache/`);
}
const starOwner = existingSystems.find(
(s) => s.cosmicStar === cosmicStar && s.status !== "archived",
);
if (starOwner) {
throw new Error(
`[sternsystem.register] cosmicStar '${cosmicStar}' is already used by '${starOwner.id}' (status: ${starOwner.status})`,
);
}
const pinnedPlatform = platform ?? existingSystems[0]?.pinnedPlatform ?? "0.0.0";
type ValidStorageType = "non-bare" | "bare" | "bundle";
const mirrors = mirrorsFlag.split(",").map((entry) => {
const m = entry.match(/^(.+):(non-bare|bare|bundle)$/);
if (m) {
return { path: m[1], storageType: m[2] as ValidStorageType };
}
return { path: entry, storageType: "non-bare" as const };
});
// mirrors[0] is always the conventional non-bare cache clone — seeded files
// and registry config/state write into the same directory (AGENTS.md).
if (
mirrors[0].storageType !== "non-bare" ||
resolveMirrorPath(workspaceRoot, mirrors[0].path) !== resolveCacheClonePath(workspaceRoot, id)
) {
throw new Error(
`[sternsystem.register] mirrors[0] must be the conventional non-bare cache clone '../systems-cache/${id}' — got '${mirrors[0].path}'`,
);
}
const newConfig = {
schemaVersion: "1.0.0" as const,
id,
cosmicStar: cosmicStar as StarName,
mirrors,
pinnedPlatform,
status: "registered" as const,
registeredAt: new Date().toISOString(),
notes: "",
};
// RFC-1307: the intake bundle is the only input for a new Sternsystem (I3).
if (!intakeDir) {
throw new Error(
"[sternsystem.register] requires --intake — run onboarding.intake.validate --id on onboarding//.input/intake first",
);
}
if (!biomeId) {
throw new Error(
"[sternsystem.register] requires --biome — run family.detect --id and biome.from-intake --id --family first",
);
}
// RFC-1307: every intake precondition runs before the first write.
const seedPlan = await prepareIntakeSeed(
{ id, cosmicStar, intakeDir, biomeId, ...(domainFlag ? { domain: domainFlag } : {}) },
context,
);
const systemDir = resolveMirrorPath(workspaceRoot, mirrors[0].path);
// A pre-existing clone dir is NEVER removed by rollback — only a dir
// register itself created is (REVIEW-CODE-2026-10-16-09 N8).
const systemDirExisted = await context.io.exists(systemDir);
await context.io.mkdir(systemDir);
const repoInitialized = await ensureCacheCloneRepo(systemDir, mirrors, workspaceRoot, context.io);
// Capture the clone's git state before the first write so a failed
// registration can distinguish register's own files/commits from anything
// that was already there (REVIEW-CODE-2026-10-16-08 N1/N2).
const preGit = await capturePreRegisterGit(systemDir, context.io);
// Uncommitted tracked or staged work would be destroyed by the rollback's
// reset --hard — refuse before the first write rather than risk it
// (REVIEW-CODE-2026-10-16-09 N12). The just-created dir is git-less, so a
// fresh clone can never trip this.
if (preGit.dirty) {
throw new Error(
`[sternsystem.register] cache clone has uncommitted changes — commit or clean ${systemDir} before registering`,
);
}
const contentDirExisted = await context.io.exists(join(systemDir, "src", "content"));
// Byte capture for files register overwrites — a pre-existing file that was
// never committed cannot be restored by the git rewind alone (N2).
const readIfExists = async (filePath: string): Promise =>
(await context.io.exists(filePath)) ? await context.io.readFile(filePath) : null;
const preConfigBytes = await readIfExists(join(systemDir, "system-config.yaml"));
const preStateBytes = await readIfExists(join(systemDir, "system-state.yaml"));
const prePinBytes = await readIfExists(join(systemDir, "system.pin.json"));
let pinCreated = false;
let contentCreated = false;
let configCreated = false;
let stateCreated = false;
let missionOpened = false;
let missionId: string | undefined;
// Paths register actually wrote — rollback removes ONLY these, never a
// pre-existing file that happens to share a planned path (REVIEW-09 N12).
const seededPaths: string[] = [];
// Untracked operator config defaults register created — removed on rollback.
const operatorConfigCreated: string[] = [];
try {
// Inside the guarded scope: a config-write failure must not orphan the
// clone directory register just created (REVIEW-CODE-2026-10-16-08 N7).
// Flags mark "write begun", not "write succeeded" — a mid-write throw
// leaves partial output that rollback must still remove.
configCreated = true;
await writeSystemConfig(
workspaceRoot,
id,
seedPlan.provisionalDeployment === undefined
? newConfig
: { ...newConfig, deployment: seedPlan.provisionalDeployment },
context.io,
);
const pinInput: Record = { id };
if (platform) pinInput.platform = platform;
pinCreated = true;
const pinResult = await runSternsystemPin(makeInput(pinInput), context);
const pinPath = pinResult.data!.pinPath;
contentCreated = true;
await writeSeedFiles(systemDir, seedPlan, context.io, seededPaths);
operatorConfigCreated.push(...(await seedOperatorConfigDefaults(systemDir, context.io)));
// readSystemState may throw on a malformed PRE-EXISTING state file — that
// file must survive rollback untouched, so the flag arms only once
// register itself is about to write.
const state = await readSystemState(workspaceRoot, id, context.io);
stateCreated = true;
await writeSystemState(
workspaceRoot,
id,
{ ...state, onboarding: seedPlan.systemState.onboarding },
context.io,
(msg) => logger.warn(` [sternsystem.register] ${msg}`),
);
// Commit config, pin, and content to cache clone before mission.open
// so syncCacheClone's git reset --hard doesn't wipe uncommitted files.
// Add ONLY register's own paths — `git add -A` would sweep pre-existing
// untracked files into register's commit, and the rollback's reset --hard
// would then destroy them (REVIEW-CODE-2026-10-16-08 N2).
if (await context.io.exists(join(systemDir, ".git"))) {
try {
const seedPaths = [
"system-config.yaml",
"system-state.yaml",
"system.pin.json",
...seedPlan.files.map((f) => f.path),
// Operator config defaults register created ride the scaffold commit,
// like every later mission that edits them (reconcile tracks them).
...[...OPERATOR_CONFIG_FILES, ...Object.keys(SITE_BIRTH_DEFAULTS)].filter((rel) =>
operatorConfigCreated.includes(join(systemDir, ...rel.split("/"))),
),
];
const existingSeedPaths: string[] = [];
for (const p of seedPaths) {
if (await context.io.exists(join(systemDir, ...p.split("/")))) {
existingSeedPaths.push(p);
}
}
const addResult = await context.io.exec("git", ["add", "--", ...existingSeedPaths], {
cwd: systemDir,
timeoutMs: 10000,
});
if (addResult.exitCode !== 0) {
throw new Error(
`git add exited ${addResult.exitCode}: ${addResult.stderr.slice(0, 200)}`,
);
}
await cacheCloneCommit(
systemDir,
`sternsystem.register: scaffold ${id}`,
undefined,
context.io,
);
// Push to the bare repo's default branch so syncCacheClone can fetch —
// same branch writeSystemState and the rollback rewind use.
try {
const pushResult = await context.io.exec(
"git",
["push", "origin", `HEAD:refs/heads/${preGit.branch}`],
{
cwd: systemDir,
timeoutMs: 10000,
},
);
if (pushResult.exitCode !== 0) {
throw new Error(
`git push exited ${pushResult.exitCode}: ${pushResult.stderr.slice(0, 200)}`,
);
}
} catch (pushErr) {
logger.warn(
` [sternsystem.register] scaffold push to origin/${preGit.branch} failed (non-fatal): ${pushErr instanceof Error ? pushErr.message : String(pushErr)}`,
);
}
} catch {
// commit failure non-fatal (nothing to commit)
}
}
const missionResult = await registerDeps.openMission(
makeInput({ system: id, brief: `Initial onboarding for ${id}` }),
context,
);
missionId = missionResult.data!.missionId;
missionOpened = true;
// RFC-1326: provision external resources the wrangler bindings need —
// cloudflare.token.check names a missing permission, then the Vectorize
// index is created while mission m000001 is still open. Failures roll back
// the birth (mission abort + file rewind) instead of surfacing at deploy.
const provisionResult = await registerDeps.provisionExternal(context, {
systemId: id,
missionOpen: true,
});
if (!provisionResult.ok) {
throw new Error(`external resource provisioning failed: ${provisionResult.error}`);
}
logger.info(
` [sternsystem.register] external resources provisioned (${provisionResult.provisioned.join(", ")})`,
);
// RFC-0951: mission.open now auto-materializes the workpiece internally.
// No explicit runMissionMaterialize call needed here.
// RFC-0574: mirror hook removed — star topology uses explicit sternsystem.sync
void workspaceRoot;
// RFC-0966: Generate initial passport at site birth. The RFC-1125 preflight
// guarantees a signing key is configured — generation failures are still
// non-fatal (e.g. malformed key material surfaces downstream).
try {
const { runSternsystemPassportGenerate } = await import("./sternsystem-passport-generate.ts");
await runSternsystemPassportGenerate(makeInput({ id, actor: "human:operator" }), context);
logger.info(` [sternsystem.register] Initial passport generated for ${id}`);
} catch (passportErr) {
logger.warn(
` [sternsystem.register] Passport generation failed (non-fatal): ${passportErr instanceof Error ? passportErr.message : String(passportErr)}`,
);
}
// RFC-1124: publish initial fleet claim at site birth (non-fatal — skips
// silently without werkstatt.fleet.json or a signing key).
try {
const { loadFleetClaimsConfig, loadSigningKeyFromEnv, publishClaim } =
await import("../fleet/claims-repo.ts");
const signingKey = await loadSigningKeyFromEnv();
if (signingKey && (await loadFleetClaimsConfig(workspaceRoot))) {
await publishClaim({
systemId: id,
werkstattRoot: workspaceRoot,
signerPrivateKeyHex: signingKey.privateKeyHex,
signerPublicKey: signingKey.publicKeyHex,
});
logger.info(` [sternsystem.register] Initial fleet claim published for ${id}`);
}
} catch (claimErr) {
logger.warn(
` [sternsystem.register] fleet claim publish failed (non-fatal): ${claimErr instanceof Error ? claimErr.message : String(claimErr)}`,
);
}
logger.success(
`[sternsystem.register] registered '${id}' (cosmicStar: ${cosmicStar}) — mission ${missionId} opened and materialized`,
);
return {
data: {
command: "sternsystem.register",
system: id,
status: "pass",
registryEntry: {
id,
cosmicStar,
status: "active",
registeredAt: newConfig.registeredAt,
},
pinPath,
firstMissionId: missionId,
seed: {
intakeId: seedPlan.systemState.onboarding.intakeId,
pbpDocuments: seedPlan.pbpDocuments,
assets: seedPlan.assets,
},
diagnostics,
},
exitCode: 0,
summary: `[sternsystem.register] ${id} registered with cosmicStar ${cosmicStar} — mission ${missionId} materialized`,
nextSteps: [
{
action: `Validate the system: pnpm exec werkstatt run sternsystem.validate --id ${id}`,
kind: "optional",
},
],
};
} catch (error) {
logger.info(
`[sternsystem.register] rolling back due to error: ${error instanceof Error ? error.message : String(error)}`,
);
if (missionOpened && missionId) {
try {
await runMissionAbort(
makeInput({ mission: missionId, reason: "sternsystem.register rollback" }),
context,
);
} catch (abortError) {
diagnostics.push(
`mission.abort during rollback failed: ${abortError instanceof Error ? abortError.message : String(abortError)}`,
);
}
}
// File-level rollback first, git rewind second — the rewind restores
// tracked files to the pre-register state, so removing register's files
// up front guarantees pre-existing content survives (REVIEW-08 N2).
if (contentCreated) {
await rollbackSeedFiles(systemDir, seededPaths, context.io);
for (const created of operatorConfigCreated) {
if (await context.io.exists(created)) await context.io.rm(created);
}
// register created src/content entirely — the dir goes away; if it
// pre-existed, only register's files were removed.
if (!contentDirExisted) {
await rollbackContent(systemDir, context.io);
}
}
if (pinCreated) {
await rollbackPin(systemDir, context.io);
}
if (configCreated) {
await rollbackConfig(workspaceRoot, id, context.io);
}
if (stateCreated) {
await rollbackSystemState(workspaceRoot, id, context.io);
}
await rollbackGitHistory(systemDir, preGit, context.io, diagnostics);
// A pre-existing file register overwrote survives the rewind only if it was
// tracked — for the untracked edge, restore the captured bytes (N2).
const restorePreExisting = async (filePath: string, bytes: string | null) => {
if (bytes === null) return;
const exists = await context.io.exists(filePath);
if (!exists || (await context.io.readFile(filePath)) !== bytes) {
await context.io.writeFile(filePath, bytes);
}
};
try {
await restorePreExisting(join(systemDir, "system-config.yaml"), preConfigBytes);
await restorePreExisting(join(systemDir, "system-state.yaml"), preStateBytes);
await restorePreExisting(join(systemDir, "system.pin.json"), prePinBytes);
} catch (restoreErr) {
diagnostics.push(
`rollback: pre-existing file restore failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`,
);
}
// A repository register initialised itself is register's own output —
// remove it so a re-run starts from the same git-less state.
if (repoInitialized) {
try {
await context.io.rm(join(systemDir, ".git"), { recursive: true });
} catch (rmErr) {
diagnostics.push(
`rollback: register-initialised .git could not be removed: ${rmErr instanceof Error ? rmErr.message : String(rmErr)}`,
);
}
}
// Only a dir register itself created is removed — and only when rollback
// left it empty; a pre-existing dir is never touched (REVIEW-09 N8).
if (!systemDirExisted) {
const outcome = await rollbackSystemDirIfEmpty(systemDir, context.io);
if (outcome === "kept-git") {
diagnostics.push("rollback: system dir left in place — .git materialized during rollback");
} else if (outcome === "kept-nonempty") {
diagnostics.push(
"rollback: system dir left in place — not empty after file-level rollback",
);
} else if (outcome === "kept-error") {
diagnostics.push("rollback: system dir left in place — could not inspect or remove it");
}
}
diagnostics.push(
`sternsystem.register failed: ${error instanceof Error ? error.message : String(error)}`,
);
return {
...failResult("sternsystem.register", [
`[sternsystem.register] ${id} failed and rolled back: ${error instanceof Error ? error.message : String(error)}`,
]),
data: {
command: "sternsystem.register",
system: id,
status: "fail",
diagnostics,
},
};
}
}