Agent Skills: demo

Generate a demo of a feature developed in this session, in a web app or in a Chromium-based desktop app (Electron) — a page of test cases grouped by feature, each with its precondition, its own screen recording and its steps with screenshots, published as an artifact, driven by playwright-cli against the app the skill starts and stops itself. Use from dev-workflow's Update Rules gate (demo / no-demo) or standalone with /demo.

UncategorizedID: hiroro-work/claude-plugins/demo

Install this agent skill to your local

pnpm dlx add-skill https://github.com/hiroro-work/claude-plugins/tree/HEAD/plugins/dev-workflow-bundle/skills/demo

Skill Files

Browse the full folder contents for demo.

Download Skill

Loading file tree…

plugins/dev-workflow-bundle/skills/demo/SKILL.md

Skill Metadata

Name
demo
Description
Generate a demo of a feature developed in this session, in a web app or in a Chromium-based desktop app (Electron) — a page of test cases grouped by feature, each with its precondition, its own screen recording and its steps with screenshots, published as an artifact, driven by playwright-cli against the app the skill starts and stops itself. Use from dev-workflow's Update Rules gate (demo / no-demo) or standalone with /demo.

demo

/demo [--plan <path>] [--base <ref>] [--lang ja|en] [--model <id>]
/demo --init

--init runs Setup mode only: it proposes and writes the settings, then stops with the return contract (status: "ok", no capture). When a settings file already exists it stops with status: "skipped", reason: "settings exist" and never overwrites. Without --init, a run that finds no settings file enters Setup mode by itself and continues into the demo.

Invocation contract, as fields in the invocation text (a field counts as provided when its value is non-empty): Plan: (the plan document the feature was built from; optional), Base ref: (git ref the diff is taken against; default HEAD), Language: (optional, see § Language), Model: (optional, see § Model). The command-line flags map onto the same fields.

Everything the run produces goes under .demo/<slug>/ at the repository root: the screenshots, one recording per test case, saved values under values/, steps.json, start-command (with start-command.pgid while the app runs), server.log and index.html. The directory is meant to be gitignored and is never committed by this skill. Outside it, only Setup mode and § Procedure step 9 (Data guide gate) write.

No-Stall Principle

The only paths that do not reach § Return contract with status: "ok", each ending with the contract's JSON block: playwright-cli --help video-chapter exiting non-zero (playwright-cli not installed); a settings file that exists but cannot be read (settings unreadable); no start_command after the merge (start_command unset); cancel at the Setup gate (setup declined); --init with a settings file present (settings exist); .claude/demo.md appearing between the Setup gate and its write (setup conflict); neither a readable plan nor a diff (nothing to demo); skip at the start-failure gate (dev server not ready); skip at the step-table gate (declined at step table); stop at the publish gate (declined at publish gate); on the first capture, the capture agent's second unparseable reply (no parseable capture result) or a result that fails the schema check (capture schema violation); the renderer rejecting steps.json (page rejected). Once the app has been started, every one of these runs § Procedure step 7 (Stop the app) before emitting the block.

The capture agent's reply is a return value: judge it and issue the next tool call at once. A later capture (a re-capture, or a capture for the publish gate's revise) is one more dispatch within § Procedure step 6 (Capture); its result is judged like the first capture's, except that a second unparseable reply or a schema violation keeps the earlier results instead of stopping. A capture with failed steps is not a stopping point. The data check's session is closed before the step-table gate on every path. Non-fatal outcomes are recorded and the run continues: a step marked failed, a later capture's (a re-capture's or a publish-gate revise's) second unparseable reply or schema violation, a missing recording, a missing or failing Artifact tool, .demo/ or .playwright-cli/ not gitignored, a process still listening after the stop, a changed-files.txt that cannot be written (the run goes on without it), a stop_command that exits non-zero or times out, an environment left in place because <url> is remote and stop_command is unset, a seed or fixture file the data guide names that cannot be read (the guide counts as silent on it), an Edit at § Procedure step 9 (Data guide gate) that fails.

Settings

Two files: .claude/demo.md (shared by the team, committed) and .claude/demo.local.md (personal, gitignored). Each holds YAML frontmatter and, after its closing ---, an optional Markdown body. For the keys, the local file overrides the shared one key by key; a null or empty value in the local file clears the key.

