The canonical forge demo capability — how forge runs a demo, what every demo must contain, and how the demo maps to the forge UI review page. A demo makes ONE behavioural delta (prior → new) visible, grounded in the initiative's acceptance criteria. The demo phase agent (skills/demo-agent, R4-07) composes this skill to author the structured demo.json and derive DEMO.md. The demo agent reads the project's generated demo skill (from skills/demo-design) to know what evidence to produce; this skill defines the contract that evidence must satisfy. Scale demo effort to the size of the change.
Install
npx skillscat add parsofish/forge-studio/demo Install via the SkillsCat registry.
Demo — the forge demo capability (the consumer contract)
What this skill is
This is the single source of truth for the demo contract between forge and a
managed project. The review phase's job is to show the operator what changed
so they can make the merge decision; the demo is that artefact. This skill owns:
- What a demo must contain (the contract below).
- How forge runs a demo — read the project's generated demo skill, author
demo.json, deriveDEMO.md. - How the demo maps to the forge UI — the same
demo.jsonthe review
screen renders (DemoComparison), so the PR artefact and the UI never drift.
The project-side demo machinery is generated by skills/demo-design when
the operator configures demoProcess in Studio. The generated skill (at<artifactRoot>/skills/<slug>/SKILL.md in the project repo) tells the demo
agent (skills/demo-agent) what evidence to produce. This skill defines the
contract that evidence satisfies — the structured artefact from
ADR 021.
Required first action
- Load the project's generated demo skill — it is listed in the project's
skillsarray in.forge/project.json. Read it; it names the evidence form
and the exact commands to run. - Invoke
brain-queryagainst the project's brain before authoring:- "How does this project demonstrate a change — test harness, UI, live API?"
- "Are there demo/recording conventions or gotchas for this project?"
The demo contract (what EVERY demo must contain)
A demo is one structured artefact: <demo-dir>/demo.json
(schema = DemoModel in `cli/demo-model.ts`,
validated by the pr_self_contained gate). The demo dir is resolved against the
project's artifactRoot: it is demo/<initiative-id>/ for a default-layout
project (artifactRoot: "."), or <artifactRoot>/history/<initiative-id>/demo/
when the project gathers its committed artifacts under a sub-root (e.g.
betterado's forge/, giving forge/history/<initiative-id>/demo/). The
iteration brief (PROMPT.md) names the exact dir — write there; forge demo render <initiative-id> resolves the same path from artifactRoot, so you never
pass --dir. It must carry:
Required core fields
title— the one-line essence of the change.essence— 2–3 sentences: what behaviour changed and why it matters, framed
prior → new. State any non-visual delta here.project,initiativeId,diffStat(git diff --stat main...HEAD).checkpoints[]— ≥1, each{ label, caption, kind?, beforeNote?, afterNote?, command?, beforeOutput?, afterOutput?, metrics?, beforeImage?, afterImage? }. Each
checkpoint shows ONE before→after pair, grounded in an acceptance criterion or a
concrete diff change.- Prefer REAL captured output over a prose note. For a CLI tool, set
commandto
the exact argv whose stdout IS the evidence (e.g.node dist/cli.js churn .); leavebeforeOutput/afterOutputempty — the orchestrator runsforge demo capture
after the agent's iteration (ADR 036), executing the command onmain(before) AND
the branch HEAD (after) and back-filling the REAL terminal output, rendered
side-by-side. The agent authors WHAT to capture; forge produces the evidence —
hand-writtenbeforeOutput/afterOutputfor a command checkpoint is overwritten by
the orchestrator's own run. The operator wants to SEE the actual output, not a
description of it. UsebeforeNote/afterNoteonly as a one-line caption, or when
no command can show the change (a pure type/internal refactor).
Rich structured sections (strongly recommended — prevents wall-of-text)
The operator's #1 complaint is demos that are walls of text or bare diffs.
Always author these sections when the diff warrants them. The renderer
collapses them gracefully when absent, but their absence means a less useful demo.
summary—{ bullets: string[], prUrl?, commitSha?, branch? }.
3–5 concise bullets: what changed, why it matters, what the operator should
look for. Include the PR URL, branch, and commit SHA when available — these
appear in the header band of the rendered DEMO.md."summary": { "bullets": [ "Adds a dark-mode toggle that follows the OS preference by default.", "Persists the choice in localStorage so it survives page reload.", "No behaviour change when OS preference is light." ], "prUrl": "https://github.com/org/repo/pull/42", "branch": "forge/INIT-2026-05-30-dark-mode", "commitSha": "a1b2c3d" }apiDiff[]— Array of{ name, change: "added"|"changed"|"removed", before?, after? }.
Each entry is one changed endpoint, function signature, config key, CLI flag, or
protocol message.before/afterare the exact JSON shape or text — the renderer
syntax-highlights them. Include this section whenever the diff touches a public
API surface, a function signature, or an observable output format."apiDiff": [ { "name": "GET /api/nodes/:id", "change": "changed", "before": "{ \"id\": \"n1\", \"label\": \"A\" }", "after": "{ \"id\": \"n1\", \"label\": \"A\", \"grade\": 0.87 }" } ]testEvidence[]— Array of{ name, result: "pass"|"fail"|"skip", delta? }.
Include whenever the diff adds or changes tests, or when you can report a
coverage delta.deltais a short annotation like"+3 new tests"or"coverage: 82% → 87%"."testEvidence": [ { "name": "computeGrade — returns float in [0,1]", "result": "pass", "delta": "+1 new test" }, { "name": "All existing tests", "result": "pass", "delta": "42/42 green" } ]filesChanged[]— Array of{ path, note? }. Optional annotated list; when
absent the renderer falls back todiffStat. When present, each entry gets a
human-readable annotation like"core logic","new file","test".usage_example(string, required for new-or-changed-capability initiatives) —
a fenced code block showing HOW to use the new capability: the HCL resource block,
the CLI invocation, or the API request that exercises it. This is the first thing
the operator looks for when reviewing a new feature.impact(string[], required for new-or-changed-capability initiatives) —
bullets describing what the new capability unlocks for the operator. Distinct fromessence(which is the technical delta) —impactis the "so what".acceptanceCriteria[]— the criteria the initiative was decomposed against,
one string per criterion, carried verbatim.Their VERDICTS are not yours to write. The per-criterion
met | partial | missed
judgment moved to the read-only review agent's own artifact,review-findings.json
(spec §5 item 5): the agent that assembles a demo must not also be the agent that
scores it against the criteria, and the reviewer that judges it never ran anything.
A demo that carries a verdict field is scoring its own evidence —DemoModelno
longer declares one, and nothing reads one.
Three load-bearing disciplines
- One behavioural delta, not a tour. Pick the single delta that is the essence
of the initiative. A tight 3–5 checkpoint scenario beats an exhaustive walk. - Baseline is a valid prior state, never "broken". Frame every checkpoint as
"prior behaviour → new behaviour", never "error → fixed". If the new feature did
not exist before, the baseline legitimately shows the prior screen/output —
describe it as prior behaviour inbeforeNote. - Never fabricate a visual for a non-visual change. An internal refactor, an
API change, a type change has no screenshot. State the delta inessence; do
not invent an on-screen overlay or re-shoot an unrelated screen. UseapiDiff
to show the before/after of the API surface instead.
How forge runs a demo
- Read the project's generated demo skill — per "Required first action" above.
- Author the demo.json at the dir named in PROMPT.md to the contract above.
Populate ALL applicable rich sections —summary,apiDiff,testEvidence,filesChanged— so the rendered DEMO.md is genuinely informative. - Derive the artefact:
Bash forge demo render <initiative-id>→ writesDEMO.mdfrom the JSON. Never hand-writeDEMO.md— it is derived, so
the PR artefact and the UI render stay identical. - Real before/after evidence is captured by the ORCHESTRATOR (ADR 036 —
orchestrator-owned gate execution). After the agent's iteration, forge itself
spawnsforge demo capture <initiative-id>in the worktree: for checkpoints
with acommand, it captures the REAL stdout onmainvs the branch HEAD
intobeforeOutput/afterOutput; for browser checkpoints, screenshots. It
re-renders DEMO.md and commits the result. The agent must NOT runforge demo captureand must never hand-writebeforeOutput/afterOutput
or thecapturefield — the orchestrator's run overwrites them, and thepr_self_containedgate rejects a command it cannot run or acapture
stamp without this run's nonce (enforced inorchestrator/phases/orchestrated-capture.ts, N2). - Commit
demo.json+DEMO.md(the bundle is born tracked — no.forge/demos/
shadow).
Effort tiers — scale the demo to the change (load-bearing)
A trivial change must not trigger a heavyweight demo. This is a real cost sink
(observed: ~$11 / 15 demo-agent iterations packaging a one-file test change). Match
the effort to the diff:
| Diff shape | Demo effort |
|---|---|
| Trivial (≤~2 files, no behavioural surface — a test add, a constant, a doc) | Notes-only. One checkpoint, essence + beforeNote/afterNote. No media capture — a demo with no command/screenshot checkpoints makes the orchestrator skip its capture run entirely. |
| Standard (a feature or fix with a clear behavioural delta) | Full checkpoints grounded in ACs; summary + apiDiff + testEvidence when applicable; metrics if measurable; media only if the generated skill specifies screenshots. |
| Large / multi-feature | A checkpoint per distinct delta (still tight per checkpoint); summary bullets for each sub-change; apiDiff for each changed surface; a metrics table for harness evidence. |
When in doubt, prefer notes-only and let the operator ask for more. Media is an
enhancement, never a gate. Do author summary + apiDiff + testEvidence even
for notes-only demos when the diff touches a visible API or adds tests — these
sections make the rendered DEMO.md genuinely useful without requiring media capture.
Evidence forms (what the generated demo skill specifies)
The generated project demo skill (from skills/demo-design) specifies one of
these evidence forms based on what the project's code actually exposes:
- Portal/browser screenshot — a rendered UI. Author checkpoints; the
orchestrator'sforge demo capturerun back-fills before/after screenshots.kind: "screenshot"for a settled UI state;kind: "video"for
time-dependent behaviour. - Harness metrics — behaviour measurable at the test layer. Run the project's
measurement command against baseline AND HEAD, scrape stable result lines,
encode them as akind: "harness"checkpoint withmetrics: [{ label, before, after, deltaPct, parity }]. Reuse the project's
test — never re-derive the measurement. Pair withtestEvidencerows.
Parity vocabulary:match(exact same result),within(within tolerance),diverged(regression),incomplete(no baseline available). - Live external API round-trip — the change stands up a REAL resource in a
live external system. The evidence floor is a real REST round-trip, not a
test-name table: provision the resource, read it back via the system's API,
and persist that GET under.forge/live-evidence/<label>.json;forge demo render
back-fills it into a checkpoint carryingliveEvidence.url. The demo MUST end
with such a checkpoint. Pair withtestEvidence(the live acceptance result)
and, for a new capability,usage_example+impact. When credentials are
absent, fall back to notes-only and document the fallback inessence—
never fabricate the live read-back. - JSON-diff / notes-only — no UI surface, no measurement command, no live
external calls. A single checkpoint whose caption +afterNoteis a rationale
block ("what changed and why it's correct"). Usesummarybullets for the
rationale.apiDiffto show any changed API/config surface.
Media capture (the optional, best-effort step — orchestrator-run, ADR 036)
For projects with a renderable UI, forge demo capture <initiative-id>
(orchestrator-run, never the agent) materialises two git worktrees (baselinemain vs changed HEAD), builds + serves each, takes ONE screenshot per
checkpoint label, then merges the captured images into demo.json
checkpoints as inline data: URIs (validateDemoModel rejects scheme-bearing
refs; video refs are relative sibling paths only) and re-renders DEMO.md.
It fills only beforeImage/afterImage — never captions, notes, metrics, or
the diffstat.
- Best-effort, never fatal. No buildable app / missing Playwright / capture
error → logs and exits 0; the demo stays notes-only. Never blocks the gate. - Serve the built tree, not a stray dev server. A reused dev server latches
onto the main repo's build and captures stale screenshots (trafficGame
lesson). The capture engine builds + serves each worktree on its own port.
How the demo maps to the forge UI page (the presentation half)
The structured demo.json is both the PR artefact (via the derived DEMO.md)
and the UI artefact: the review screen renders the same DemoModel natively viaDemoComparison.tsx. The schema is the template — what you put in demo.json
is exactly what the operator sees, so:
- The header band shows: title, cycle id, branch, commit SHA, PR link.
summarybullets appear in a dedicated Summary section above the checkpoints.- Each checkpoint becomes a
CheckpointCard(caption + before/after media or
notes + a metrics table for harness evidence). apiDiffrenders as syntax-highlighted before/after code blocks.testEvidencerenders as a pass/fail table with coverage deltas.filesChangedrenders as an annotated file list with the diffstat collapsible.- The screen mirrors structured state to
data-*(data-section="demo-comparison",
per-checkpointdata-checkpoint/data-checkpoint-kind) per the forge-ui
DOM-as-metrics convention. snapshotCycleArtefactscopiesdemo.json+DEMO.mdinto_logs/<cycleId>/artifacts/for the bridge to serve.
If you change the demo's structure, change DemoComparison + its data-*
alongside it — the schema, the renderer, and the UI are one contract.
The project-side requirements (what makes a project demo-able)
For onboarding (the forge-onboard-project skill + forge preflight), a project
must have:
- A
demoProcessin.forge/project.jsonwith ≥1captureand ≥1verify
step — validated byforge preflightDEMO clause. - Generated demo machinery — the
skills/demo-designskill generates<artifactRoot>/skills/<slug>/SKILL.mdinto the project repo;forge preflight
DEMO clause passes once this is present. - A truthful test gate (contract C1) — the same gate that proves the change
is what the demo shows. - A valid prior state — running the baseline must not error; the demo frames
it as prior behaviour.
Done when
demo/<initiative-id>/demo.json validates, DEMO.md is derived (not
hand-written) and committed alongside it, the effort matches the diff size,
every checkpoint is grounded in an AC or a real diff change and framed prior→new,
the summary / apiDiff / testEvidence / filesChanged sections are authored
where the diff warrants them, and (for visual projects) media capture has been
attempted best-effort.
Two faces: executed demo vs builder deliverable
The typed demoProcess descriptor (.forge/project.json) is the single shared
declaration of what a demo must capture/verify/present — but it has two married
faces, not two competing contracts (docs/reference/project-contract.md §DEMO).
Face A is the executed demo this skill defines: demo.json, composed at
cycle time by the demo agent (skills/demo-agent, R4-07). Face B is the demo-builder's per-initiative
HTML skill (skills/demo-builder, R1-03-F2): save-time authoring/preview of
what a demo could look like, not a cycle-time artefact. The descriptor is
shared; the deliverables intentionally differ.