The shared harness
The small harness the journey and every recipe are written against: a host wrapper over the SDK’s public surface, the typed accessors that read a notification’s untyped params safely, and the segment runner that turns a sequence of steps into one pass-or-fail report.
Read this once and the recipes below read as ordinary programs. Nothing here is required to use the SDK — it is the scaffolding these examples share, and your own application would have its own.
The code below is the source that runs, not a retelling of it. It is reproduced unchanged except that comments citing the repository it is developed in — issue numbers, internal file paths, specification section numbers — are removed, because they resolve nothing for you. No executable line differs.
host.ts
Section titled “host.ts”/** * One owned `muse serve` host, plus the notification recorder the streaming * assertions wait on. * * Everything here goes through the shipped `@muse-code/sdk` public surface — * `spawnMspConnection`, `MspHandshake.initialize`, `Connection.command`, * `Connection.request`, `SpawnedMspConnection.close`. The journey never * reaches into an SDK internal, so what it proves is exactly what a consumer * gets. */
import { spawnMspConnection } from "@muse-code/sdk";import type { SpawnedMspConnection } from "@muse-code/sdk";
/** A JSON-RPC notification the host pushed at us. */export interface RecordedNotification { readonly method: string; readonly params: Record<string, unknown>;}
export interface HostOptions { /** Absolute path to the release-built binary. */ readonly museBin: string; /** Isolated `HOME`. Every host in one journey shares it, so state persists. */ readonly home: string; /** The workspace a session is started in. */ readonly workspaceRoot: string; /** * The clientInfo the handshake announces to the host's session/audit * attribution. REQUIRED, matching {@link HostSpawnSpec}: this used to * default to the quickstart's identity, so a new release-host recipe that * forgot it announced itself as the quickstart and its sessions were * attributed to a journey it never ran (T-24225-6). Every caller names * itself. */ readonly clientInfo: { readonly name: string; readonly version: string };}
/** * The generic spawn shape behind {@link Host.start}. Cookbook recipes use it * to launch `muse-conformance serve-fixture` (the canned stdio host) with the * same recorder and bounded waits the quickstart journey uses for `muse serve`. */export interface HostSpawnSpec { /** The host executable. */ readonly command: string; readonly args: readonly string[]; readonly cwd?: string; /** The COMPLETE child environment: nothing else is inherited. */ readonly env: Record<string, string>; /** The clientInfo the MSP handshake announces. */ readonly clientInfo: { readonly name: string; readonly version: string };}
/** * This is how the JOURNEY's own child is launched; it is not reader guidance, * and README.md says so. */export const SDK_GATE_ENV = "MUSE_EXPERIMENTAL_SDK_ENABLED";
/** The pre-Seam-C placeholder cursor. A real session must never return it. */export const STUB_VIEW_CURSOR = "pending:seam-c-session-view-fold";
/** * The isolated child environment every journey-spawned host gets — the * env_clear equivalent: nothing else is inherited, so a journey can never * read the developer's credentials, telemetry settings, or real muse state. * ONE builder, shared by `Host.start` and the release-host recipes that spawn * through the SDK facade, so a new isolation variable lands everywhere at * once. */export function isolatedHostEnv(home: string): Record<string, string> { return { HOME: home, PATH: process.env["PATH"] ?? "/usr/bin:/bin", TBH_CREDENTIAL_BACKEND: "file", TBH_DISABLE_TELEMETRY: "1", [SDK_GATE_ENV]: "on", };}
export class TimeoutError extends Error { constructor(what: string, budgetMs: number) { super(`${what} did not happen within ${String(budgetMs)}ms`); this.name = "TimeoutError"; }}
/** A bounded wait. Never leaves a dangling timer behind. */export async function within<T>( what: string, budgetMs: number, work: Promise<T>,): Promise<T> { let timer: NodeJS.Timeout | undefined; try { return await Promise.race([ work, new Promise<never>((_resolve, reject) => { timer = setTimeout(() => { reject(new TimeoutError(what, budgetMs)); }, budgetMs); }), ]); } finally { if (timer !== undefined) clearTimeout(timer); }}
export class Host { readonly msp: SpawnedMspConnection; readonly stderr: string[]; readonly #seen: RecordedNotification[] = []; readonly #waiters: Array<{ match: (notification: RecordedNotification) => boolean; settle: (notification: RecordedNotification) => void; }> = [];
// The SDK keeps appending to the array `start` handed the spawn, so this // holds the REFERENCE, never a copy: a snapshot taken at handshake time is // empty exactly when the host later dies and its stderr is the only evidence. private constructor(msp: SpawnedMspConnection, stderr: string[]) { this.msp = msp; this.stderr = stderr; }
/** Spawn the release binary and complete the MSP handshake. */ static async start(options: HostOptions, budgetMs: number): Promise<Host> { return await Host.spawn( { command: options.museBin, args: ["serve"], cwd: options.workspaceRoot, env: isolatedHostEnv(options.home), clientInfo: options.clientInfo, }, budgetMs, ); }
/** Spawn any MSP host command and complete the handshake. */ static async spawn(spec: HostSpawnSpec, budgetMs: number): Promise<Host> { const stderr: string[] = []; const handshake = spawnMspConnection({ command: spec.command, args: [...spec.args], ...(spec.cwd === undefined ? {} : { cwd: spec.cwd }), env: spec.env, onStderr: (chunk) => stderr.push(chunk), });
let msp: SpawnedMspConnection; try { msp = await within( "the MSP handshake", budgetMs, handshake.initialize({ clientInfo: spec.clientInfo }), ); } catch (error) { // BOUND this peek. `child.exit` only settles when the child actually // exits, so a host that accepted the spawn and then went silent would // swallow the TimeoutError above and hang the caller forever — past the // journey's own cleanup, leaking the child. A missing classification is // worth far less than a diagnosable failure. const exit = await within("the host's exit after a failed handshake", 2_000, handshake.child.exit) .catch(() => undefined); throw new Error( `handshake failed (exit ${exit === undefined ? "still running" : JSON.stringify(exit)}); stderr: ${stderr.join("")}`, { cause: error }, ); }
const host = new Host(msp, stderr); msp.connection.onNotification((notification) => { host.#record({ method: notification.method, params: (notification.params ?? {}) as Record<string, unknown>, }); }); msp.child.exit.catch(() => undefined); return host; }
#record(notification: RecordedNotification): void { this.#seen.push(notification); for (let index = this.#waiters.length - 1; index >= 0; index -= 1) { const waiter = this.#waiters[index]; if (waiter !== undefined && waiter.match(notification)) { this.#waiters.splice(index, 1); waiter.settle(notification); } } }
/** Every notification recorded so far, oldest first. */ notifications(): readonly RecordedNotification[] { return [...this.#seen]; }
/** * Wait for the first notification matching `match`. Already-received * notifications count, so there is no race between sending a command and * subscribing to its stream. */ async waitFor( what: string, budgetMs: number, match: (notification: RecordedNotification) => boolean, ): Promise<RecordedNotification> { const already = this.#seen.find(match); if (already !== undefined) return already; const arrival = new Promise<RecordedNotification>((resolve) => { this.#waiters.push({ match, settle: resolve }); }); // Racing the process exit turns "the host died" into that sentence // instead of an opaque timeout. const died = this.msp.exited.then((exit) => { throw new Error( `the host exited (code ${String(exit.code)}, signal ${String(exit.signal)}) while waiting for ${what}; stderr: ${this.stderr.join("")}`, ); }); died.catch(() => undefined); return await within(what, budgetMs, Promise.race([arrival, died])); }
/** Close stdin and wait for the orderly drain. */ async close(budgetMs: number): Promise<{ code: number | null; signal: string | null }> { return await within("the host's orderly drain and exit", budgetMs, this.msp.close()); }
/** * Best-effort teardown for a failure path. Never throws. */ async abandon(budgetMs: number): Promise<void> { try { await this.close(budgetMs); } catch (error) { process.stderr.write( `warning: the host did not drain (${String(error)}). The SDK exposes no kill path, ` + `so this child may outlive the journey.\n`, ); } }}
/** * The owned-host guard every journey needs: a segment that runs after an * earlier one failed to produce a host says so, instead of dereferencing * `undefined`. Structural on purpose — each journey has its own context shape * and only the `host` slot is shared. */export function requireHost(context: { readonly host?: Host }): Host { if (context.host === undefined) throw new Error("no host: an earlier segment did not finish"); return context.host;}
/** Narrow an untyped MSP result member to an object, with a useful message. */export function objectAt( value: Record<string, unknown>, key: string, where: string,): Record<string, unknown> { const member = value[key]; if (member === null || typeof member !== "object" || Array.isArray(member)) { throw new Error(`${where}: "${key}" is not an object (got ${JSON.stringify(member)})`); } return member as Record<string, unknown>;}
/** Narrow an untyped MSP result member to a non-empty string. */export function stringAt( value: Record<string, unknown>, key: string, where: string,): string { const member = value[key]; if (typeof member !== "string" || member.length === 0) { throw new Error(`${where}: "${key}" is not a non-empty string (got ${JSON.stringify(member)})`); } return member;}
export function arrayAt( value: Record<string, unknown>, key: string, where: string,): readonly unknown[] { const member = value[key]; if (!Array.isArray(member)) { throw new Error(`${where}: "${key}" is not an array (got ${JSON.stringify(member)})`); } return member;}
export function equals(actual: unknown, expected: unknown, what: string): void { if (actual !== expected) { throw new Error(`${what}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); }}index.ts
Section titled “index.ts”/** * The shared journey kit, importable as `@muse-code/sdk-cookbook/kit` — the * package's ONLY export. There is deliberately no root export: nothing * consumes one (recipes and tests use relative paths, CI runs * `dist/src/main.js` by path), and a root barrel would both drift as a second * hand-maintained export list and make every recipe module a load-time * dependency of the quickstart journey. The kit is just the expect-block * contract and the owned-host recorder. */
export { classify, formatReport, runJourney, runSegment, summarize,} from "./segments.js";export type { ExpectBlock, JourneyReport, Segment, SegmentOutcome, SegmentResult,} from "./segments.js";
export { Host, SDK_GATE_ENV, STUB_VIEW_CURSOR, TimeoutError, arrayAt, equals, isolatedHostEnv, objectAt, requireHost, stringAt, within,} from "./host.js";export type { HostOptions, HostSpawnSpec, RecordedNotification } from "./host.js";segments.ts
Section titled “segments.ts”/** * Segment results and the expect-block contract. * * The journey runs every segment for real. A segment that today cannot pass * because of a named open issue carries an `expectBlock`. The block never * weakens the assertion and never stubs the segment out: the segment still * runs, still asserts the spec-correct behavior, and its real failure text is * kept in the report. * * The contract has two directions, and the second one is the point: * * - assertion fails with the SIGNATURE the named issues cause -> * `expectBlocked`. The journey stays green and prints the issue numbers. * - assertion fails with anything else -> `failed`. A block excuses exactly * the defect it names and nothing else, so an unrelated regression in a * blocked segment still reds the journey. * - assertion PASSES while an expect-block is declared -> `unblocked`. * The journey FAILS. The fix landed, so the segment must be promoted to * required by deleting its `expectBlock`. A stale block cannot rot * silently, and the promotion happens the first time CI runs after the * fix — that is the automatic light-up. */
/** What actually happened when a segment ran. */export type SegmentOutcome = /** Ran and asserted clean. Required for the journey to be green. */ | "passed" /** Failed exactly as its named open issues predict. Not fatal. */ | "expectBlocked" /** Passed while expect-blocked: the block is stale. FATAL. */ | "unblocked" /** Failed with no expect-block, or in a way no block predicted. FATAL. */ | "failed";
/** Why a segment cannot pass yet, in the issue tracker's own numbers. */export interface ExpectBlock { /** Open issue numbers. Empty is never valid. */ readonly issues: readonly number[]; /** One plain sentence: what those issues do to this segment. */ readonly because: string; /** * The failure text those issues produce. The block excuses ONLY a failure * matching this; anything else is a real failure. Without it a block would * quietly absorb every future regression in its segment. */ readonly signature: RegExp;}
/** One named step of the journey. */export interface Segment<Context> { readonly id: string; /** Plain-words title, used verbatim in the printed report. */ readonly title: string; /** Present only while a named open issue stops this segment passing. */ readonly expectBlock?: ExpectBlock; /** Runs the real work and throws on any failed assertion. */ run(context: Context): Promise<void>;}
export interface SegmentResult { readonly id: string; readonly title: string; readonly outcome: SegmentOutcome; readonly durationMs: number; readonly expectBlock: ExpectBlock | undefined; /** The real failure text, kept whether or not the failure was expected. */ readonly failure: string | undefined;}
export interface JourneyReport { readonly segments: readonly SegmentResult[]; /** True only when no segment is `failed` or `unblocked`. */ readonly ok: boolean;}
function describe(error: unknown): string { if (error instanceof Error) { const cause = error.cause === undefined ? "" : `\n caused by: ${String(error.cause)}`; return `${error.message}${cause}`; } return String(error);}
export function classify( expectBlock: ExpectBlock | undefined, failure: string | undefined,): SegmentOutcome { if (expectBlock === undefined) return failure === undefined ? "passed" : "failed"; if (failure === undefined) return "unblocked"; return expectBlock.signature.test(failure) ? "expectBlocked" : "failed";}
/** * Runs one segment and classifies it. `now` is injected so the deterministic * unit tests can drive duration without a clock. */export async function runSegment<Context>( segment: Segment<Context>, context: Context, now: () => number = () => performance.now(),): Promise<SegmentResult> { const started = now(); let failure: string | undefined; try { await segment.run(context); } catch (error) { failure = describe(error); } return { id: segment.id, title: segment.title, outcome: classify(segment.expectBlock, failure), durationMs: Math.round(now() - started), expectBlock: segment.expectBlock, failure, };}
/** * Run a journey's segments in order, ALWAYS run `teardown`, then summarize. * * This is the loop every recipe would otherwise hand-copy, and the copy's * risky part is the `finally`: the SDK has no kill path, so a recipe that * forgets it leaks a spawned host child on the first failing segment. * Teardown is a callback because the kit does not know what a journey's * context owns. */export async function runJourney<Context>( segments: ReadonlyArray<Segment<Context>>, context: Context, teardown: (context: Context) => Promise<void>,): Promise<JourneyReport> { const results: SegmentResult[] = []; try { for (const segment of segments) { results.push(await runSegment(segment, context)); } } finally { await teardown(context); } return summarize(results);}
export function summarize(segments: readonly SegmentResult[]): JourneyReport { return { segments, ok: !segments.some( (segment) => segment.outcome === "failed" || segment.outcome === "unblocked", ), };}
const MARK: Record<SegmentOutcome, string> = { passed: "PASS ", expectBlocked: "BLOCK", unblocked: "STALE", failed: "FAIL ",};
function issueList(block: ExpectBlock): string { return block.issues.map((issue) => `#${String(issue)}`).join(", ");}
/** The human-facing report. Every expect-block states its issue numbers. */export function formatReport(report: JourneyReport): string { const lines: string[] = []; for (const segment of report.segments) { lines.push( `${MARK[segment.outcome]} ${segment.id.padEnd(26)} ${String(segment.durationMs).padStart(6)}ms ${segment.title}`, ); const block = segment.expectBlock; if (block !== undefined && segment.outcome === "expectBlocked") { lines.push(` expect-blocked on ${issueList(block)}: ${block.because}`); lines.push(` observed: ${indent(segment.failure ?? "(no detail)")}`); } if (block !== undefined && segment.outcome === "unblocked") { lines.push( ` EXPECT-BLOCK IS STALE. This segment now passes, so ${issueList(block)} looks fixed.`, ); lines.push( ` Promote it: delete this segment's expectBlock so the journey requires it from now on.`, ); } if (segment.outcome === "failed") { if (block !== undefined) { lines.push( ` This failure does NOT match the expect-block signature for ${issueList(block)}` + ` (${String(block.signature)}), so it is a real failure.`, ); } lines.push(` ${indent(segment.failure ?? "(no detail)")}`); } } const counts = new Map<SegmentOutcome, number>(); for (const segment of report.segments) { counts.set(segment.outcome, (counts.get(segment.outcome) ?? 0) + 1); } const tally = (["passed", "expectBlocked", "unblocked", "failed"] as const) .map((outcome) => `${outcome}=${String(counts.get(outcome) ?? 0)}`) .join(" "); lines.push(`journey ${report.ok ? "OK" : "NOT OK"} (${tally})`); return lines.join("\n");}
function indent(text: string): string { return text.split("\n").join("\n ");}