The bodies, the shared one followed by the local one, read as one text: the data guide, prose in the resolved language saying where the seed or fixture files are, which accounts (identifier and role) and which main records they hold, and how one shell command line inserts a record the seed lacks. The shared body names where a credential comes from (a seed file, the local body, auth_state); a credential itself, or a personal insertion command, goes only in the local body. With attach: cdp, the guide also says what state the app keeps outside its windows (its user data directory, a local database) and how a run starts with it fresh (a throwaway user data directory in start_command, a reset_command), or that it is kept on purpose (a signed-in profile). Where the guide is silent, the run finds the fixtures or seed itself. .claude/demo.md is the guide's write target, .claude/demo.local.md when it alone exists.

| Key | Default | Meaning | |---|---|---| | start_command | required | Starts the app, in the background, under sh -c with the environment below and its stdin held open, with ${DEMO_PORT} in the command text expanded to the port first. A local app listens on DEMO_PORT; a remote target names its URL in url | | reset_command | none | Runs once, in the foreground, before start_command, with the same environment and with ${DEMO_PORT} expanded the same way. Resets a throwaway environment | | stop_command | none | Runs once, in the foreground, at the start of § Procedure step 7 (Stop the app), with the same environment. Stops or cleans up what start_command created; the process group and port kills follow. ${DEMO_PORT} is expanded the same way. Bounded by ready_timeout seconds | | case_reset | unset | What runs before each test case's setup, after the browser opens or the run attaches. Unset: the built-in reset (web mode: nothing beyond the fresh browser; with attach: cdp: every window's storage and cookies cleared and the window reloaded). none: nothing; in web mode it counts as unset. Any other value: a shell command line the capture agent runs as one Bash call with the environment below and DEMO_SESSION (the playwright-cli session name) exported; a non-zero exit counts as a failed setup action | | attach | none | cdp: the app is a Chromium-based desktop app that exposes a DevTools endpoint on DEMO_PORT; the run attaches to it instead of opening a browser, and url, when set, is a URL: that endpoint's origin. Any other value is treated as unset and said in one line | | url | none | Starting with http:// or https://: the URL itself, with ${VAR} expanded from the environment plus DEMO_PORT. Otherwise a command the wait re-runs, with the same environment, until it exits zero with a non-empty stdout line, within ready_timeout; that first non-empty line is the URL. With attach: cdp, only the URL form applies | | ready_timeout | 120 | Seconds to wait for the URL to resolve and answer, at most 590 | | port | none | When set, used as DEMO_PORT instead of a free port found at run time. Digits, or ${VAR} expanding to digits: the port itself. Otherwise a command run before the start with the same environment and DEMO_PORT=0; the first line of its stdout is the port | | auth_state | none | Storage state file passed to playwright-cli state-load after the browser opens or, with attach: cdp, after attaching and before the case's setup; over CDP the file's cookies are loaded, not its localStorage | | error_cases | true | false: derive no error test cases. Any other value is true | | video | full | normal: error test cases are not recorded; their screenshots are still taken. Any other value is full |

Environment. Every command a key names runs as DEMO_PORT=<port> DEMO_BASE_REF=<base> DEMO_CHANGED_FILES=<changed files> <command> (the port command with DEMO_PORT=0). <base> is Base ref as resolved in § Procedure step 2 (Inputs); <changed files> is .demo/<slug>/changed-files.txt, one path per line, written at the start of § Procedure step 4 (Start the app). That prefix is <environment> in § Procedure.

When neither file exists, or --init was given, read references/setup-mode.md and follow it. When a file exists but Read fails for any other reason, or when it cannot be told whether the file is missing or unreadable, stop with status: "error", reason: "settings unreadable: <path>" rather than entering Setup mode. When the merge leaves no start_command, stop with status: "error", reason: "start_command unset". <case reset> names what the case_reset row resolves to, its web-mode rule for none applied first: default for the built-in reset, none for nothing, command for a command line; an empty or boolean value counts as unset.

Language

