diff --git a/catalog/archdev.json b/catalog/archdev.json index 6adb6de..4f73ebc 100644 --- a/catalog/archdev.json +++ b/catalog/archdev.json @@ -10,12 +10,13 @@ "homepage": "https://archdev.ai", "repository": "https://github.com/ArchAstro/archdev", "license": "MIT", - "keywords": ["archdev", "code-review", "tasks", "activity", "mcp"], + "keywords": ["archdev", "code-review", "tasks", "activity", "mcp", "screenshots"], "category": "Developer Tools", "skills": [ { "name": "archdev", "path": "archdev" }, { "name": "tasks", "path": "tasks" }, - { "name": "tasks-execute", "path": "tasks-execute" } + { "name": "tasks-execute", "path": "tasks-execute" }, + { "name": "shots", "path": "shots" } ], "mcp": { "name": "archdev", diff --git a/plugins/antigravity/skills/shots/SKILL.md b/plugins/antigravity/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/antigravity/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/claude/.claude-plugin/plugin.json b/plugins/claude/.claude-plugin/plugin.json index 955a33e..135391f 100644 --- a/plugins/claude/.claude-plugin/plugin.json +++ b/plugins/claude/.claude-plugin/plugin.json @@ -15,6 +15,7 @@ "code-review", "tasks", "activity", - "mcp" + "mcp", + "screenshots" ] } diff --git a/plugins/claude/skills/shots/SKILL.md b/plugins/claude/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/claude/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/codex/.codex-plugin/plugin.json b/plugins/codex/.codex-plugin/plugin.json index 5eb8c98..2daec4b 100644 --- a/plugins/codex/.codex-plugin/plugin.json +++ b/plugins/codex/.codex-plugin/plugin.json @@ -7,7 +7,8 @@ "code-review", "tasks", "activity", - "mcp" + "mcp", + "screenshots" ], "skills": "./skills/", "mcpServers": "./mcp.json", diff --git a/plugins/codex/skills/shots/SKILL.md b/plugins/codex/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/codex/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/copilot/plugin.json b/plugins/copilot/plugin.json index 6d476c8..6481798 100644 --- a/plugins/copilot/plugin.json +++ b/plugins/copilot/plugin.json @@ -15,6 +15,7 @@ "code-review", "tasks", "activity", - "mcp" + "mcp", + "screenshots" ] } diff --git a/plugins/copilot/skills/shots/SKILL.md b/plugins/copilot/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/copilot/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/cursor/.cursor-plugin/plugin.json b/plugins/cursor/.cursor-plugin/plugin.json index 094e0d0..28dea06 100644 --- a/plugins/cursor/.cursor-plugin/plugin.json +++ b/plugins/cursor/.cursor-plugin/plugin.json @@ -14,7 +14,8 @@ "code-review", "tasks", "activity", - "mcp" + "mcp", + "screenshots" ], "skills": "./skills/", "mcpServers": "./mcp.json" diff --git a/plugins/cursor/skills/shots/SKILL.md b/plugins/cursor/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/cursor/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/grok/.claude-plugin/plugin.json b/plugins/grok/.claude-plugin/plugin.json index 955a33e..135391f 100644 --- a/plugins/grok/.claude-plugin/plugin.json +++ b/plugins/grok/.claude-plugin/plugin.json @@ -15,6 +15,7 @@ "code-review", "tasks", "activity", - "mcp" + "mcp", + "screenshots" ] } diff --git a/plugins/grok/skills/shots/SKILL.md b/plugins/grok/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/grok/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/opencode/package.json b/plugins/opencode/package.json index 14af4cf..24fa882 100644 --- a/plugins/opencode/package.json +++ b/plugins/opencode/package.json @@ -17,6 +17,7 @@ "tasks", "activity", "mcp", + "screenshots", "opencode", "opencode-plugin" ], diff --git a/plugins/opencode/skills/shots/SKILL.md b/plugins/opencode/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/opencode/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/plugins/pi/package.json b/plugins/pi/package.json index 5557992..3033ac4 100644 --- a/plugins/pi/package.json +++ b/plugins/pi/package.json @@ -8,6 +8,7 @@ "tasks", "activity", "mcp", + "screenshots", "pi-package" ], "homepage": "https://archdev.ai", diff --git a/plugins/pi/skills/shots/SKILL.md b/plugins/pi/skills/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/plugins/pi/skills/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol. diff --git a/shots/SKILL.md b/shots/SKILL.md new file mode 100644 index 0000000..c454009 --- /dev/null +++ b/shots/SKILL.md @@ -0,0 +1,579 @@ +--- +name: shots +description: Use when an agent should capture, publish or read screenshots and movies with `archdev shots` — checking whether Shots is on, publishing stills of UI work into `.astroshot//`, rendering React, Ink or PTY fixtures to PNG, recording browser, terminal, macOS window or frame-sequence movies, reading a person's Astroshots review comments (`review.json`), uploading a run for web review, or authoring and reading user stories. Triggers include "take a screenshot", "capture shots", "record a movie of this flow", "what did the reviewer say", ".astroshot", "user story", "upload the shots", and the "ArchDev shots" hook reminder. +--- + +# Shots + +`archdev shots` turns UI work into reviewable evidence. A capture writes a PNG +(or a poster PNG plus a video) into `.astroshot//` at the worktree +root and records it in that directory's `manifest.json`. A person reviews the +frames in the Astroshots app or in `archdev shots review`, and their decisions +and comments land in `review.json` next to the images. You read that feedback +back with `archdev shots status`. + +Everything here needs ArchDev 0.52.1 or newer. `archdev shots --help` lists +the commands; every subcommand has its own `--help`, and the CLI is the +authority when it disagrees with this file. + +## Activity and risk ownership + +Follow the `archdev` skill for activity reporting. Plan/task lifecycle events +retain their `risk.plan` / `risk.task` assessments. Server automation owns hosted +PR and code-region grading: report PR events without fetching, waiting for, +reusing, or attaching a server grade. No local assessment, annotation or stop +hold is required. + +## 1. Check that Shots is on before anything else + +Shots is off by default, and the setting belongs to the person, not to you. +Every `archdev shots` command except `doctor` and `--help` refuses while it is +off, exits 1 and prints: + +```text +archdev shots is off. Enable it with: archdev settings set shots on +``` + +Check first: + +```sh +archdev shots doctor +``` + +`doctor` is read-only: it starts no browser and writes nothing. Its first line +is `archdev shots is on` or the off message above, and it exits 0 only when +Shots is on. The other lines say where it found the tools each mode needs: + +| Tool | Needed for | +|---|---| +| Chrome | React stills and browser movies; also PTY and frames movies when ffmpeg is missing | +| Node (22 or newer) and the Node helper | React and Ink stills, and browser movie scripts | +| ffmpeg | encoding movies (optional: without it Chrome encodes them, always as WebM) | +| ffprobe | movie metadata | + +A missing tool only blocks the modes that need it. `archdev shots doctor --json` +gives the same report as JSON (`setting`, then one object per tool with +`path` and `needed_for`). + +`doctor` does not check the Node helper's npm packages, and the helper the +CLI ships has none installed. React and Ink stills and `movie run --script` +fail with `astroshot node helper could not load "tsx/esm/api"` (or `"vite"`, +`"playwright-core"`) until those packages sit next to a helper copy. See +"Node helper packages" under Troubleshooting. PTY stills, captures and +movies without `--script` do not use them. + +`archdev settings get` shows `Shots: on (config)` and `Shots upload: auto +(config)` rows once the person has set them; before that it shows no Shots +rows, which means both defaults (off, manual). + +### When Shots is off + +Ask the person to turn it on. Tell them to run this in their own terminal, +outside the agent session: + +```sh +archdev settings set shots on +``` + +Do not try to enable it yourself. `archdev settings set shots …` and +`archdev settings set shots-upload …` refuse inside an agent session or +without an interactive terminal ("Shots settings require a human terminal +outside an agent session"), and that includes commands a harness runs for +the person from the chat (such as Claude Code's `!` prefix). Do not edit the +ArchDev config file, set a different `ARCHDEV_PROFILE` or `--profile`, or +otherwise route around the refusal: the setting is the person's consent to +have screenshots taken. If they decline, carry on without captures and say +which evidence is missing. + +The setting is stored per ArchDev profile. If the session runs with +`ARCHDEV_PROFILE=` or `--profile `, the person must enable that +profile: `archdev --profile settings set shots on`. The off message +does not include the profile name. + +### Upload preference + +`archdev settings set shots-upload auto|manual` (default `manual`) is the +person's preference for sending a session's captures to ArchDev for web +review. It has the same human-terminal rule. With `manual`, upload only when +the person asks (section 6). + +### Unattended harnesses + +A test harness that runs without a person at the keyboard can keep its own +profile with Shots on, for example a profile named `astroshot-harness` whose +`config.json` the harness writes with `{"shots":{"enabled":"on"}}`, and run +`ARCHDEV_PROFILE=astroshot-harness archdev shots capture …`. That pattern is +for harness code a team has reviewed and committed. It is not a way to +capture in an interactive session where the person has not turned Shots on. + +## 2. Publish stills + +`archdev shots capture` publishes an existing image (PNG, JPEG, WebP or GIF), +or takes a PNG from an `agent-browser` session, into `.astroshot//`. +It prints the path it wrote. + +```sh +archdev shots capture --feature account-settings --slug dialog \ + --title "Account dialog" \ + --description "Name and email fields are editable." \ + --source ./account-dialog.png + +archdev shots capture --feature account-settings --slug saved \ + --title "Saved" \ + --description "The success toast is visible after saving." \ + --url /account --viewport 1280x800 \ + --from-agent-browser "$SESSION" +``` + +- `--feature` is kebab-case (`[a-z0-9-]+`). `stories` and `friction-logs` are + reserved for user stories and are refused. +- `--slug` is `[a-z0-9][a-z0-9_-]*`. Files are numbered: `0001-dialog.png`, + `0002-saved.png`. Numbering continues from the highest file already in the + directory. +- `--source PATH` and `--from-agent-browser SESSION` are mutually exclusive. + The browser form runs `agent-browser --session SESSION screenshot`; it takes + a full-page screenshot unless you pass `--no-full` (viewport only) or + `--selector SEL` (one element). `agent-browser` must be on `PATH`, and a + session name that does not exist yet starts a new, blank session, so pass + the name of the session you have been driving. +- The image is validated before anything is written: bytes that do not match + the file extension fail with `not a valid . image: …` and leave the + directory unchanged. The published file keeps the source's extension. +- `--title` defaults to the slug in title case (`saved` becomes "Saved"); + `--description` should say what the frame + proves. `--url` and `--viewport` are free-form context shown to the + reviewer. +- `--root DIR` overrides the worktree root. By default the root is the Git + top level of the current directory (or the current directory outside Git). + +### Runs + +`manifest.json` holds one run at a time: a `run_id`, a `status` and the +`shots` captured in that run. + +- A capture with the default `--status running` appends to the current run + while that run's status is `running`. +- `--status pass`, `fail` or `idle` on a capture appends that shot and closes + the run. To close a run without a new shot: + + ```sh + archdev shots capture --feature account-settings --status pass --finalize + ``` + + The `--status` is what closes the run: `--finalize` without it sets the + status to `running`, exits 0 and leaves the run open. `--finalize` refuses + `--source` and `--from-agent-browser`. +- The next capture after a closed run starts a new run with a fresh run id. + Earlier images stay on disk. +- `--run-id ID` pins the run: captures with the same id append to it (and + reopen it if it was closed). `--finalize --run-id ID` refuses when the + manifest's run is a different one (`manifest run_id "…" does not match + "…"`). A multi-step harness should pass one `--run-id` to every capture + and to the final `--finalize`. +- A capture with a different `--run-id` replaces the current run, even one + still `running`, without a warning. Two harnesses must not share a + feature name. + +Read review comments (section 5) before you close a run or start a new one. +Comments are scoped to the run they were written in, so once a new run +starts, `archdev shots status` no longer shows them. + +`pass` and `fail` describe whether your journey ran, never whether a person +looked at it. Review state lives only in `review.json` (section 5). + +Captures in one feature directory are serialized by a lock directory, +`.capture.lock`. See Troubleshooting for lock errors. + +## 3. Render fixtures to PNG + +Use a fixture when a component or terminal state can be reproduced without +running the whole application. Pick the smallest boundary that shows the +state: + +| State to show | Command | +|---|---| +| A React component with fixed props | `archdev shots react` | +| An Ink component tree in a fixed state | `archdev shots ink` | +| A real terminal program and its keyboard flow | `archdev shots pty` | +| Routing, sign-in, live data, the whole app shell | drive the app with `agent-browser`, then `archdev shots capture --from-agent-browser` | + +Each takes the fixture path and either `-o, --out PATH` (write a PNG and +print `wrote PATH`) or `--feature F --slug S` (render and publish into +`.astroshot/F/` in one step, with optional `--title` and `--description`). +Fixture rendering always uses the Git top level as the root and appends to +the feature's current run; use `-o` plus `archdev shots capture` when you +need `--run-id`, `--status` or `--root`. + +Fixtures, their imports and PTY commands run with your permissions. A +temporary browser or pseudoterminal is not a sandbox, so review fixtures you +did not write before rendering them. + +### React + +Needs Chrome, Node and the helper packages (see section 1), plus `react` and +`react-dom` in the fixture's project. The fixture default-exports the +component and its capture settings: + +```tsx +import { AccountDialog } from "../src/AccountDialog"; + +export default { + width: 960, + height: 900, + background: "transparent", + selector: "[role=dialog]", + waitFor: "text=Save", + settleMs: 200, + component: {}} />, +}; +``` + +Settings: `width` and `height` (viewport, default 1280×800), `background`, +`selector` (element to capture), `waitFor` (selector or `text=…` that must be +visible), `settleMs`, `fullPage`, `stripOverlay`, `omitBackground`. + +```sh +archdev shots react fixtures/account-dialog.tsx -o account-dialog.png +archdev shots react fixtures/account-dialog.tsx --feature account-settings --slug dialog --title "Account dialog" +``` + +Import the production component, give it synthetic props, and mock network +and server-only modules. For aliases, global styles, deduplicated packages +and stubbed modules, put a `react-shot.config.ts` (or `.mts`, `.js`, `.mjs`) +next to the app package exporting `root`, `alias`, `styles`, +`postcssConfig`, `dedupe` and `stubModules`. `--root` picks the package root +when discovery is ambiguous, `--config` names the config file, `--width` and +`--height` override the fixture, and `--headed` shows the browser. + +Several fixtures at once, from a YAML manifest: + +```yaml +shots: + - fixture: fixtures/account-dialog.tsx + out: screenshots/account-dialog.png + - fixture: fixtures/empty-state.tsx + out: screenshots/empty-state.png + width: 640 +``` + +```sh +archdev shots react batch shots.yaml +``` + +`fixture` and `out` are relative to the manifest's directory. Batch output +goes to the `out` paths only; publish the results with `archdev shots +capture --source` if they belong in the review stream. + +### Ink + +Needs Node and the helper packages (see section 1), and `ink` 7 and `react` +19 installed in the fixture's project. That project's `package.json` needs +`"type": "module"`; otherwise rendering fails with `Top-level await is +currently not supported with the "cjs" output format`. + +```tsx +import React from "react"; +import { Box, Text } from "ink"; + +export default { + cols: 72, + rows: 18, + expectText: ["Install agent", "Continue"], + component: ( + + Install agent + Continue + + ), +}; +``` + +```sh +archdev shots ink fixtures/install-wizard.tsx -o install-wizard.png +archdev shots ink batch terminal-shots.yaml --out-dir screenshots +``` + +`--cols`, `--rows` and `--scale` (up to 4) override the fixture. The batch +manifest has the same `shots: [{fixture, out}]` shape as React's. + +### PTY + +Runs a real program in a pseudoterminal. Build the program first. The +fixture is YAML (or JSON with a `.json` extension): + +```yaml +version: 1 +command: ./target/debug/install-wizard +args: [] +cwd: .. +cols: 100 +rows: 30 +timeoutMs: 15000 +actions: + - waitFor: Choose an option + - key: down + - key: enter + - waitFor: Ready +expectText: + - Ready +``` + +```sh +archdev shots pty fixtures/install-wizard.yaml -o install-wizard.png +archdev shots pty fixtures/install-wizard.yaml --feature install-wizard --slug ready --title "Ready" +``` + +- `command` runs directly, without a shell; `cwd` is relative to the fixture. + On Windows, point at the `.exe`: `.cmd` and `.bat` need a shell. +- Actions: `waitFor` (optional `timeoutMs`), `waitForExit: true`, `key` + (`enter`, `up`, `down`, `left`, `right`, `tab`, `escape`, `backspace`, + `space`, `ctrl-c`, `ctrl-d`), `text` (typed literally) and `pauseMs`. + Prefer `waitFor` over pauses, and end with distinctive `expectText`. +- A nonzero exit fails the capture unless the fixture sets + `allowNonZeroExit: true`. `env` adds environment variables. +- Mouse input, resizing mid-run and Sixel graphics are not modelled. + +Open every PNG you produce and check it before you publish or report it: +clipping, wrapping, loading states, colors, and private data. + +## 4. Record movies + +Movies show a journey; stills remain the evidence for individual states. +Every movie writes a poster PNG and a video into `.astroshot//` and +adds them to the manifest as one shot. Review state attaches to the poster +file. The video is WebM unless you pass `--format mp4`, and `mp4` takes +effect only for PTY and frames movies with ffmpeg on `PATH`: browser movies +and every movie encoded without ffmpeg are WebM. Read the extension of +`videoPath` in the JSON the command prints. + +Choose the source from what you are recording: + +| Recording | Source | +|---|---| +| A web page or single-page app | `--source browser` | +| A terminal program | `--source pty` | +| A native macOS app window | `--source desktop.window` | +| PNG frames your own code produces | `movie start` / `push-frame` / `stop` | + +Do not record a browser or a terminal by grabbing its desktop window. + +```sh +archdev shots movie run --source browser --feature checkout --slug purchase \ + --url http://localhost:3000/checkout --script fixtures/purchase.mjs \ + --size 1280x800 --run-id checkout-r1 + +archdev shots movie run --source pty --feature install-wizard --slug walkthrough \ + --fixture fixtures/install-wizard.yaml + +archdev shots movie run --source desktop.window --feature onboarding --slug first-run \ + --bundle-id com.example.App --duration-ms 4000 +``` + +- Shared options: `--feature`, `--slug` (kebab-case), `--title`, + `--description`, `--run-id`, `--root`, `--size WxH`, `--fps` (up to 60), + `--format`, `--status`. +- `browser`: `--url`, `--settle-ms`, `--headed`, and `--script PATH`, an ES + module whose default export is `async (page) => { … }` and receives the + Playwright page after `--url` loads (scripts need the helper packages, + section 1). Without `--url` or `--script` it + records a placeholder page. +- `pty`: `--fixture`, the same YAML as a PTY still. +- `desktop.window` (macOS only): select the window with `--bundle-id`, + `--window-id`, `--title-regex`, `--owner`, `--pid` or `--pick`; record for + `--duration-ms` (default 3000) with `--cursor` to show the pointer. It + refuses nearly blank frames unless you pass `--allow-blank`. + `archdev shots movie list-windows` lists candidate windows, and + `archdev shots movie check-screen-access` reports the Screen Recording + permission (exit 2 when denied; `--request` asks for it, `--open-settings` + opens System Settings). +- An option that belongs to another source is refused, for example + `--url cannot be used with --source pty`. + +Frames you produce yourself: + +```sh +archdev shots movie start --feature report --slug build-up --size 1280x720 --fps 4 +archdev shots movie push-frame --feature report --file frames/0001.png +archdev shots movie mark --feature report --slug totals --note "Totals appear" +archdev shots movie push-frame --feature report --file frames/0002.png +archdev shots movie stop --feature report --status pass +``` + +`mark` adds a chapter at the current time. `start` prints the session as +JSON, including its `id`. When a feature has more than one open session, +pass `--session ID` to `push-frame`, `mark` and `stop`: without it the +command picks one of them, not necessarily the one you started. + +Close a movie run the same way as stills, with the same run id: + +```sh +archdev shots movie finalize --feature checkout --run-id checkout-r1 --status pass +``` + +## 5. Read review feedback + +```sh +archdev shots status +archdev shots status --feature account-settings --json +``` + +The table lists every image in each feature directory with its kind, state +and comment count, then totals. `--json` prints `{root, features: [{feature, +run_id, status, shots: [{file, slug, title, description, kind, video, +duration_ms, chapters, captured_at, viewport, url, state, comments: [{id, +body, created_at}]}]}], totals: {seen, stale, unseen}}`. + +| State | Meaning | +|---|---| +| `seen` | A person marked this exact image (same SHA-256) seen in the current run | +| `stale` | A person reviewed an earlier version of this file; the bytes changed since. Comments still apply | +| `unseen` | No review for this file in the current run | + +`status` lists every image in the directory, including earlier runs, and +those report `unseen` because review is scoped to the manifest's current +run. The current run's files are the ones listed under `shots` in +`.astroshot//manifest.json`; check there before reporting a frame +as unreviewed. + +Read status while the run you are asking about is still the current one, +then report every comment to the person, address it, and capture new +images. +Never write `review.json` yourself or mark your own work seen: the person +reviews the new bytes. + +`archdev shots review` opens the review tray in a terminal (macOS and Linux; +it needs an interactive terminal, so it is for the person, not for you). +Arguments are roots to watch; `--no-graphics`, `--no-watch` and `--no-index` +turn off images, live reload and the index cache. + +## 6. Upload for web review + +```sh +archdev shots upload --feature account-settings --root "$(git rev-parse --show-toplevel)" +``` + +Uploads the feature's current run (the shots in `manifest.json`, with movie +videos) to ArchDev so it can be reviewed on the web. It needs `archdev auth +login` with a user session (agent-only sessions cannot upload), a Git +`origin` remote, and a manifest with `"version": 1`. It prints one +` ` line per uploaded file, then the group key and the review +URL; `--json` prints `{group_key, url, uploads}`. Unchanged files are +skipped, and `uploads.json` in the feature directory remembers what was sent. +Exit 1 means at least one file failed; the errors are on stderr. + +`--root` defaults to the current directory, not the Git top level, so pass it +when you are not at the worktree root. + +Upload only when the person asked for it, or when their `shots-upload` +setting is `auto`. + +## 7. User stories + +A user story is a scripted walk through the product from a user's point of +view, run by an agent, with a screenshot and notes per step. Stories live +under `.astroshot/stories//` (the older `.astroshot/friction-logs/` +tree is still read) and appear in the review tray's User stories tab, not in +the Shots stream. Do not publish story screenshots with `archdev shots +capture`. + +```sh +archdev shots stories new invite-teammate --title "Invite a teammate" \ + --description "An admin invites a new member and the member accepts." +archdev shots stories list +archdev shots stories show invite-teammate +RUN_DIR="$(archdev shots stories run-dir invite-teammate)" +``` + +- `new` creates `prompt.md` (headings: Goal, Persona, Environment, + Preconditions, Steps, Out of scope, Output contract) and `meta.json` with + status `draft`. `--title` is required and the slug is kebab-case. An + existing slug fails with `File exists (os error 17)`. +- `list` prints `slug, title, status, run count, latest run id` per story; + `--json` adds whether the latest run was acknowledged. +- `show ` prints the prompt and the steps of the latest run; + `--run ID` picks another run, `--json` returns `{prompt_md, steps}`. +- `run-dir ` creates an empty `runs//` directory and prints + its path. Use a new one for every attempt. +- The stories commands always use the Git top level as the root. + +Fill in `prompt.md` with 5 to 12 user actions, each with what "done" looks +like. Keep secrets out of it; name environment variables instead. Use a +clean session or a throwaway account, not the person's own login, unless the +prompt says otherwise. + +To run a story, for each step: do the action, wait for the done signal, save +a numbered PNG into `$RUN_DIR` (`0001-open-settings.png`), and append one +JSON line to `$RUN_DIR/log.jsonl`: + +```json +{"step": 1, "id": "open-settings", "title": "Open settings", "description": "Opened /settings from the avatar menu.", "transcript": "I open settings from the avatar menu. The entry is easy to find, but the page loads without a heading, so I check the URL before moving on to Members.", "screenshots": ["0001-open-settings.png"], "good": ["Settings is one click from the avatar"], "improve": ["The settings page has no visible heading"], "url": "/settings"} +``` + +`step`, `id`, `title`, `description`, `transcript`, `screenshots`, `good` +and `improve` are required (`good` and `improve` may be empty arrays); +`url` and `captured_at` are optional. `transcript` is spoken narration, two +to five sentences in plain prose, that says what you did, what worked and +what did not, and leads into the next step. Set `meta.json` `status` to +`running` while you work and `complete` or `failed` at the end. If you stop +before writing any step, delete the empty run directory. Report the run +directory, the step count, and the most important `improve` items. + +## Hook reminders + +When Shots is on and the ArchDev hooks are installed, editing UI files +(`.tsx`, `.jsx`, `.css`, `.scss`, `.html`, `.heex`, and similar; a +repository can set its own list in `archdev.json` under `shots.ui_globs`) +makes the hook print a reminder that starts with `ArchDev shots: Capture +shots for the flows touched by these UI edits`. A still published from the +same agent session into the same worktree clears it (`archdev shots capture`, +or `react`, `ink` or `pty` with `--feature`); movies and captures run from +another session (a subagent, the person's terminal) do not. When the edits +need no screenshot (a refactor with no +visible change), dismiss it with the session id from the reminder: + +```sh +archdev repo hook dismiss --harness --session \ + --evidence shots --reason "No visible UI change" +``` + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `archdev shots is off. Enable it with: archdev settings set shots on` | Shots is off for this profile. Ask the person to run the command in their own terminal (section 1). | +| `Shots settings require a human terminal outside an agent session` | You tried to change the setting. Ask the person instead. | +| `not a valid . image: PATH` | The bytes do not match the extension (a JPEG saved as `.png`, an HTML error page, a truncated file). Recapture it or fix the extension. | +| `unsupported image extension …` | Stills must be `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. | +| `astroshot node helper could not load "tsx/esm/api"` (or `vite`, `@vitejs/plugin-react`, `playwright-core`) | The helper's npm packages are missing. See "Node helper packages" below. | +| `timed out after Ns waiting for capture lock: …/.capture.lock` | Another capture into the same feature is running. The wait defaults to 120 seconds; `ASTROSHOT_LOCK_TIMEOUT_SECONDS=N` changes it. | +| `stale capture lock from dead pid N: …; remove it and retry` | A capture process died holding the lock. Confirm no capture is running, then remove that `.capture.lock` directory. | +| `feature must be kebab-case …`, `slug must be …`, `reserved feature: …` | Rename the feature or slug (section 2). | +| `manifest run_id "…" does not match "…"` | `--finalize --run-id` names a run that is not the current one. | +| `--X cannot be used with --source Y` | The movie option belongs to another source. | +| `… Install Chrome or set ASTROSHOT_CHROME` | No Chrome found for React stills or browser movies. Install Chrome, or set `ASTROSHOT_CHROME` to its executable. | +| `Not authenticated. Run archdev auth login first.` | `upload` needs a signed-in user. | +| `Media upload needs a user session with media:write` | The session is agent-only. The person signs in with `archdev auth login`. | +| `archdev shots review needs an interactive terminal.` | The review tray is for the person; read feedback with `archdev shots status`. | +| `Screen Recording denied …` | macOS permission for `desktop.window`. The person grants it in System Settings and restarts the app that launched the command. | + +### Node helper packages + +React and Ink stills and browser movie scripts run in a Node helper script +(`helper.mjs`) that loads `tsx`, `vite`, `@vitejs/plugin-react` and +`playwright-core` from its own directory, not from your project. The copy the +CLI writes to its cache has no packages next to it, so these modes fail until +someone installs them. Installing npm packages changes the person's machine: +ask first. With their agreement, keep a helper copy in a directory that has +the packages, and point the CLI at it with `ASTROSHOT_NODE_HELPER`: + +```sh +helper_dir="$HOME/.local/share/archdev-shots-helper" +mkdir -p "$helper_dir" +cp "$(ls -t "$HOME"/.cache/astroshot/node-helper/*/helper.mjs "$HOME"/Library/Caches/astroshot/node-helper/*/helper.mjs 2>/dev/null | head -n 1)" "$helper_dir/" +npm install --prefix "$helper_dir" tsx vite @vitejs/plugin-react playwright-core +export ASTROSHOT_NODE_HELPER="$helper_dir/helper.mjs" +``` + +The cached `helper.mjs` exists after the first React or Ink attempt, even a +failed one. Copy it again after upgrading ArchDev: the helper and the CLI +speak a versioned protocol.