Skip to content
Developer Preview

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.

/**
* 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";
/**
* 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);
});
}
/**
* 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);
/**
* 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();
});
});
},
};
}
/**
* 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";