Language: or --lang, when given, is the language and nothing is read. Otherwise resolve it by reading only the language key from the YAML frontmatter of ~/.claude/dev-workflow.local.md, .claude/dev-workflow.md and .claude/dev-workflow.local.md, in that order, later files overriding earlier ones; a null or empty value clears it, and a missing file, or one without frontmatter, contributes nothing. If none sets it, take language from ~/.claude/settings.json; otherwise use ja. The page's fixed labels, the step table and every line written for the user follow the resolved language.

Model

Model: or --model is applied as the model parameter of the capture Agent dispatch. It is valid only if it is one of the ids the current Agent tool's model parameter accepts; check the tool's live schema in this session. A full claude-* id is not among its aliases and is invalid. An absent or invalid value is treated as sonnet.

Dispatch authorization

This skill's procedure dispatches subagents, so invoking the skill is the request to use that mechanism: an ambient instruction allowing subagent dispatch only when the user asked for it — a permission-shaped restriction — is already satisfied by this invocation. Do not ask the user to re-confirm the dispatch, and do not silently substitute inline execution for a dispatch this procedure specifies. Only two things justify that substitution: technical availability (the dispatch tool is not present and callable on the current tool surface), and an explicit contract term from the caller bounding this skill to its own thread. A permission-shaped restriction is neither.

Procedure

