diff --git a/__tests__/fixtures/published-guide/README.md b/__tests__/fixtures/published-guide/README.md
new file mode 100644
index 0000000..2daca5c
--- /dev/null
+++ b/__tests__/fixtures/published-guide/README.md
@@ -0,0 +1,16 @@
+# Published command fixture
+
+`cli.mdx` is the complete, unmodified Marketing guide at the immutable source in
+`provenance.json`. The test extracts its email/song examples and chat-link flag,
+then runs the built CLI with synthetic credentials and intercepted native fetch.
+No endpoint is contacted. Request records omit headers.
+
+Run `pnpm build` then `pnpm exec vitest run __tests__/publishedCommands.test.ts`.
+Documentation owners can set `RECOUP_CLI_REFERENCE_PATH` to an absolute candidate
+Markdown path to run the same parser/request assertions before publishing.
+
+When these examples intentionally change, copy the complete guide from its owning
+commit, update provenance and SHA256, and review the expected requests together.
+The default check protects this pinned guide; it does not monitor future live docs
+or establish installed npm package behavior. Coverage is limited to email/song
+examples and the retired email flag, not every CLI command or authorization rule.
diff --git a/__tests__/fixtures/published-guide/cli.mdx b/__tests__/fixtures/published-guide/cli.mdx
new file mode 100644
index 0000000..0ebdddc
--- /dev/null
+++ b/__tests__/fixtures/published-guide/cli.mdx
@@ -0,0 +1,192 @@
+---
+title: "CLI"
+description: "Install the Recoup CLI and interact with the platform from your terminal."
+---
+
+The Recoup CLI (`@recoupable/cli`) wraps the Recoup API for terminal-first workflows. It's available as a global npm package and comes pre-installed in sandbox environments.
+
+
+**The CLI is in beta.** The reference below follows the v0.1.15 source command set. Check `recoup --version` and `--help` for the package installed in your environment. For research workflows and most content operations, call the [REST API](/api-reference) directly — the CLI is gradually catching up.
+
+**Agents:** if a command isn't listed below, don't retry — fall back to the corresponding REST endpoint linked under each command.
+
+
+## Install
+
+```bash
+npm install -g @recoupable/cli
+```
+
+## Authenticate
+
+Set your API key as an environment variable:
+
+```bash
+export RECOUP_API_KEY=your-api-key
+```
+
+Verify it works:
+
+```bash
+recoup whoami
+```
+
+
+Get an API key from the [API Keys page](https://chat.recoupable.dev/keys) or use the [agent signup](/agents) for instant key generation.
+
+
+## Configuration
+
+| Variable | Required | Default | Description |
+|----------|----------|---------|-------------|
+| `RECOUP_API_KEY` | Yes | — | Your Recoup API key |
+| `RECOUP_API_URL` | No | `https://recoup-api.vercel.app` | API base URL override; set `https://api.recoupable.dev` to use the canonical API domain |
+
+All commands support `--json` for machine-readable output and `--help` for usage info.
+
+---
+
+## Commands
+
+### whoami
+
+Show the authenticated account. See [`GET /api/accounts/id`](/api-reference/accounts/id).
+
+```bash
+recoup whoami
+recoup whoami --json
+```
+
+### orgs
+
+List organizations. See [`GET /api/organizations`](/api-reference/organizations/list).
+
+```bash
+recoup orgs list
+recoup orgs list --account
+```
+
+### artists
+
+List artists. See [`GET /api/artists`](/api-reference/artists/list).
+
+```bash
+recoup artists list
+recoup artists list --org
+recoup artists list --account
+```
+
+### emails
+
+Send an email to the authenticated account. See [`POST /api/emails`](/api-reference/emails/create).
+
+```bash
+recoup emails --subject "Pulse Report" --text "Here's your weekly summary."
+recoup emails --subject "Update" --html "Report
Details here.
"
+```
+
+| Flag | Required | Description |
+|------|----------|-------------|
+| `--subject ` | No | Email subject line; defaults from the body when omitted |
+| `--text ` | No | Plain text or Markdown body |
+| `--html ` | No | Raw HTML body (takes precedence over `--text`) |
+| `--cc ` | No | CC recipient (repeatable) |
+| `--chat-id ` | No | Chat ID for a chat link in the email footer |
+| `--account ` | No | Send to a specific account (org keys only) |
+
+### chats
+
+Manage chats. See [`GET /api/chats`](/api-reference/chat/chats) and [`POST /api/chats`](/api-reference/chat/create).
+
+```bash
+recoup chats list
+recoup chats create --name "My Topic"
+recoup chats create --name "My Topic" --artist
+```
+
+### sandboxes
+
+Manage sandboxes. See [`GET /api/sandboxes`](/api-reference/sandboxes/list) and [`POST /api/sandboxes`](/api-reference/sandboxes/create).
+
+```bash
+recoup sandboxes list
+recoup sandboxes create
+recoup sandboxes create --command "ls -la"
+```
+
+### tasks
+
+Check background task status. See [`GET /api/tasks/runs`](/api-reference/tasks/runs).
+
+```bash
+recoup tasks status --run
+```
+
+### songs
+
+Run AI music analysis. See [`POST /api/songs/analyze`](/api-reference/songs/analyze) and [`GET /api/songs/analyze/presets`](/api-reference/songs/analyze-presets).
+
+```bash
+recoup songs presets
+recoup songs analyze --preset catalog_metadata --audio https://example.com/track.mp3
+recoup songs analyze --prompt "Describe the production style" --audio https://example.com/track.mp3
+```
+
+Exactly one of `--preset` or `--prompt` is required, together with `--audio`. The API rejects both selectors together and verifies the audio URL before inference. The coordinated CLI validation fix reports these errors before sending a request; older installed packages may send them to the API.
+
+| Flag | Required | Description |
+|------|----------|-------------|
+| `--preset ` | Conditional | Curated analysis preset. Required when `--prompt` is omitted. |
+| `--prompt ` | Conditional | Custom text prompt. Required when `--preset` is omitted. |
+| `--audio ` | Yes | Public URL to an audio file (MP3, WAV, FLAC), required for every preset and custom prompt. Metadata-only analysis is not supported. |
+| `--max-tokens ` | No | Max tokens to generate (default 512). |
+
+---
+
+## content
+
+Content creation pipeline — generate AI-powered social videos for artists.
+
+### List templates
+
+```bash
+recoup content templates
+```
+
+### Validate an artist
+
+Check that an artist has the required assets before creating content. See [`GET /api/content/validate`](/api-reference/content/validate).
+
+```bash
+recoup content validate --artist
+```
+
+### Estimate cost
+
+Preview estimated cost and duration before kicking off the pipeline. See [`GET /api/content/estimate`](/api-reference/content/estimate).
+
+```bash
+recoup content estimate
+recoup content estimate --lipsync --batch 2 --compare
+```
+
+### Create content
+
+Run the full content-creation pipeline for an artist. See [`POST /api/content/create`](/api-reference/content/create).
+
+```bash
+recoup content create --artist
+recoup content create --artist --template --lipsync --upscale
+```
+
+| Flag | Required | Description |
+|------|----------|-------------|
+| `--artist ` | Yes | Artist account ID |
+| `--template ` | No | Template name (default: random) |
+| `--lipsync` | No | Enable lipsync mode |
+| `--upscale` | No | Enable upscaling |
+| `--caption-length ` | No | Max caption length in characters |
+
+
+For finer-grained control (individual image, video, caption, transcription, edit, upscale, or analyze operations), call the [content REST endpoints](/api-reference/content/generate-image) directly. Those primitives aren't yet exposed as individual CLI subcommands.
+
diff --git a/__tests__/fixtures/published-guide/intercept.cjs b/__tests__/fixtures/published-guide/intercept.cjs
new file mode 100644
index 0000000..5573ab8
--- /dev/null
+++ b/__tests__/fixtures/published-guide/intercept.cjs
@@ -0,0 +1,19 @@
+const { appendFileSync } = require("node:fs");
+
+globalThis.fetch = async (input, options = {}) => {
+ const url = new URL(String(input));
+ if (url.origin !== "https://cli-fixture.invalid") {
+ throw new Error("Unexpected fixture origin");
+ }
+ appendFileSync(process.env.CLI_FIXTURE_REQUEST_LOG, JSON.stringify({
+ method: options.method,
+ path: url.pathname,
+ body: options.body ? JSON.parse(options.body) : null,
+ }) + "\n");
+ return new Response(JSON.stringify({
+ message: "Synthetic email response",
+ response: "Synthetic analysis response",
+ elapsed_seconds: 0,
+ presets: [],
+ }), { status: 200, headers: { "Content-Type": "application/json" } });
+};
diff --git a/__tests__/fixtures/published-guide/provenance.json b/__tests__/fixtures/published-guide/provenance.json
new file mode 100644
index 0000000..e681e29
--- /dev/null
+++ b/__tests__/fixtures/published-guide/provenance.json
@@ -0,0 +1,6 @@
+{
+ "repository": "https://github.com/recoupable/marketing",
+ "commit": "e1cc8eaf09c19a037e5615bffde76b3e8cc573c3",
+ "path": "content/docs/source/cli.mdx",
+ "sha256": "960556fcd300c5216fa708db8f1ca9379ae4592788b4df57b7cfcda2f08c3eae"
+}
diff --git a/__tests__/publishedCommands.test.ts b/__tests__/publishedCommands.test.ts
new file mode 100644
index 0000000..ff5f1e4
--- /dev/null
+++ b/__tests__/publishedCommands.test.ts
@@ -0,0 +1,109 @@
+import { createHash } from "node:crypto";
+import { spawnSync } from "node:child_process";
+import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import provenance from "./fixtures/published-guide/provenance.json";
+
+const fixtures = join(__dirname, "fixtures", "published-guide");
+const snapshot = readFileSync(join(fixtures, "cli.mdx"), "utf8");
+// Documentation owners can run the same guard against a candidate guide.
+const guide = process.env.RECOUP_CLI_REFERENCE_PATH
+ ? readFileSync(process.env.RECOUP_CLI_REFERENCE_PATH, "utf8")
+ : snapshot;
+
+function section(name: string) {
+ const heading = `### ${name}\n`;
+ expect(guide.split(heading)).toHaveLength(2);
+ return guide.split(heading)[1].split(/^#{1,3} /m)[0];
+}
+
+function examples(name: string) {
+ return [...section(name).matchAll(/```bash\n([\s\S]*?)```/g)]
+ .flatMap((match) => match[1].trim().split("\n"));
+}
+
+function run(command: string) {
+ // These published examples use double-quoted arguments, without shell expansion.
+ // Reject unsupported syntax rather than accidentally executing a shell.
+ const tokens = command.match(/"[^"\n]*"|[^\s"]+/g) ?? [];
+ expect(tokens[0]).toBe("recoup");
+ expect(tokens.join(" ")).toBe(command);
+ const args = tokens.slice(1).map((token) => token.replace(/^"|"$/g, ""));
+ const directory = mkdtempSync(join(tmpdir(), "recoup-cli-guide-"));
+ const log = join(directory, "requests.jsonl");
+ try {
+ const result = spawnSync(process.execPath, [
+ "--require", join(fixtures, "intercept.cjs"),
+ join(__dirname, "..", "dist", "bin.cjs"), ...args,
+ ], {
+ encoding: "utf8",
+ timeout: 10000,
+ env: {
+ RECOUP_API_KEY: "fixture-not-a-real-key",
+ RECOUP_API_URL: "https://cli-fixture.invalid",
+ CLI_FIXTURE_REQUEST_LOG: log,
+ },
+ });
+ expect(result.error).toBeUndefined();
+ const requests = existsSync(log)
+ ? readFileSync(log, "utf8").trim().split("\n").map((line) => JSON.parse(line))
+ : [];
+ return { ...result, requests };
+ } finally {
+ rmSync(directory, { recursive: true, force: true });
+ }
+}
+
+describe("published command contract (built CLI, intercepted requests)", () => {
+ it("pins the unmodified maintained guide to its recorded source", () => {
+ expect(createHash("sha256").update(snapshot).digest("hex")).toBe(provenance.sha256);
+ });
+
+ it("runs every email example from the guide", () => {
+ const commands = examples("emails");
+ expect(commands).toHaveLength(2);
+ const expected = [
+ { subject: "Pulse Report", text: "Here's your weekly summary." },
+ { subject: "Update", html: "Report
Details here.
" },
+ ];
+ commands.forEach((command, index) => {
+ const result = run(command);
+ expect(result.status).toBe(0);
+ expect(result.requests).toEqual([{ method: "POST", path: "/api/emails", body: expected[index] }]);
+ });
+ });
+
+ it("maps the published chat link option to chat_id", () => {
+ const option = section("emails").match(/\| `(--[^`]+)` \| No \| Chat ID for a chat link/);
+ expect(option).not.toBeNull();
+ const result = run(`${examples("emails")[0]} ${option![1].replace("", "fixture-chat")}`);
+ expect(result.status).toBe(0);
+ expect(result.requests).toEqual([{ method: "POST", path: "/api/emails", body: {
+ subject: "Pulse Report", text: "Here's your weekly summary.", chat_id: "fixture-chat",
+ } }]);
+ });
+
+ it("rejects retired --room-id before any request", () => {
+ const result = run(`${examples("emails")[0]} --room-id fixture-chat`);
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("unknown option '--room-id'");
+ expect(result.requests).toEqual([]);
+ });
+
+ it("runs every song example from the guide", () => {
+ const commands = examples("songs");
+ expect(commands).toHaveLength(3);
+ const expected = [
+ { method: "GET", path: "/api/songs/analyze/presets", body: null },
+ { method: "POST", path: "/api/songs/analyze", body: { preset: "catalog_metadata", audio_url: "https://example.com/track.mp3" } },
+ { method: "POST", path: "/api/songs/analyze", body: { prompt: "Describe the production style", audio_url: "https://example.com/track.mp3" } },
+ ];
+ commands.forEach((command, index) => {
+ const result = run(command);
+ expect(result.status).toBe(0);
+ expect(result.requests).toEqual([expected[index]]);
+ });
+ });
+});