The quickstart journey
The complete first-session program the Quickstart guide walks through: spawn a host, complete the handshake, start a session, run a turn and read it as it streams, answer an approval, cancel a turn, close the host, and reload the session in a new process.
Every step of this program runs against a release-built host on every change to the SDK, the harness or these docs. A step that stops behaving as the protocol specifies fails that run rather than being quietly documented as working.
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”/** * The owned-host spawn and notification recorder now live in the shared * cookbook kit so the quickstart journey and every cookbook recipe spawn and * observe hosts the same way. This module re-exports it unchanged: * `Host.start` still launches `muse serve` with the same environment, * budgets, and failure text. */
export { Host, SDK_GATE_ENV, STUB_VIEW_CURSOR, TimeoutError, arrayAt, equals, objectAt, stringAt, within,} from "@muse-code/sdk-cookbook/kit";export type { HostOptions, RecordedNotification } from "@muse-code/sdk-cookbook/kit";journey.ts
Section titled “journey.ts”/** * The `@muse-code/sdk` first-session journey, end to end, against a release-built * `muse serve`. * * * 1. **Every assertion states the spec-correct behavior.** Where today's host * is wrong, the segment carries an `expectBlock` naming the open issue. * Nothing here pins a known-wrong result as golden — a journey that froze * today's resume state would become the thing that blocks the fix. * 2. **An expect-block cannot rot.** It names the issues, and it names the * SIGNATURE of the failure those issues cause. A failure that does not * match the signature is a real failure, not an excused one. A segment * that starts passing while blocked FAILS the journey so it gets promoted. * * Rule 2 is what surfaced that — the journey reported all six blocks * `unblocked` the first time it ran against a host with a provider * configured. Rule 1 is why the promotion was a deletion and not a rewrite: * no assertion had been bent to match the blocked behavior, so there was * nothing to unbend. * * The journey has two modes. {@link runConfiguredJourney} is the ACCEPTANCE * mode: it seeds `JourneyOptions.home` with a HOME pointing at a loopback fake * first-party endpoint (`provider.ts`), so the host really runs a model and * every segment is exercised. {@link runJourney} with no seeded `home` is the * credential-free degradation path — still supported, never the acceptance * artifact. * * Read `README.md` next to this file for the same journey in plain words. */
import { mkdtemp } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";
import { EXPECTED_SCHEMA_FINGERPRINT, MspError } from "@muse-code/sdk";
import { Host, STUB_VIEW_CURSOR, arrayAt, equals, objectAt, stringAt,} from "./host.js";import type { RecordedNotification } from "./host.js";import { startConfiguredProvider } from "./provider.js";import { runJourney as runKitJourney } from "./segments.js";import type { JourneyReport, Segment } from "./segments.js";
/** * How this journey identifies itself to the host's session/audit attribution. * Named at both spawn sites rather than defaulted in the kit, so a cookbook * recipe can never inherit the quickstart's identity by omission (T-24225-6). */const QUICKSTART_CLIENT_INFO = { name: "muse_sdk_quickstart", version: "0.0.0" };
/** * The artifact the approval prompt asks for. The provider-configured mode * routes its ONE scripted `shell` tool call on this name plus the tool's own, * so the discriminator and the prompt can never drift apart. */const APPROVAL_ARTIFACT = "approved.txt";
/** The prompt every turn segment sends. Plain, deterministic, harmless. */const PROMPT = "Reply with the single word: hello";/** The single word {@link PROMPT} asks for, and what the fake provider replies. */const REPLY_TEXT = "hello";/** A prompt that must make the agent ask permission before touching the disk. */const APPROVAL_PROMPT = `Create a file called ${APPROVAL_ARTIFACT} containing the word yes`;
const HANDSHAKE_BUDGET_MS = 30_000;const COMMAND_BUDGET_MS = 30_000;const STREAM_BUDGET_MS = 60_000;const CLOSE_BUDGET_MS = 30_000;
export interface JourneyOptions { /** Absolute path to the release-built binary under test. */ readonly museBin: string; /** * Isolated HOME. Defaults to a fresh temp dir, which is the CREDENTIAL-FREE * degradation path: with no provider configured the host cannot run a model, * so the six model-dependent segments fail for real. The acceptance run uses * {@link runConfiguredJourney}, which seeds this with a HOME pointing at a * loopback fake first-party endpoint. */ readonly home?: string; /** Workspace root a session is started in. Defaults to a fresh temp dir. */ readonly workspaceRoot?: string;}
/** Everything the segments read and write as the journey proceeds. */interface Context { readonly museBin: string; readonly home: string; readonly workspaceRoot: string; /** The session started by `session-new` and resumed later. */ sessionId?: string; /** The live host segments 1-4 share; closed before the resume segments. */ host?: Host; /** The resume host, opened by `resume` and closed by `terminate`. */ resumeHost?: Host; /** The `session/resume` result the resume segments all read. */ resumed?: Record<string, unknown>;}
function agentText(notification: RecordedNotification): string | undefined { const item = notification.params["item"]; if (item === null || typeof item !== "object") return undefined; const record = item as Record<string, unknown>; if (record["kind"] !== "agentMessage") return undefined; return typeof record["text"] === "string" ? record["text"] : undefined;}
function requireHost(context: Context): Host { const host = context.host; if (host === undefined) throw new Error("no live host: an earlier segment did not finish"); return host;}
function requireSessionId(context: Context): string { const sessionId = context.sessionId; if (sessionId === undefined) throw new Error("no session: `session-new` did not finish"); return sessionId;}
/** * Runs one complete turn on the given host and returns the `turn/completed` * notification. Shared by the turn, approval and cancel segments so all three * observe the same streaming contract. */async function startTurn( host: Host, sessionId: string, prompt: string,): Promise<{ turnId: string }> { const ack = await host.msp.connection.command( "turn/start", { sessionId, input: [{ type: "text", text: prompt }] }, { maxAttempts: 1 }, ); equals(ack["status"], "accepted", "turn/start ack status"); // `started` and `queued` are BOTH correct answers, so pinning `started` was // a real-bug detector that also fired on legal behavior. When `turn/start` // lands after the previous turn completed but before the session settles to // idle, the host mints a queued turn instead of a fresh one — the ack then // says `queued` with `startedNewTurn: false`, and its `turnId` names the // queued stream, never the running turn. Either way the turn is admitted // and `turn/started` still fires for that id, so the journey's contract // holds. `steered` is deliberately NOT accepted: it merges into a RUNNING // turn, which is not what any segment here asks for. const disposition = ack["disposition"]; if (disposition !== "started" && disposition !== "queued") { throw new Error( `turn/start ack disposition: expected "started" or "queued", got ${JSON.stringify(disposition)}`, ); } equals(ack["startedNewTurn"], disposition === "started", "turn/start ack startedNewTurn"); return { turnId: stringAt(ack, "turnId", "turn/start ack") };}
/** Fails with the host's own reason text when a turn ends non-`completed`. */function requireTerminal( completed: RecordedNotification, expected: string,): void { const terminal = completed.params["terminal"]; if (terminal !== expected) { const reason = completed.params["reason"]; throw new Error( `turn/completed terminal was ${JSON.stringify(terminal)}, expected ${JSON.stringify(expected)}` + (typeof reason === "string" ? `; host reason: ${reason}` : ""), ); }}
const SEGMENTS: ReadonlyArray<Segment<Context>> = [ { id: "spawn", title: "Spawn the release-built host and keep it alive", async run(context) { // Host.start spawns and handshakes in one step, which is what a // consumer writes. Spawn is proven by the handshake answering at all: // a host that dies on launch (closed SDK gate = exit 5, bad config = // exit 3) surfaces its classified exit here instead of a hang. context.host = await Host.start( { museBin: context.museBin, home: context.home, workspaceRoot: context.workspaceRoot, clientInfo: QUICKSTART_CLIENT_INFO, }, HANDSHAKE_BUDGET_MS, ); }, }, { id: "handshake", title: "The handshake result describes the host and matches the SDK's schema pin", async run(context) { const host = requireHost(context); const result = host.msp.initializeResult as unknown as Record<string, unknown>; const serverInfo = objectAt(result, "serverInfo", "initialize result"); stringAt(serverInfo, "name", "initialize serverInfo"); stringAt(serverInfo, "version", "initialize serverInfo"); stringAt(result, "museHome", "initialize result"); stringAt(result, "sessionDurability", "initialize result"); const schema = objectAt(result, "schema", "initialize result"); equals( stringAt(schema, "fingerprint", "initialize schema"), EXPECTED_SCHEMA_FINGERPRINT, "the served schema fingerprint vs the fingerprint @muse-code/sdk pins", ); // A mismatch is a warning, never an error — so the equality above is // what actually binds, and this asserts the SDK drew the same // conclusion. equals(host.msp.fingerprintWarning, undefined, "SDK fingerprint warning"); }, }, { id: "session-new", title: "Start a new session in the workspace", async run(context) { const host = requireHost(context); const result = await host.msp.connection.command( "session/start", { workspaceRoot: context.workspaceRoot }, { maxAttempts: 1 }, ); const session = objectAt(result, "session", "session/start result"); const sessionId = stringAt(session, "sessionId", "session/start session"); equals(session["status"], "idle", "a fresh session's status"); equals( session["workspaceRoot"], context.workspaceRoot, "the session's workspace root", ); // A DEFAULT start writes no durable fact, so the served fold has no // view events yet and the cursor is pinned to exactly the // before-genesis `""` — the same pin the serve assembly asserts. // `stringAt`'s non-empty read predated the amendment and rejected the // legitimate `""`; the exact pin also rejects the pre-Seam-C stub // sentinel and any wrongly-minted non-empty cursor. const cursor = result["viewCursor"]; equals(cursor, "", "a default start's before-genesis view cursor"); // The push side of the same fact: the host announces the session it // just created, and it is the same session. const started = await host.waitFor( "the session/started notification", COMMAND_BUDGET_MS, (notification) => notification.method === "session/started", ); const announced = objectAt(started.params, "session", "session/started params"); equals(announced["sessionId"], sessionId, "the announced session id");
context.sessionId = sessionId; // NOTE: the committed golden `schema/msp/transcripts/session-start` // also carries `approvalMode` in this result; the release host omits it // here and pushes `session/approvalModeChanged` instead. }, }, { id: "session-effective-model", title: "The new session names the provider and model it will actually use", async run(context) { const host = requireHost(context); const sessionId = requireSessionId(context); const read = await host.msp.connection.request("session/read", { sessionId, excludeItems: true, }); const session = objectAt(read, "session", "session/read result"); stringAt(session, "providerId", "session/read session"); stringAt(session, "modelId", "session/read session"); }, }, { id: "turn", title: "Send a prompt and receive the agent's answer as a stream", async run(context) { const host = requireHost(context); const sessionId = requireSessionId(context); const { turnId } = await startTurn(host, sessionId, PROMPT);
await host.waitFor( "the turn/started notification", COMMAND_BUDGET_MS, (notification) => notification.method === "turn/started" && notification.params["turnId"] === turnId, ); const completed = await host.waitFor( "the turn/completed notification", STREAM_BUDGET_MS, (notification) => notification.method === "turn/completed" && notification.params["turnId"] === turnId, ); requireTerminal(completed, "completed");
// The turn is only useful if the agent actually said something, and it // must have arrived incrementally, not in one final lump. const deltas = host .notifications() .filter((notification) => notification.method === "item/delta"); if (deltas.length === 0) { throw new Error("the turn completed without streaming a single item/delta"); } const answers = host.notifications().map(agentText).filter((text) => text !== undefined); const answer = answers.at(-1); if (answer === undefined || answer.length === 0) { throw new Error("the turn completed without a non-empty agentMessage item"); } }, }, { id: "approval", title: "Answer the agent's permission request and see it resolved", async run(context) { const host = requireHost(context); const sessionId = requireSessionId(context); const { turnId } = await startTurn(host, sessionId, APPROVAL_PROMPT);
// Racing turn/completed against approval/requested turns "the turn died // before asking" into that sentence rather than a bare timeout. const event = await host.waitFor( "an approval/requested notification", STREAM_BUDGET_MS, (notification) => notification.method === "approval/requested" || (notification.method === "turn/completed" && notification.params["turnId"] === turnId), ); if (event.method !== "approval/requested") { const reason = event.params["reason"]; throw new Error( `the turn ended before asking for approval` + (typeof reason === "string" ? `; host reason: ${reason}` : ""), ); }
const approvalId = stringAt(event.params, "approvalId", "approval/requested params"); const decided = await host.msp.connection.command( "approval/decide", { sessionId, approvalId, requirementId: { approvalId, sourceIndex: 0 }, choiceId: "allow_once", feedback: null, }, { maxAttempts: 1 }, ); equals(decided["terminal"], true, "the approval decision is terminal"); const resolved = await host.waitFor( "the approval/resolved notification", COMMAND_BUDGET_MS, (notification) => notification.method === "approval/resolved" && notification.params["approvalId"] === approvalId, ); equals(resolved.params["approvalId"], approvalId, "the resolved approval id"); }, }, { id: "cancel", title: "Cancel a turn while it is running", // The block's second arm excused an `already_terminal` race the retired // logged-out provider caused by failing every turn in milliseconds; the // provider-configured mode removes that race at the source by keeping the // turn in flight (see `provider.ts` `HOLD_MS`) rather than re-importing // the excuse. async run(context) { const host = requireHost(context); const sessionId = requireSessionId(context); const { turnId } = await startTurn(host, sessionId, PROMPT); await host.waitFor( "the turn/started notification", COMMAND_BUDGET_MS, (notification) => notification.method === "turn/started" && notification.params["turnId"] === turnId, );
// The turn must still be running when the cancel lands. A turn that // already reached a terminal state cannot prove cancellation. const alreadyDone = host .notifications() .find( (notification) => notification.method === "turn/completed" && notification.params["turnId"] === turnId, ); if (alreadyDone !== undefined) { const reason = alreadyDone.params["reason"]; throw new Error( `the turn reached ${JSON.stringify(alreadyDone.params["terminal"])} before it could be cancelled` + (typeof reason === "string" ? `; host reason: ${reason}` : ""), ); }
await host.msp.connection.command( "turn/cancel", { sessionId, turnId }, { maxAttempts: 1 }, ); const completed = await host.waitFor( "the cancelled turn/completed notification", STREAM_BUDGET_MS, (notification) => notification.method === "turn/completed" && notification.params["turnId"] === turnId, ); requireTerminal(completed, "cancelled"); }, }, { id: "resume", title: "Close the host, reopen it, and load the same session with its state", async run(context) { // The session lease belongs to the live host, so the first one has to // drain cleanly before a second can load the session. That drain is // itself the spec-14990 Scenario-4.4 contract: stdin closed, orderly // drain, exit 0. const first = requireHost(context); const firstExit = await first.close(CLOSE_BUDGET_MS); equals(firstExit.code, 0, "the first host's exit code after stdin EOF"); context.host = undefined;
const sessionId = requireSessionId(context); const host = await Host.start( { museBin: context.museBin, home: context.home, workspaceRoot: context.workspaceRoot, clientInfo: QUICKSTART_CLIENT_INFO, }, HANDSHAKE_BUDGET_MS, ); context.resumeHost = host;
const resumed = await host.msp.connection.command( "session/resume", { sessionId, excludeItems: false }, { maxAttempts: 1 }, ); context.resumed = resumed;
const session = objectAt(resumed, "session", "session/resume result"); equals(session["sessionId"], sessionId, "the resumed session id"); equals( session["workspaceRoot"], context.workspaceRoot, "the resumed workspace root", ); const cursor = stringAt(resumed, "viewCursor", "session/resume result"); if (cursor === STUB_VIEW_CURSOR) { throw new Error(`session/resume returned the pre-Seam-C stub cursor ${cursor}`); } const history = objectAt(resumed, "history", "session/resume result"); equals(history["mode"], "inline", "the resumed history mode for an include-items resume"); arrayAt(history, "items", "session/resume history"); arrayAt(resumed, "pendingRequests", "session/resume result"); }, }, { id: "resume-effective-model", title: "The resumed session still names the provider and model it will use", async run(context) { const resumed = context.resumed; if (resumed === undefined) throw new Error("`resume` did not finish"); const session = objectAt(resumed, "session", "session/resume result"); stringAt(session, "providerId", "resumed session"); stringAt(session, "modelId", "resumed session"); }, }, { id: "resume-history", title: "The resumed history carries the turn that already happened", async run(context) { const resumed = context.resumed; if (resumed === undefined) throw new Error("`resume` did not finish"); const history = objectAt(resumed, "history", "session/resume result"); const items = arrayAt(history, "items", "session/resume history"); const kinds = items.map((item) => item !== null && typeof item === "object" ? (item as Record<string, unknown>)["kind"] : undefined, ); if (!kinds.includes("userMessage")) { throw new Error( `resumed history has no userMessage; kinds were ${JSON.stringify(kinds)}`, ); } if (!kinds.includes("agentMessage")) { throw new Error( `resumed history has no agentMessage; kinds were ${JSON.stringify(kinds)}`, ); } }, }, { id: "resume-rejects-unknown-cursor", title: "Resuming from a cursor that never existed is refused", async run(context) { const host = context.resumeHost; if (host === undefined) throw new Error("`resume` did not finish"); const sessionId = requireSessionId(context); // A cursor for a different session at an impossible sequence. No host // can ever have minted it. const impossible = `v:${sessionId}:999999`; let result: Record<string, unknown> | undefined; try { result = await host.msp.connection.command( "session/resume", { sessionId, cursor: impossible, excludeItems: false }, { maxAttempts: 1 }, ); } catch (error) { // ONLY the pinned refusal counts. A bare `instanceof MspError` lets // any unrelated protocol error (invalidParams, internal, a lease // failure) read as a correct refusal, so this segment would stay // green on MspError-shaped breakage once the expect-block comes off. // The shape is `notFound` / -32011. if (error instanceof MspError && error.kind === "notFound") return; throw error; } throw new Error( `session/resume accepted a cursor that never existed (${impossible}) and returned ${JSON.stringify( result, ).slice(0, 400)}`, ); }, }, { id: "terminate", title: "Close stdin and let the host drain and exit cleanly", async run(context) { const host = context.resumeHost; if (host === undefined) throw new Error("`resume` did not finish"); const exit = await host.close(CLOSE_BUDGET_MS); equals(exit.code, 0, "the host's exit code after stdin EOF"); equals(exit.signal, null, "the host's exit signal after stdin EOF"); const classification = await host.msp.child.exit; equals(classification.kind, "cleanShutdown", "the SDK's exit classification"); context.resumeHost = undefined; }, },];
/** Segment ids in run order. Exported so tests can pin the journey's shape. */export const SEGMENT_IDS: readonly string[] = SEGMENTS.map((segment) => segment.id);
/** * Expect-blocked segment id → its issue numbers, in run order. * * The list is empty today, so the section is absent — a heading that promises * a list of gaps and delivers the word "nothing" is scaffolding, not * disclosure. * * When a fix lands, the ONE required edit is deleting the segment's * `expectBlock`; every test then goes green while the README still calls the * segment broken. When a block RETURNS, the journey stays green — * expect-blocked is a pass — while the README still says nothing is wrong. * Neither may be silent, so a block cannot arrive or leave without the README * saying so. */export const EXPECT_BLOCKED: ReadonlyArray<{ readonly id: string; readonly issues: readonly number[];}> = SEGMENTS.filter((segment) => segment.expectBlock !== undefined).map((segment) => ({ id: segment.id, issues: (segment.expectBlock as { issues: readonly number[] }).issues,}));
/** What a provider-configured run observed, beyond the segment report. */export interface ConfiguredJourneyResult { readonly report: JourneyReport; /** The loopback endpoint the host was pointed at. */ readonly baseUrl: string; /** How many times the host fetched the model catalog. */ readonly catalogGets: number; /** * How many scripted tool calls the endpoint served. The harness's own * contract is AT MOST ONE — see `provider.ts`. The acceptance test asserts * it, because serving a second one silently parks the `cancel` segment. */ readonly scriptedToolCalls: number;}
export interface ConfiguredJourneyOptions { /** Absolute path to the release-built binary under test. */ readonly museBin: string; /** Workspace root a session is started in. Defaults to a fresh temp dir. */ readonly workspaceRoot?: string;}
/** * The ACCEPTANCE mode: the same twelve segments, against a host whose HOME * already has a provider configured. * * The provider is the loopback fake first-party endpoint from `provider.ts` — * no live provider, no API key, nothing off `127.0.0.1` — so this is the mode * CI runs. Every segment is required; none is expect-blocked. * * {@link runJourney} without a seeded `home` remains supported as the * credential-free degradation path. It is NOT the acceptance artifact: a host * with no provider cannot run a model, so its six model-dependent segments * fail for real and say so. */export async function runConfiguredJourney( options: ConfiguredJourneyOptions,): Promise<ConfiguredJourneyResult> { const provider = await startConfiguredProvider({ scriptedToolCallWhen: [APPROVAL_ARTIFACT, `"shell"`], scriptedToolCallCommand: `printf yes > ${APPROVAL_ARTIFACT}`, replyText: REPLY_TEXT, }); try { const report = await runJourney({ museBin: options.museBin, home: provider.home, ...(options.workspaceRoot === undefined ? {} : { workspaceRoot: options.workspaceRoot }), }); return { report, baseUrl: provider.baseUrl, catalogGets: provider.catalogGets(), scriptedToolCalls: provider.scriptedToolCalls(), }; } finally { await provider.close(); }}
export async function runJourney(options: JourneyOptions): Promise<JourneyReport> { const context: Context = { museBin: options.museBin, home: options.home ?? (await mkdtemp(join(tmpdir(), "muse-quickstart-home-"))), workspaceRoot: options.workspaceRoot ?? (await mkdtemp(join(tmpdir(), "muse-quickstart-ws-"))), }; // The loop, the always-run teardown, and the summary are the kit's // (`runJourney`), so this journey and every cookbook recipe cannot drift on // the part that leaks a spawned host when a segment throws (T-24225-6). return await runKitJourney(SEGMENTS, context, async (owned) => { await owned.host?.abandon(CLOSE_BUDGET_MS); await owned.resumeHost?.abandon(CLOSE_BUDGET_MS); });}main.ts
Section titled “main.ts”/** * Run the journey and print the segment report. * * node dist/src/main.js --bin <path-to-release-built-muse> * node dist/src/main.js --bin <path-to-release-built-muse> --no-provider * * The default is the provider-configured (acceptance) mode: the harness starts * a loopback fake first-party endpoint and points the host's HOME at it, so * every one of the twelve segments runs for real. Nothing leaves `127.0.0.1` * and no API key is used. * * `--no-provider` selects the credential-free degradation path: no provider is * configured, so the host cannot run a model and the six model-dependent * segments fail. That run is useful for isolating the segments that need no * model; it is NOT the acceptance artifact. * * Exit 0 means every required segment passed and every expect-block is still * accurate. Exit 1 means a required segment failed, or a blocked segment * started passing and now needs promoting. */
import { runConfiguredJourney, runJourney } from "./journey.js";import { formatReport } from "./segments.js";
function argument(name: string): string | undefined { const index = process.argv.indexOf(name); if (index === -1) return undefined; return process.argv[index + 1];}
const museBin = argument("--bin") ?? process.env["MUSE_BIN"];if (museBin === undefined || museBin.length === 0) { process.stderr.write( "usage: node dist/src/main.js --bin <path-to-release-built-muse> [--no-provider]\n" + " (or set MUSE_BIN). Build it with:\n" + " install muse, then: MUSE_BIN=$(command -v muse)\n", ); process.exit(2);}
if (process.argv.includes("--no-provider")) { const report = await runJourney({ museBin }); process.stdout.write(`${formatReport(report)}\n`); process.stdout.write( "\nmode=credential-free (no provider configured) — this is the degradation path, not the acceptance run\n", ); process.exit(report.ok ? 0 : 1);}
const configured = await runConfiguredJourney({ museBin });process.stdout.write(`${formatReport(configured.report)}\n`);process.stdout.write( `\nmode=provider-configured baseUrl=${configured.baseUrl}` + ` catalogGets=${String(configured.catalogGets)}` + ` scriptedToolCalls=${String(configured.scriptedToolCalls)}\n`,);process.exit(configured.report.ok ? 0 : 1);provider.ts
Section titled “provider.ts”/** * The journey's provider-configured mode: a loopback fake first-party endpoint * plus the `HOME` that points `muse serve` at it. * * It serves the same two routes with the same bodies, and the `HOME` it seeds * carries the same two files with the same shape: * * `GET <base>/muse-code/models` -> one visible, dated row, so the host's * `pick_default_model_from` yields a model. * `POST <base>/responses` -> a text SSE turn, or ONE scripted `shell` * tool call (see below). * `$HOME/.config/muse/settings.json` -> `endpoint_transport` at that base URL * with `auth: "bearer"`. * `$HOME/.config/muse/auth.json` -> a stored credential in the `meta` slot. * * Nothing here reaches the network: the listener binds `127.0.0.1:0` and the * stored credential is a fixed dummy the fake never checks. There is no live * provider and no API key, so this mode is safe to run in CI on every PR — it * is what lets the journey assert the turn, approval and cancel segments for * real instead of expect-blocking them. * * The credential-free mode (no `home` seeded) is still supported and is the * documented no-provider degradation path; see README.md. */
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";import { createServer } from "node:http";import type { Server } from "node:http";import { tmpdir } from "node:os";import { join } from "node:path";
/** The catalog's single model id. */const FAKE_MODEL_ID = "fake-model";
/** * The stored credential. It is LOAD-BEARING that one exists at all — a * keyless rig makes the "credentialed" arm credential-independent — but its * value is never checked, so it is a fixed literal and not a secret. */const DUMMY_API_KEY = "quickstart-journey-dummy-key";
export interface FakeProviderOptions { /** * Substrings that appear together in the completion request body of the ONE * turn that should get the scripted tool call, and in no other request. */ readonly scriptedToolCallWhen: readonly string[]; /** The command the scripted `shell` tool call asks to run. */ readonly scriptedToolCallCommand: string; /** The text every other completion answers with. */ readonly replyText: string;}
/** A running fake endpoint and the `HOME` configured to talk to it. */export interface ConfiguredProvider { /** The seeded `HOME` to hand `JourneyOptions.home`. */ readonly home: string; /** The loopback base URL the seeded `HOME` points at. */ readonly baseUrl: string; /** How many times the model catalog was fetched. */ catalogGets(): number; /** * How many scripted tool calls were served. The harness's own contract is * that this is AT MOST ONE: after the approval segment's turn, its artifact * name is in the replayed history of every later turn, so a content match * with no once-only guard hands the `cancel` segment a tool call too and * parks it on an approval nobody answers. */ scriptedToolCalls(): number; /** Stop the listener and drop every connection it is still holding. */ close(): Promise<void>;}
/** * How long each completion is held open between its first delta and its * `response.completed`. * * The `cancel` segment asserts that a RUNNING turn can be cancelled. An * endpoint that answers instantly turns that into a race the journey loses * intermittently: the turn reaches terminal before `turn/cancel` is admitted * and the host rejects the cancel with `already_terminal`. That race is * precisely the excuse the retired expect-block used to carry, so the fake * removes it at the source instead of re-importing it — the turn is * unambiguously in flight when the cancel lands. * * The value is slack, but it is NOT free slack: a serve turn drives the * reminder plugins' child sessions through this same endpoint, so a segment * waits on several holds in series. Measured on the release host, the `turn` * segment costs about five holds end to end. 3s therefore buys a ~30x margin * over the ~100ms window the host leaves on its own, while keeping `turn` near * 15s against its 60s `STREAM_BUDGET_MS` — a 5s hold would put `turn` at ~25s, * 42% of that budget, trading a cancel race for a timeout race. */const HOLD_MS = 3_000;
function sse(value: unknown): string { return `data: ${JSON.stringify(value)}\n\n`;}
/** One visible, dated row so the host's default-model pick yields a model. */function catalogBody(): string { return JSON.stringify({ object: "list", data: [ { id: FAKE_MODEL_ID, object: "model", metadata: { "muse-code": { release_date: "2026-01-01", is_hidden: false, limit: { context: 1_000_000, output: 1024 }, }, }, }, ], });}
function responseFrame(id: string, status: string, extra: Record<string, unknown> = {}): unknown { return { id, object: "response", model: FAKE_MODEL_ID, status, output: [], ...extra };}
/** The head of a completion: created, then the one content-bearing event. */function completionHead(text: string): string { return ( sse({ type: "response.created", sequence_number: 1, response: responseFrame("resp_journey_text", "in_progress"), }) + sse({ type: "response.output_text.delta", sequence_number: 2, output_index: 0, item_id: "msg_journey_text", content_index: 0, delta: text, }) );}
/** The head of the scripted `shell` tool call. */function toolCallHead(callId: string, command: string): string { return ( sse({ type: "response.created", sequence_number: 1, response: responseFrame("resp_journey_tool", "in_progress"), }) + sse({ type: "response.function_call_arguments.done", sequence_number: 2, output_index: 0, item_id: `fc_${callId}`, name: "shell", call_id: callId, arguments: JSON.stringify({ command }), }) );}
function completionTail(id: string): string { return sse({ type: "response.completed", sequence_number: 3, response: responseFrame(id, "completed", { usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, }), });}
/** * Start the loopback endpoint and seed a `HOME` that points at it. * * The caller owns the returned handle and MUST `close()` it; the listener and * its hold timers keep the process alive otherwise. */export async function startConfiguredProvider( options: FakeProviderOptions,): Promise<ConfiguredProvider> { const catalog = catalogBody(); let catalogGets = 0; let scriptedToolCalls = 0; /** Every in-flight hold, so `close()` can never be blocked by one. */ const holds = new Set<NodeJS.Timeout>();
const server: Server = createServer((request, response) => { const chunks: Buffer[] = []; request.on("data", (chunk: Buffer) => chunks.push(chunk)); request.on("end", () => { const body = Buffer.concat(chunks).toString("utf8"); if (request.method === "GET" && (request.url ?? "").endsWith("/muse-code/models")) { catalogGets += 1; response.writeHead(200, { "content-type": "application/json" }); response.end(catalog); return; }
// Once-only, content-routed: see `scriptedToolCallWhen` and // `scriptedToolCalls` above for why both halves are load-bearing. const isScripted = scriptedToolCalls === 0 && options.scriptedToolCallWhen.every((needle) => body.includes(needle));
let head: string; let responseId: string; if (isScripted) { scriptedToolCalls += 1; responseId = "resp_journey_tool"; head = toolCallHead(`call_journey_${String(scriptedToolCalls)}`, options.scriptedToolCallCommand); } else { responseId = "resp_journey_text"; head = completionHead(options.replyText); }
response.writeHead(200, { "content-type": "text/event-stream" }); response.write(head); const hold = setTimeout(() => { holds.delete(hold); // A cancelled turn aborts the request, so the socket is often already // gone by now. Finishing a dead response is a no-op, not an error. if (!response.writableEnded) response.end(completionTail(responseId)); }, HOLD_MS); holds.add(hold); }); // An aborted request (the cancel path) must not reach the process as an // unhandled error event. request.on("error", () => undefined); response.on("error", () => undefined); });
await new Promise<void>((resolve) => { server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); if (address === null || typeof address === "string") { server.close(); throw new Error("the fake provider endpoint did not bind a TCP port"); } const baseUrl = `http://127.0.0.1:${String(address.port)}`;
const home = await mkdtemp(join(tmpdir(), "muse-quickstart-provider-home-")); const configDir = join(home, ".config", "muse"); await mkdir(configDir, { recursive: true }); await writeFile( join(configDir, "settings.json"), `${JSON.stringify({ schema_version: 1, endpoint_transport: { base_url: baseUrl, auth: "bearer" } }, null, 2)}\n`, ); await writeFile( join(configDir, "auth.json"), `${JSON.stringify({ schema_version: 1, providers: { meta: { api_key: DUMMY_API_KEY } } })}\n`, );
return { home, baseUrl, catalogGets: () => catalogGets, scriptedToolCalls: () => scriptedToolCalls, close: async () => { for (const hold of holds) clearTimeout(hold); holds.clear(); // Sockets the host left open would keep `close` pending forever. server.closeAllConnections(); await new Promise<void>((resolve) => { server.close(() => { resolve(); }); }); }, };}segments.ts
Section titled “segments.ts”/** * The expect-block contract now lives in the shared cookbook kit so the * quickstart journey and every cookbook recipe classify outcomes with the * same code. This module re-exports it unchanged: the journey's import * surface, semantics, and report format are exactly what they were when the * contract lived in this file. */
export { classify, formatReport, runJourney, runSegment, summarize,} from "@muse-code/sdk-cookbook/kit";export type { ExpectBlock, JourneyReport, Segment, SegmentOutcome, SegmentResult,} from "@muse-code/sdk-cookbook/kit";