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.
-
Preconditions. Run
playwright-cli --help video-chapter. A non-zero exit stops withstatus: "error",reason: "playwright-cli not installed". When its output starts withplaywright-cli video-chapter,<chapters>=true; otherwise<chapters>=falseand 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). -
Inputs. With
Plan:,<slug>is the file name without.md;Readthe plan. When thatReadfails, 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. WithoutPlan:,<slug>is<YYYYMMDD-HHMMSS>of now. Read the change set againstBase ref(defaultHEAD):git diff <base>and the content of filesgit ls-files --others --exclude-standardlists. No readable plan and an empty change set →status: "error",reason: "nothing to demo". Runmkdir -p .demo/<slug>. -
Derive the test cases. From the data guide (read with the settings in step 1),
Readthe parts of the seed or fixture files it names that name accounts and records; when it names none, find them asreferences/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 (
Afor the first,Bfor 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
setupactions that bring the app to that precondition (from where § Procedure step 3's "Preconditions" paragraph says it starts; withattach: cdp, each action starts with the window it runs in as a step'spathnames 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_casesistrue. 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 thecase_resetrow defines leaves it; withnone, where the previous case left it, so itssetupfirst 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; withcommand, 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 throughauth_statewhen it is set, otherwise throughsetupactions 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 stepdatasayssave as <name>; each later step that needs it saysuse <name>in itsdata. - Order and numbers. Every normal case, feature by feature, then every error case, feature by feature. A step's
nruns through all cases; screenshots are named after it. - Each step holds
n,case(its case's id), the page path (withattach: cdp, the window instead: a string its title or URL shows intab-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, insetupwhen the app keeps it across pages. When no operation can produce it, the path carries the state and the step's data sayssubstituted path, so the gate shows it; withattach: 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.
- 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 (
-
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 theportkey 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 ofnode -e 'const s=require("net").createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})'. Replace${DEMO_PORT}in thereset_command,start_commandandstop_commandtexts with<port>, then say in one line whichreset_commandandstart_commandare about to run. Whenreset_commandis 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 ofserver.log.Writethestart_commandtext to.demo/<slug>/start-command(Readit 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>&1withrun_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, passingtimeout: (ready_timeout + 10) * 1000on that Bash call. Any HTTP response counts as ready, with--expectany response holding its text.- With
attachunset:--url "<value>"when theurlkey is a URL (its value expanded as § Settings says); when it is unset, the firsthttp://orhttps://URL inserver.log, elsehttp://localhost:<port>. - With
attachunset:--url-command '<value>'when theurlkey is a command, in single quotes so the shell does not expand$DEMO_PORTbefore the script runs; a'inside the value becomes'\''. - With
attach: cdp:--url "<origin>/json/list" --expect '"type": "page"', where<origin>is theurlkey when set (expanded as § Settings says, without a trailing/; a command value counts as unset and is said in one line), elsehttp://localhost:<port>, so the app counts as ready once it lists a window. - stdout
ready <url>→<url>is that value, or<origin>withattach: cdp. stdouttimeout→ 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
attachunset,<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/. Withattach: 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.logand<port>, and askretry/skip.retryruns step 7 (Stop the app), then this step again fromreset_command; there is no retry limit.skipruns step 7 and stops withstatus: "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. Withattachunset: once per distinctsetup(nosetupcounts as one):open,state-load <auth_state>when set, perform thatsetup(the data check runs nocase_reset), thengotothe 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), thenclose; asetupthat fails is noted on every case that uses it. Withattach: cdp:attach --cdp=<url>,tab-list, thentab-selecteach distinct window the steps name thattab-listshows and read its snapshot, thenclose; nosetupis performed, and neither a window not listed yet nor a window still before asetupits 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. -
Step-table gate (USER GATE). Show
<url>, whetherreset_commandran, 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 sayssubstituted pathnaming the state its path carries, the error-case count line when there is one, thevideovalue, every data script in full, and what the data check changed or noted, in the resolved language. Say in one sentence thatrevisecan drop error cases and thaterror_cases: falseleaves them out altogether; then say in one more sentence what runs before each case'ssetupand that a re-capture runs it again — with<case reset>command, thecase_resetcommand verbatim; withnone, nothing but loadingauth_state; withdefaultandattach: cdp, the built-in reset as thecase_resetrow defines it, said of the running app (in web modedefaultopens a fresh browser and needs no sentence). Askapprove/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, itssetupor its actions — and captured again, at most twice (yes/no); keep that answer as<recapture>.revisetakes the user's corrections to the URL, the cases, the steps, the data or a script, applies them and shows the tables again.skipruns step 7 and stops withstatus: "skipped",reason: "declined at step table". Afterapprove, 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. -
Capture.
<session>=demo-<slug>;<capture prompt>=references/capture-prompt.md, orreferences/capture-prompt-cdp.mdwithattach: cdp. Before each dispatch, delete the recordings of every case it captures, in onenode -ethat lists.demo/<slug>/withfs.readdirSyncand removes withfs.rmSyncevery file named<id>.webmor<id>-<k>.webm. Dispatch oneAgent(subagent_type: general-purpose,modelper § 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>withattach: cdp),{{chapters}},{{video}},{{auth_state}}(auth_state, ornone),{{language}}and{{case_reset}}filled in — the last with the<case reset>paragraph under the## Case resetheading 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 sentenceYour previous reply carried no parseable JSON block.appended to--- RETURN ---, thencloseagain. A second failure on the first capture runs step 7 and stops withstatus: "error",reason: "no parseable capture result"; on a later capture it is handled as Later captures says. Schema check, before anything is written:stepsis an array holding eachnof the steps passed in this dispatch exactly once; each entry has astatusofokorfailed, a non-empty stringscreenshot(a barestep-NN.pngfile name; a path is reduced to its base name), a non-empty stringresult, an optional stringnote, and, on afailedentry, an optionalcauseofdata,stepsorapp. A violation on the first capture runs step 7 and stops withstatus: "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, somefailed, 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'srevise, 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>is0on the first entry to this step. When<recapture>isyes,<recapture count>is below2, and a step isfailedwithcausedataorsteps(astepsfailure whosenotenames thecase_resetcommand is not re-captured): correct each case holding such a step —data: append an insertion script to the step'sscript;steps: read the step'snoteand the last 40 lines ofserver.log, then correct the case'ssetup, 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, add1to<recapture count>and decide again. When no re-capture is due (<recapture>isno, nodataorstepsfailure is left, or<recapture count>is2), 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'sn, 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 askpublish/revise/stop.publishgoes to step 7 with the results as they are; the page shows those steps as not confirmed.revisetakes 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.stopruns step 7 and stops withstatus: "skipped",reason: "declined at publish gate". -
Stop the app. Full stop sequence (applies to every case), in this order:
- When
stop_commandis set, say in one line that it runs, then run<environment> <stop_command>in the foreground, passingtimeout: (ready_timeout + 10) * 1000; a non-zero exit or a timeout keeps one line for the report,stop_command exited <code>: <reason>orstop_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. - When step 4 has run
scripts/run-open-stdin.mjsin this run and.demo/<slug>/start-command.pgidexists,Readthe process group id<pgid>from it and runkill -- -<pgid>. - Read the PIDs from
lsof -ti tcp:<stop port>; when there are none, skip the kill. Otherwise runkill <pids>. - Wait with
node -e "setTimeout(()=>{},2000)". - When
<pgid>was read in item 2, runkill -9 -- -<pgid>(a non-zero exit means the group is already gone). - Read
lsof -ti tcp:<stop port>again andkill -9what 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 noready):<stop port>is<port>; no further line.<url>'s host islocalhost,127.0.0.1or::1:<stop port>is the port named in<url>; no further line.- any other host (a remote target):
<stop port>is<port>. Whenstop_commandis unset, keep one line for the report saying the environmentstart_commandcreated was left in place; otherwise keep no such line.
- When
-
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 itsletter,nameanddescription),cases(each approved case'sid,featureas its feature's position in that list,kind,titleandprecondition), andsteps(each approved step with itscaseid, merged with the agent'sstatus,result,noteandscreenshot). A case getsvideo: "<id>.webm"when that file exists under.demo/<slug>/and is 15MB or smaller; check every case in onenode -ethat lists each.webmwith its size. Runnode "<base dir>/scripts/render.mjs" --steps .demo/<slug>/steps.json --out .demo/<slug>/index.html --lang <language>. A non-zero exit stops withstatus: "error",reason: "page rejected: <first stderr line>"(the app is already stopped). CallArtifactwithfile_path.demo/<slug>/index.html,filesmapping eachstep-NN.pngand each case video named insteps.jsonto its file under.demo/<slug>/,iconcamera, anddescriptionone 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 failingArtifactleavesurlnull; the page path still reports. Rungit 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). -
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'oksteps 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 (Readit 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 askyes/no, and show the seed diff and askapply/decline, each only when its candidate exists.yesappends the lines at the end of the guide's write target withEdit.applyapplies the diff to the seed file withEdit. Nothing is committed. A failingEditis noted for the report and the run continues.<edited>holds the paths whoseEditsucceeded. -
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
setupand actions as captured, the publish gate's answer when it was shown, each recording over 15MB left off the page, the stop result (thestop_commandline 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.