Quickstart tutorials fail in a predictable way when a model invents commands that never ran in continuous integration. The durable fix is to freeze every command block from a reviewed fixture manifest and let the model draft only surrounding prose. Humans must still sign secret handling, abort behavior, host assumptions, and teardown, because those claims are operational rather than syntactic. Teams that invert this split publish samples that look complete and then break on the first missing environment variable.
This article treats getting-started copy as a compile problem with a human-owned annex. It does not classify API reference paragraphs, error catalogs, or release notes. The workflow below is a proposal with labeled example files, not a report of production traffic or measured reader conversion.
What the split actually protects
Tutorial readers copy blocks in order and stop at the first unexpected prompt. A model that drafts those blocks from chat memory will smooth over flags, image tags, and working directories that only exist in one author's laptop session. Continuous integration already knows which commands exited zero against a pinned fixture pack, so that pack is the only honest source for the command column.
Prose around those commands is a different artifact. Sentence rhythm, heading order, and short warnings can be drafted after the command hashes lock. Secret injection, what happens when step three fails, which ports collide on a shared laptop, and how to delete volumes are not properties of a successful CI job. Those statements need a named reviewer because they describe harm, not syntax.
Decision table: draft versus own
| Tutorial fragment | Source of truth | Model may draft? | Human must sign? |
|---|---|---|---|
| Command text, flags, working directory | Reviewed quickstart.manifest.yaml plus CI log |
No; compile verbatim | Reviewer signs the manifest, not each paste |
| Expected stdout snippets from fixtures | Captured, truncated logs with redaction rules | No; compile with a max-line cap | Owner signs redaction rules |
| Section titles and transition sentences | Frozen commands plus outline | Yes, after hash lock | Spot-check for invented flags |
| Required env var names | Manifest env_from list |
Compile names only | Owner writes how values are minted |
| Secret values, tokens, connection strings | Never in git or prompts | No | Owner forbids literals in the compiler |
| Failure meaning when a command exits non-zero | Runbook, not CI green path | No | Owner writes abort and retry |
| Teardown, volume deletion, billed-cloud cleanup | Ops policy | No | Owner signs destructive commands |
| "Safe for production" or uptime claims | Support and legal | No | Do not generate |
Keep the table in the repository beside the compiler. When a pull request adds a command, the table tells the reviewer whether prose generation is even allowed yet.
Artifact: a fixture manifest the compiler can refuse
Label the following files as a worked example. They are not captured from a public product and they do not assert benchmarks.
# docs/_fixtures/quickstart.manifest.yaml
# status: reviewed
schema_version: 1
id: qs-local-stack-2026-09
ci_job: docs-quickstart-smoke
runtime:
os: linux
compose_file: fixtures/quickstart/compose.yaml
workdir: /workspace/demo
steps:
- id: clone_sample
argv: ["git", "clone", "--depth", "1", "https://example.invalid/demo.git", "."]
timeout_s: 60
- id: up_deps
argv: ["docker", "compose", "up", "-d", "--wait", "db"]
timeout_s: 120
- id: apply_schema
argv: ["./scripts/migrate", "up"]
env_from: ["DATABASE_URL"]
timeout_s: 30
- id: run_probe
argv: ["./scripts/probe", "--json"]
timeout_s: 15
teardown:
argv: ["docker", "compose", "down", "-v"]
human_owned: true
forbid:
- secret_literals
- production_safety_claims
- host_network_assumptions
redact_stdout:
max_lines: 8
deny_patterns: ["postgres://", "Bearer ", "AKIA"]
The manifest pins argv arrays, not shell one-liners with unquoted interpolation. teardown.human_owned is a compiler hard stop: the model never writes the cleanup section, even if CI ran compose down. forbid is checked against both compiled blocks and drafted prose before merge.
Numbered workflow
1. Record commands from the smoke job, not from chat
Run the same argv arrays the tutorial will show. Capture exit codes and a redacted stdout slice. Do not paste a model's guessed docker run into the manifest because it "looks standard."
# proposal: record from the docs smoke job on 2026-09-15
set -euo pipefail
cd fixtures/quickstart
docker compose up -d --wait db
./scripts/migrate up
./scripts/probe --json | head -n 8
docker compose down -v
Store the job name in ci_job so a later compiler can fail when the job is renamed or skipped on a branch. If the smoke job is allowed to fail, the tutorial has no freeze point and generation should refuse.
2. Hash every frozen command block
# compile_quickstart.py — example compiler, not a packaged product
from __future__ import annotations
import hashlib, json, re, sys
from pathlib import Path
import yaml
FORBID = (
re.compile(r"postgres://\S+", re.I),
re.compile(r"Bearer\s+\S+", re.I),
re.compile(r"AKIA[0-9A-Z]{16}"),
re.compile(r"\bsafe for production\b", re.I),
)
def argv_block(argv: list[str]) -> str:
return " ".join(argv)
def digest(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
def main(manifest_path: str, prose_path: str | None) -> int:
man = yaml.safe_load(Path(manifest_path).read_text())
if man.get("status") != "reviewed":
print("refuse: manifest is not reviewed", file=sys.stderr)
return 2
frozen = []
for step in man["steps"]:
block = argv_block(step["argv"])
frozen.append({"id": step["id"], "block": block, "sha256_16": digest(block)})
out = {
"manifest_id": man["id"],
"ci_job": man["ci_job"],
"commands": frozen,
"teardown_human_owned": bool(man.get("teardown", {}).get("human_owned")),
}
Path("docs/_generated/quickstart.commands.json").write_text(json.dumps(out, indent=2))
if prose_path:
prose = Path(prose_path).read_text()
for pat in FORBID:
if pat.search(prose):
print(f"refuse: forbidden pattern {pat.pattern}", file=sys.stderr)
return 3
for item in frozen:
if item["block"] not in prose:
print(f"refuse: missing frozen block {item['id']}", file=sys.stderr)
return 4
print(json.dumps({"ok": True, "steps": len(frozen)}))
return 0
if __name__ == "__main__":
sys.exit(main(*sys.argv[1:4], *([None] if len(sys.argv) < 3 else [])))
The sixteen-character digest is a review aid, not a cryptographic security boundary. Its job is to make silent flag edits visible in the pull request diff of quickstart.commands.json.
3. Draft prose only after the JSON exists
Feed the compiler output plus a short outline into a drafting step. The prompt may request headings, a one-sentence goal, and a note that readers should stop on non-zero exit. The prompt must not ask the model to invent extra commands, default passwords, or cloud regions.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. A team that already reviews fixtures can run that drafting step with MonkeyCode's free model access on the free server option, then keep the signed annex in the same pull request. Those two availability claims are the only product facts used here; this article does not name models, quotas, hardware, or runtimes.
4. Attach a human-owned operational annex
Place the annex in a file the compiler never overwrites, for example docs/quickstart.ops.md. Required sections:
-
Secrets. How
DATABASE_URLis created for local use, where it must not be pasted, and that sample values in docs are non-functional placeholders. -
Failure. What a migrate timeout means, whether re-running
./scripts/migrate upis idempotent, and when to stop and file an issue instead of retrying. - Host assumptions. Linux is what the smoke job ran; macOS file sharing, rootless Docker, and CI-in-Docker nesting are out of scope unless separately signed.
- Teardown. Exact destructive command, what volumes it deletes, and that billed resources are not covered by this tutorial.
If any of those four headings is missing, the publish gate fails even when every frozen argv is present.
5. Close with three publish gates
Gate A — Manifest reviewed and ci_job still exists on the default branch.
Gate B — Every argv in the tutorial hashes to docs/_generated/quickstart.commands.json.
Gate C — Operational annex signed; forbid patterns absent from drafted prose.
A documentation preview that skips Gate C will still look finished. That is the failure mode this workflow is built to catch.
Reproducible test plan
Label this as an unexecuted example suite. Wire it to the same job named in the manifest.
# test_quickstart_frozen.py — example assertions
import json, pathlib, yaml
ROOT = pathlib.Path(__file__).parents[1]
def test_manifest_status_reviewed():
man = yaml.safe_load((ROOT / "docs/_fixtures/quickstart.manifest.yaml").read_text())
assert man["status"] == "reviewed"
assert man["teardown"]["human_owned"] is True
def test_generated_hashes_match_argv():
man = yaml.safe_load((ROOT / "docs/_fixtures/quickstart.manifest.yaml").read_text())
gen = json.loads((ROOT / "docs/_generated/quickstart.commands.json").read_text())
assert gen["ci_job"] == man["ci_job"]
assert len(gen["commands"]) == len(man["steps"])
def test_tutorial_contains_frozen_blocks_only_in_order():
gen = json.loads((ROOT / "docs/_generated/quickstart.commands.json").read_text())
text = (ROOT / "docs/quickstart.md").read_text()
positions = [text.index(item["block"]) for item in gen["commands"]]
assert positions == sorted(positions)
def test_ops_annex_has_required_headings():
ops = (ROOT / "docs/quickstart.ops.md").read_text().lower()
for heading in ("secrets", "failure", "host assumptions", "teardown"):
assert heading in ops
Add a CI rule that docs-quickstart-smoke must run before compile_quickstart.py. If the smoke job is skipped with paths-ignore on markdown-only branches, the freeze is stale by construction.
Limitations
The compiler cannot see commands the smoke job never executed. Windows shells, podman, and rootless setups need their own manifests or an explicit unsupported label. Redaction patterns miss novel secret shapes, so stdout capture remains a leak risk if logs are attached raw. Drafted prose can still smuggle host-network claims that are not secret-shaped; Gate C needs a human reader, not only regex.
This approach is a poor fit for incident runbooks, contractual SLA pages, and any tutorial that must mint real credentials in the reader's cloud account. It is also the wrong tool when the product has no deterministic smoke job, because there is then nothing honest to freeze. Maintainers who want a single chat transcript to become a getting-started guide should not use this split.
The compile-and-sign sequence is slow on purpose. That cost is the point: quickstart pages are copied more often than they are reread, so an invented flag is cheaper to type than to support. If a docs change is only typography, skip generation and edit the prose file against the already frozen JSON.
A reasonable next check is whether your smoke job and tutorial still share one argv list; if they do not, the fixture manifest is the smaller patch. MonkeyCode is optional for the prose draft once that list is reviewed, and it is unnecessary when the page is already a signed annex with no surrounding copy to generate.
Top comments (0)