<base dir> is this skill's directory as the harness reported it when the skill loaded.

  1. Preconditions. Run playwright-cli --help video-chapter. A non-zero exit stops with status: "error", reason: "playwright-cli not installed". When its output starts with playwright-cli video-chapter, <chapters> = true; otherwise <chapters> = false and say in one line that this version records without title cards, cursor or action callouts. Then resolve the language per § Language and the model per § Model, then the settings per § Settings (Setup mode when neither file exists).

  2. Inputs. With Plan:, <slug> is the file name without .md; Read the plan. When that Read fails, use the plan text when it is already in this conversation; otherwise say in one line that the steps come from the diff alone and continue. Without Plan:, <slug> is <YYYYMMDD-HHMMSS> of now. Read the change set against Base ref (default HEAD): git diff <base> and the content of files git ls-files --others --exclude-standard lists. No readable plan and an empty change set → status: "error", reason: "nothing to demo". Run mkdir -p .demo/<slug>.

  3. Derive the test cases. From the data guide (read with the settings in step 1), Read the parts of the seed or fixture files it names that name accounts and records; when it names none, find them as references/setup-mode.md § Read the project does. Then, from the plan's After, Scope and Test plan and from the diff:

    • Features. List the behaviors a person would check on screen, then group them into features. A feature is the set of actions a checker performs in a row for one purpose; it does not follow the count of tasks or commits. Group when the actions handle the same data for the same purpose, or when one action uses what an earlier one created or changed. Split when the purpose differs, when a part can be checked on its own, or when a different role performs it. When unsure, group. A single feature takes the same shape. Each feature gets a letter (A for the first, B for the second, and so on), a name stating what the user can do and a one- or two-sentence description.
    • Test cases. Per feature, each test case has a title saying in one sentence what it checks, a precondition (the sign-in state and the data it needs), the setup actions that bring the app to that precondition (from where § Procedure step 3's "Preconditions" paragraph says it starts; with attach: cdp, each action starts with the window it runs in as a step's path names one), and its steps. A case's id is the feature letter plus its order within the feature (A-1, A-2). Normal cases: the feature's main flow, usually one case (more when the main flow branches); 3–10 normal steps across all features.
    • Error cases, when error_cases is true. One test case per branch the diff implements (input validation, permissions, behavior that depends on state), each branch once; steps that prepare the branch belong to that case. Errors the diff does not implement (raised by the framework or by shared code) are not walked. There is no cap; when the error cases exceed 10, the gate shows their count and a time estimate (20–40 seconds per case) in one line. The expected result is the error text itself.
    • Preconditions. A case starts where the case reset leaves the app: with <case reset> default, where the built-in reset the case_reset row defines leaves it; with none, where the previous case left it, so its setup first brings the app to a known screen (signs out when signed in, opens the start window) whatever that state is, since a later capture runs the case after a different predecessor; with command, as the command leaves it. State the app keeps outside its windows counts as data an earlier case created. A case that needs a signed-in user gets there through auth_state when it is set, otherwise through setup actions that sign in on the sign-in page with an account from the fixtures or seed as the data guide names them. Only when the diff changes sign-in itself (the code that creates or checks a session, the authentication settings, the account fields used to sign in, the sign-in page) does signing in become steps. A case that relies on data an earlier case created says so in its precondition, and the case order follows from it. An error case uses records that earlier cases do not change or delete; when they would, its data names a record of its own. A value that can be taken only once (a confirmation code consumed on use) is taken by the first step that needs it, with a script whose step data says save as <name>; each later step that needs it says use <name> in its data.
    • Order and numbers. Every normal case, feature by feature, then every error case, feature by feature. A step's n runs through all cases; screenshots are named after it.
    • Each step holds n, case (its case's id), the page path (with attach: cdp, the window instead: a string its title or URL shows in tab-list), the action (one imperative sentence), the expected result (one sentence), and the data it needs. The path names a page or a window, nothing more. A state the app or its platform produces through on-screen operation (a filter, a selection, a sort order, a display mode) is reached by the operation that produces it, never written into the path by hand: as an earlier step of the case when the state lives in the page and is lost on opening the page again, in setup when the app keeps it across pages. When no operation can produce it, the path carries the state and the step's data says substituted path, so the gate shows it; with attach: cdp, a window path never carries such a state. The data is covered by the project's fixtures or seed as the data guide names them, created on screen by an earlier step, or inserted by a script written here in the way the data guide says, in that order of preference. A script's full text goes into the step table so the gate shows it. A step that creates a record on screen (a sign-up, a new item) fills its unique fields with values carrying <YYYYMMDD-HHMMSS> of now (for example an email address), so a second capture does not collide with the first. Nothing is cleaned up afterwards.
  4. Start the app. Write .demo/<slug>/changed-files.txt: the paths of the change set read in step 2 (Inputs), the changed files then the untracked ones, one per line, literal (an empty file when there are none). <port> is the port key resolved as § Settings says (a command's first stdout line must be an integer from 1 to 65535; a non-zero exit or any other line goes to the start-failure gate below, which then shows the command's output) or, when unset, the output of node -e 'const s=require("net").createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})'. Replace ${DEMO_PORT} in the reset_command, start_command and stop_command texts with <port>, then say in one line which reset_command and start_command are about to run. When reset_command is set, run <environment> <reset_command> in the foreground; a non-zero exit goes to the start-failure gate below, which then shows the reset output instead of server.log. Write the start_command text to .demo/<slug>/start-command (Read it first when an earlier run left it), then run <environment> node "<base dir>/scripts/run-open-stdin.mjs" .demo/<slug>/start-command > .demo/<slug>/server.log 2>&1 with run_in_background: true. Resolve the URL and wait for it in one call:

    • <environment> node "<base dir>/scripts/wait-ready.mjs" --timeout <ready_timeout> with the options below, passing timeout: (ready_timeout + 10) * 1000 on that Bash call. Any HTTP response counts as ready, with --expect any response holding its text.
    • With attach unset: --url "<value>" when the url key is a URL (its value expanded as § Settings says); when it is unset, the first http:// or https:// URL in server.log, else http://localhost:<port>.
    • With attach unset: --url-command '<value>' when the url key is a command, in single quotes so the shell does not expand $DEMO_PORT before the script runs; a ' inside the value becomes '\''.
    • With attach: cdp: --url "<origin>/json/list" --expect '"type": "page"', where <origin> is the url key when set (expanded as § Settings says, without a trailing /; a command value counts as unset and is said in one line), else http://localhost:<port>, so the app counts as ready once it lists a window.
    • stdout ready <url> → <url> is that value, or <origin> with attach: cdp. stdout timeout → the start-failure gate, which then also shows the script's stderr. Anything else (exit 2, a stdout of neither form, node itself failing) → the start-failure gate, which then shows the script's stdout and stderr.
    • With attach unset, <base url> is <url> without query or fragment, with / appended unless it already ends with /. A path's URL is <base url> followed by the path without its leading /. With attach: cdp, a path names a window, and no <base url> is defined.

    The start-failure gate (USER GATE): show the last 20 lines of server.log and <port>, and ask retry / skip. retry runs step 7 (Stop the app), then this step again from reset_command; there is no retry limit. skip runs step 7 and stops with status: "skipped", reason: "dev server not ready".

    Data check. Before any page is opened, a record the steps rely on that the seed or fixture files the data guide names, or another persistent store the project keeps (an exported dataset), do not hold is a gap. Then, once the URL answers, read the app in a separate session (playwright-cli -s=demo-<slug>-check), by mode. With attach unset: once per distinct setup (no setup counts as one): open, state-load <auth_state> when set, perform that setup (the data check runs no case_reset), then goto the URL of each distinct path of the cases that use it (read the snapshot it prints inline, or from the .playwright-cli/ file it names), then close; a setup that fails is noted on every case that uses it. With attach: cdp: attach --cdp=<url>, tab-list, then tab-select each distinct window the steps name that tab-list shows and read its snapshot, then close; no setup is performed, and neither a window not listed yet nor a window still before a setup its cases perform (a sign-in screen) is a gap. In both modes, a page that shows an error, a not-found, or an empty list where its step needs records is a gap too. For each gap, write an insertion script into the earliest step that needs the data, as in step 3. A page or window that cannot be reached, or that fails for a reason data cannot explain, is noted on its step for the gate.

  5. Step-table gate (USER GATE). Show <url>, whether reset_command ran, the features (letter, name, description), the test cases (id, kind, title, precondition, setup), the step table (number, case, path, action, expected result, data), one line per step whose data says substituted path naming the state its path carries, the error-case count line when there is one, the video value, every data script in full, and what the data check changed or noted, in the resolved language. Say in one sentence that revise can drop error cases and that error_cases: false leaves them out altogether; then say in one more sentence what runs before each case's setup and that a re-capture runs it again — with <case reset> command, the case_reset command verbatim; with none, nothing but loading auth_state; with default and attach: cdp, the built-in reset as the case_reset row defines it, said of the running app (in web mode default opens a fresh browser and needs no sentence). Ask approve / revise / skip, and in the same question set ask whether a test case that fails for missing data or for how its steps were written may be corrected — its scripts, its setup or its actions — and captured again, at most twice (yes / no); keep that answer as <recapture>. revise takes the user's corrections to the URL, the cases, the steps, the data or a script, applies them and shows the tables again. skip runs step 7 and stops with status: "skipped", reason: "declined at step table". After approve, the only questions left are the publish gate in step 6, only when failures remain, and step 9 (Data guide gate), only when it has something to offer.

  6. Capture. <session> = demo-<slug>; <capture prompt> = references/capture-prompt.md, or references/capture-prompt-cdp.md with attach: cdp. Before each dispatch, delete the recordings of every case it captures, in one node -e that lists .demo/<slug>/ with fs.readdirSync and removes with fs.rmSync every file named <id>.webm or <id>-<k>.webm. Dispatch one Agent (subagent_type: general-purpose, model per § Model, run_in_background: false); it reaches the app and records each case itself. The prompt is three fenced sections: --- TASK --- holds the text of <capture prompt> after its --- rule, verbatim, with {{session}}, {{out_dir}} (.demo/<slug>), {{url}} (<base url>, or <url> with attach: cdp), {{chapters}}, {{video}}, {{auth_state}} (auth_state, or none), {{language}} and {{case_reset}} filled in — the last with the <case reset> paragraph under the ## Case reset heading of <capture prompt>, without its **label**: prefix, with <reset line> = export DEMO_PORT=<port> DEMO_BASE_REF=<base> DEMO_CHANGED_FILES=<changed files> DEMO_SESSION=<session>; <case_reset> substituted where the paragraph names it; --- STEPS --- holds the cases to capture as one JSON object { "cases": [ { "id", "kind", "title", "precondition", "setup" } ], "steps": [ { "n", "case", "path", "action", "expected", "data", "script" } ] }; --- RETURN --- holds one sentence: the reply ends with a single fenced JSON block and nothing after it.

    Pre-invocation reminder: the agent's reply is a return value, not a turn boundary. The next tool call after it returns is playwright-cli -s=<session> close, issued by this same thread. See § No-Stall Principle.

    When the agent returns: run playwright-cli -s=<session> close (its output is ignored). Take the last fenced JSON block of the reply and parse it. No block, or a block that does not parse → dispatch once more with the sentence Your previous reply carried no parseable JSON block. appended to --- RETURN ---, then close again. A second failure on the first capture runs step 7 and stops with status: "error", reason: "no parseable capture result"; on a later capture it is handled as Later captures says. Schema check, before anything is written: steps is an array holding each n of the steps passed in this dispatch exactly once; each entry has a status of ok or failed, a non-empty string screenshot (a bare step-NN.png file name; a path is reduced to its base name), a non-empty string result, an optional string note, and, on a failed entry, an optional cause of data, steps or app. A violation on the first capture runs step 7 and stops with status: "error", reason: "capture schema violation"; on a later capture it is handled as Later captures says.

    Return-point no-stall reminder: regardless of outcome — every step ok, some failed, any non-error result — the next action, playwright-cli -s=<session> close, is issued in the next tool call. After the first capture and after a re-capture, the re-capture decision follows, or the publish gate when Later captures sends the run there; after a capture for the publish gate's revise, the publish gate follows. See § No-Stall Principle.

    Later captures. A later capture dispatches only the given cases and their steps, together with every case they rely on for data, with the app still running and no reset_command. Before it, the <YYYYMMDD-HHMMSS> in those cases' unique values is replaced with the current time. Its results replace those steps' earlier ones. A second unparseable reply or a schema violation keeps those cases' earlier results instead, and the run goes on to the publish gate.

    Re-capture. <recapture count> is 0 on the first entry to this step. When <recapture> is yes, <recapture count> is below 2, and a step is failed with cause data or steps (a steps failure whose note names the case_reset command is not re-captured): correct each case holding such a step — data: append an insertion script to the step's script; steps: read the step's note and the last 40 lines of server.log, then correct the case's setup, its actions, or the saved values it uses. Run a later capture of the corrected cases. When it returns a result that passes the schema check, add 1 to <recapture count> and decide again. When no re-capture is due (<recapture> is no, no data or steps failure is left, or <recapture count> is 2), go to the publish gate.

    Publish gate (USER GATE). When every step is ok, skip it and go to step 7 (Stop the app). Otherwise show, per test case with a failure, its id and title and each failed step's n, action, expected result, result, note, cause in words (missing data / how the steps were written / how the app behaved, in the resolved language) and screenshot path, and ask publish / revise / stop. publish goes to step 7 with the results as they are; the page shows those steps as not confirmed. revise takes the user's corrections to those cases, runs a later capture of only those cases, which does not count toward <recapture count>, and returns to this gate; there is no limit. stop runs step 7 and stops with status: "skipped", reason: "declined at publish gate".

  7. Stop the app. Full stop sequence (applies to every case), in this order:

    1. When stop_command is set, say in one line that it runs, then run <environment> <stop_command> in the foreground, passing timeout: (ready_timeout + 10) * 1000; a non-zero exit or a timeout keeps one line for the report, stop_command exited <code>: <reason> or stop_command timed out: <reason>, where <reason> is the last non-empty stderr line cut to 80 chars, or (no stderr), and the stop goes on.
    2. When step 4 has run scripts/run-open-stdin.mjs in this run and .demo/<slug>/start-command.pgid exists, Read the process group id <pgid> from it and run kill -- -<pgid>.
    3. Read the PIDs from lsof -ti tcp:<stop port>; when there are none, skip the kill. Otherwise run kill <pids>.
    4. Wait with node -e "setTimeout(()=>{},2000)".
    5. When <pgid> was read in item 2, run kill -9 -- -<pgid> (a non-zero exit means the group is already gone).
    6. Read lsof -ti tcp:<stop port> again and kill -9 what is left. If a PID still listens after that, keep one line for the report.

    The lines kept are those of the last run of this step. The three cases differ only in <stop port> and in one report line:

    • <url> is not resolved (the wait returned no ready): <stop port> is <port>; no further line.
    • <url>'s host is localhost, 127.0.0.1 or ::1: <stop port> is the port named in <url>; no further line.
    • any other host (a remote target): <stop port> is <port>. When stop_command is unset, keep one line for the report saying the environment start_command created was left in place; otherwise keep no such line.
  8. Render and publish. Write .demo/<slug>/steps.json: title (the change in two to four words), dek (the plan's After sentence, or one sentence from the diff), url, features (each approved feature that still has a case, with its letter, name and description), cases (each approved case's id, feature as its feature's position in that list, kind, title and precondition), and steps (each approved step with its case id, merged with the agent's status, result, note and screenshot). A case gets video: "<id>.webm" when that file exists under .demo/<slug>/ and is 15MB or smaller; check every case in one node -e that lists each .webm with its size. Run node "<base dir>/scripts/render.mjs" --steps .demo/<slug>/steps.json --out .demo/<slug>/index.html --lang <language>. A non-zero exit stops with status: "error", reason: "page rejected: <first stderr line>" (the app is already stopped). Call Artifact with file_path .demo/<slug>/index.html, files mapping each step-NN.png and each case video named in steps.json to its file under .demo/<slug>/, icon camera, and description one sentence naming the feature. When the files together exceed 64MB, publish again to the same URL with the remaining videos. Do not load the design skills: the page's layout and theming are fixed in the renderer. A missing or failing Artifact leaves url null; the page path still reports. Run git check-ignore .demo .playwright-cli; when its output lacks either path, keep one line for the report asking to add .demo/ and .playwright-cli/ to .gitignore (this skill does not edit it outside Setup mode).

  9. Data guide gate. <edited> is [] on entry. Build two candidates. <guide additions>: for each item § Settings lists for the data guide, what this run used that the guide does not state yet, one or two lines each, in the resolved language, without this run's own values (the timestamps, the records this run created); a line with a credential-like value (a password, a token, a key) carries where the value comes from instead of the value. <seed candidates>: the records the scripts of the normal cases' ok steps inserted, only when the guide names a seed file (the first, when it names several); draft the addition in that file's own idiom as a diff, from the seed file as read in step 3 (Read it when step 3 did not). When both candidates are empty, show no gate and go to step 10 (Report). Otherwise (USER GATE), in one question set, show the lines to append verbatim and ask yes / no, and show the seed diff and ask apply / decline, each only when its candidate exists. yes appends the lines at the end of the guide's write target with Edit. apply applies the diff to the seed file with Edit. Nothing is committed. A failing Edit is noted for the report and the run continues. <edited> holds the paths whose Edit succeeded.

  10. Report. In the resolved language: the artifact URL (or the page path), the test case count and how many test cases are not confirmed, one line per test case with its id, title and confirmed / not confirmed, how many re-captures ran, and for each case they corrected, every script added in full and its setup and actions as captured, the publish gate's answer when it was shown, each recording over 15MB left off the page, the stop result (the stop_command line when one was kept, and whether a process still listens), the gitignore line when there is one, the guide lines appended or that they were declined, and the seed file edited with a note that its change is uncommitted, or that it was declined. Then the return contract's JSON block.

Return contract

Emit a single fenced JSON block at the end of the response, matching:

{ "status": "ok" | "skipped" | "error", "reason": null | "<why>", "url": null | "<artifact url>", "page": null | "<path to index.html>", "cases": <n>, "steps": <n>, "failed": <n>, "edited": [ "<path>", ... ] }

cases, steps and failed are 0 when the capture did not run. failed counts steps. edited is always present: the paths § Procedure step 9 (Data guide gate) edited, [] on every path that does not reach it (Setup mode's own writes are reported by Setup mode, not here). Only one fenced JSON block appears in the response.

Sub-skill caller directive

When invoked as a sub-skill (i.e. via Skill(demo) from an orchestrator), the fenced JSON verdict block this skill emits is the structured return value of the skill's procedure — it is not a deliverable to the user, and emitting it does not terminate the orchestrator's turn. The same agent that ran this skill must immediately issue the next tool call dictated by the orchestrator's flow. Do not insert a prose summary, an acknowledgment, or a "shall I proceed?" sentence between the JSON verdict and the next tool call. Only one fenced JSON block — the verdict block — appears in the response, so callers can locate it unambiguously. The skill's own procedure is over; the orchestrator's procedure continues without pause.

Fallback

When the Agent tool is not on the tool surface, this thread runs the flow of <capture prompt> (§ Procedure step 6 (Capture)) itself, builds the same steps array, and continues at the schema check.