demo · git:20261020.7a2f6ca · 2026-10-20 · sha256 f065dbead2639353
demo git:20261020.7a2f6caA
Immutable. This exact content never changes and is served at /api/v1/blob/f065dbead2639353.
---
name: demo
description: Generate a demo of a feature developed in this session, in a web app, in a Chromium-based desktop app (Electron), or in a native mobile app on an iOS simulator or Android emulator — 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, or by agent-device for a mobile app, against the app the skill starts and stops itself. Use from dev-workflow's Update Rules gate (demo / no-demo) or standalone with /demo.
allowed-tools: Read, Write, Edit, Glob, Grep, Agent, Artifact, AskUserQuestion, Bash(playwright-cli *), Bash(agent-device *), Bash(node *), Bash(mkdir -p .demo/*), Bash(git check-ignore *), Bash(git diff *), Bash(git ls-files *), Bash(lsof -ti tcp:*), Bash(kill *), Bash(DEMO_PORT=* pnpm *), Bash(DEMO_PORT=* npm *), Bash(DEMO_PORT=* yarn *), Bash(DEMO_PORT=* bun *), Bash(DEMO_PORT=* make *), Bash(DEMO_PORT=* bundle exec *), Bash(DEMO_PORT=* rails *), Bash(DEMO_PORT=* python *), Bash(DEMO_PORT=* uv run *), Bash(DEMO_PORT=* docker compose *), Bash(DEMO_PORT=* go run *), Bash(DEMO_PORT=* cargo run *), Bash(DEMO_PORT=* node *)
---
# demo
```text
/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`); `agent-device --version` exiting non-zero (`agent-device not installed`); a settings file that exists but cannot be read (`settings unreadable`); with `<target>` `web` or `cdp`, no `start_command` after the merge (`start_command unset`); a mobile `target` without `app` (`incomplete settings`); `cancel` at the candidate gate or the Setup gate (`setup declined`); `--init` with a settings file present (`settings exist`); `.claude/demo.md`, or `.claude/demo.local.md` when an account section is written, 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 recording returned in several files (only the first is on the page), 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 `<target>` `cdp` or mobile, the guide also says what state the app keeps outside what the driver resets (a user data directory, the app's storage on the device) and how a run starts with it fresh (a throwaway user data directory in `start_command`, a `reset_command`, a `case_reset` command that clears it), 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.
**Target.** `<target>` is the resolved `target` value, after the `attach` alias; every "with `<target>` `X`" below refers to it, not to the key as written in the file. **mobile** names `ios` and `android` together. `<app>` is the `app` key's value; `<device>` is the `device` key's value, or nothing; `<open line>` is `open <app> --platform <target> <device>`, run through `agent-device`; `<ext>`, the recording extension, is `webm` with `web` or `cdp` and `mp4` with mobile. With mobile and no `app` → `status: "error"`, `reason: "incomplete settings: app is required with target ios or android"`, before anything is started. Keys that have no meaning with the resolved target (`url` and `auth_state` with mobile; `app` and `device` with `web` or `cdp`) are said in one line when set and ignored.
| Key | Default | Meaning |
|---|---|---|
| `target` | `web` | `web`: a browser the run opens. `cdp`: 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. `ios` / `android` (mobile): a native app on a simulator or emulator of that platform, driven with `agent-device` (`app` required). Any other value is treated as `web` and said in one line |
| `attach` | none | `cdp`, with `target` unset: the same as `target: cdp`. Otherwise ignored |
| `app` | none | The app's bundle identifier (iOS) or package name (Android) that `agent-device open` takes; required with mobile |
| `device` | none | With mobile: the device selector `agent-device` takes, verbatim (`--udid <id>`, `--device <name>` or `--serial <id>`), appended to every `agent-device boot` and `open`. Unset: `agent-device` picks a booted device of that platform and fails when several match |
| `start_command` | required with `web` or `cdp` | 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`. Optional with mobile |
| `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, or builds and installs a mobile app. Bounded by `ready_timeout` seconds |
| `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 (with mobile, before the app is opened). Unset: the built-in reset, by `<target>` — `web`: nothing beyond the fresh browser; `cdp`: every window's storage and cookies cleared and the window reloaded; mobile: `<open line> --relaunch` (the app restarted, its data kept). `none`: nothing (with mobile: `<open line>` without `--relaunch`); with `target` `web` 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, or the `agent-device` session name with mobile) exported; a non-zero exit counts as a failed `setup` action |
| `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 `<target>` `cdp`, only the URL form applies. No meaning with mobile |
| `ready_timeout` | `120` | Seconds to wait for the URL to resolve and answer, or for `agent-device boot` and `open` to return, 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. With mobile it only expands `${DEMO_PORT}` and names what to stop |
| `auth_state` | none | Storage state file passed to `playwright-cli state-load` after the browser opens or, with `<target>` `cdp`, after attaching and before the case's `setup`; over CDP the file's cookies are loaded, not its localStorage. No meaning with mobile |
| `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`); with mobile the prefix also carries `DEMO_APP=<app> DEMO_TARGET=<target>` after `DEMO_CHANGED_FILES`. `<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. With `<target>` `web` or `cdp`, 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` 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.** Resolve the language per § Language and the model per § Model, then the settings per § Settings (Setup mode when neither file exists). Then check the driver, by `<target>`:
- `web` and `cdp`: 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.
- mobile: run `agent-device --version`. A non-zero exit stops with `status: "error"`, `reason: "agent-device not installed"`.
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 `<target>` `cdp`, each action starts with the window it runs in as a step's `path` names one; with mobile, each action names the screen it runs on), 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 or screens counts as data an earlier case created. A case that needs a signed-in user gets there through `auth_state` when it is set (not with mobile), 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 `<target>` `cdp`, the window instead: a string its title or URL shows in `tab-list`; with mobile, the screen instead: the heading or a label `snapshot -i` shows on it), the action (one imperative sentence), the expected result (one sentence), and the data it needs. The path names a page, a window or a screen, 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 `<target>` `cdp` or mobile, a window or screen 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 (with mobile and `start_command` unset: that nothing is started). With mobile, run `agent-device boot --platform <target> <device> --timeout <ready_timeout * 1000>` first, passing `timeout: (ready_timeout + 10) * 1000` on that Bash call; a non-zero exit goes to the start-failure gate below, which then shows its output. When `reset_command` is set, run `<environment> <reset_command>` in the foreground, passing `timeout: (ready_timeout + 10) * 1000` on that Bash call; a non-zero exit goes to the start-failure gate below, which then shows the reset output instead of `server.log`. When `start_command` is set, `Write` its 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`.
**Readiness, by `<target>`.** With mobile: run `agent-device --session demo-<slug>-check <open line> --relaunch --timeout <ready_timeout * 1000>`, passing `timeout: (ready_timeout + 10) * 1000` on that Bash call. A zero exit makes the app ready; neither `<url>` nor `<base url>` is defined. A non-zero exit goes to the start-failure gate below, which then shows the command's output and, when `start_command` was started, the last 20 lines of `server.log`. With `<target>` `web` or `cdp`: 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 `<target>` `web`: `--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 `<target>` `web`: `--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 `<target>` `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 `<target>` `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 `<target>` `web`, `<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 `<target>` `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 the boot (with mobile) or `reset_command`; there is no retry limit. `skip` runs step 7 and stops with `status: "skipped"`, `reason: "dev server not ready"`.
**Data check.** Before anything 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 app is ready, read it in a separate session (`playwright-cli -s=demo-<slug>-check`, or with mobile the `agent-device` session `demo-<slug>-check` the readiness opened), by `<target>`. With `<target>` `web`: 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 `<target>` `cdp` or mobile, no `setup` is performed, and a window or screen still before a `setup` its cases perform (a sign-in screen) is not a gap; `cdp`: `attach --cdp=<url>`, `tab-list`, then `tab-select` each distinct window the steps name that `tab-list` shows and read its snapshot (a window not listed yet is not a gap either), then `close`; mobile: `snapshot -i` and read the first screen, then `close`, in one command line joined with `;`. With every `<target>`, 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, window or screen 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>` (with mobile, `<target>` and `<app>`), 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 mobile, followed by the app restarted with `--relaunch`); with `none`, nothing but loading `auth_state` (with mobile, the app opened again without `--relaunch`, as the `case_reset` row defines it); with `default` and `<target>` `cdp` or mobile, the built-in reset as the `case_reset` row defines it, said of the running app (with `default` and `<target>` `web`, a fresh browser opens and no sentence is needed). 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 (or the app), 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>` by `<target>`: `web`: `references/capture-prompt.md`; `cdp`: `references/capture-prompt-cdp.md`; mobile: `references/capture-prompt-mobile.md`. 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 whose name starts with `<id>.` or `<id>-`. `<close>` is `playwright-cli -s=<session> close`, or `agent-device --session <session> close` with mobile. 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 the placeholders it uses filled in: `{{session}}` (`<session>`), `{{out_dir}}` (`.demo/<slug>`), `{{url}}` (`<base url>`, or `<url>` with `<target>` `cdp`), `{{chapters}}`, `{{video}}`, `{{auth_state}}` (`auth_state`, or `none`), `{{app}}` (`<app>`), `{{target}}` (`<target>`), `{{device}}` (`<device>`), `{{language}}` and `{{case_reset}}` — the last with the `<case reset>` paragraph under the `## Case reset` heading of `<capture prompt>`, without its `**label**: ` prefix, with `<reset line>` = `export <the variable assignments of <environment>> 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 `<close>`, issued by this same thread. See § No-Stall Principle.
When the agent returns: run `<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, `<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:
0. With mobile, run `agent-device --session demo-<slug> close; agent-device --session demo-<slug>-check close` as one command line (its output is ignored; the simulator or emulator is left running).
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 cases differ only in `<stop port>` and in one report line:
- `<url>` is not resolved (the wait returned no `ready`, or mobile): `<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` (`<url>`; omitted with mobile), `target` (`<target>`), `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>.<ext>"` when that file exists under `.demo/<slug>/` and is 15MB or smaller; check every case in one `node -e` that lists each `.<ext>` 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, each case whose recording came back in several files (the capture agent notes it on the case's first step; only the first file is on 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:
```json
{ "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.