From e14f1746b8afb93f5e91ece213a10df78bc15c44 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 16 Oct 2026 17:20:01 -0700 Subject: [PATCH 1/3] docs: how to write PR descriptions, with demos recorded against the stub AGENTS.md (which CLAUDE.md includes) now says what a PR description holds and in what order, how to record and embed demos with demo/, and how the stub server works and is extended. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c974ea0..16f98f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,3 +109,35 @@ Scripts and AI agents use the CLI through `--json`, so it is a contract, not a s - Squash-merge with the PR title; release-please reads it. - Use `--force-with-lease` pinned to the expected commit only for rebases. Otherwise push plainly. - Read a pushed commit's hash from the push output before reporting it. + +## Pull request descriptions + +A reviewer should see what a change is like to use without building it. A PR that changes what a user can see or do has these sections, in this order. Leave out the ones with nothing to say. + +1. **A paragraph on what users can now do**, then the commands as a `bash` block of examples, one per command or flag, with a comment where it isn't obvious. +2. **How it behaves:** prompts and their order, confirmations, what `--json` prints, exit codes, and anything a script relies on. +3. **Demos:** one recording for each command or flow in the change (below). +4. **Not in this PR:** what was deliberately left out, why, and when it would be worth adding. +5. **Follow-ups elsewhere**, such as source.coop server changes the CLI works around. Name the file and the problem. +6. **Testing:** the commands run and how many tests were added, and what wasn't tried, such as a real or staging API. Say it plainly. + +### Demos + +Record with `demo/` (see `demo/README.md`). Recordings are asciicasts uploaded to asciinema.org, where a reviewer can pause, rewind and copy the text. + +- **One demo per command or flow,** each with a bold title. Show the interactive paths: prompts, ← back, the review, and `-i` screens with their keys. Show the failure paths too: a refusal, a typed confirmation typed wrong, and an exit code (`; echo "exit $?"`). Pipe `--json` through `jq -c` to keep it short. +- **Paste what `record.py --upload` prints:** `[![name](https://asciinema.org/a/ID.svg)](https://asciinema.org/a/ID)`. GitHub shows the SVG as a still of the last frame, and it links to the player. Don't commit GIFs or video: GitHub plays video only when it's dragged into the web editor, so scripts can't add it. +- **Say in the PR that the demos run against the stub,** so the `http://localhost:PORT` URLs in them aren't mistaken for real ones. +- **Uploads are public to anyone with the link.** Record only the stub's made-up data, never a real account or token. Uploads belong to the machine's asciinema install ID, and asciinema.org may delete those not linked to an account. Run `asciinema auth` once to link yours. +- **Recording is a review pass.** An awkward message or a screen titled wrongly in a demo is a bug: fix it in the PR and re-record. + +### The stub server + +`demo/stub.py` stands in for the source.coop API when recording. It isn't for tests, which mock the API with `wiremock`. + +- **What it is:** stdlib Python, `python3 -I demo/stub.py PORT [FIXTURES]`, listening only on 127.0.0.1 and serving under `/api/v1`. `record.py` starts a fresh one on a free port for each demo, so every demo starts from `fixtures.json` as written. Within a demo, what one command creates, edits or deletes is what the next command sees, and it's gone when the stub stops. +- **Isolation:** each session gets `SOURCE_TOKEN=demo` (any token works), `SOURCE_API_URL` pointing at the stub, and a throwaway `HOME`. So the CLI never reads the real login session or the OS keychain, and drafts land in the throwaway `HOME`. Don't point a demo at a real environment instead. +- **Fixtures:** made-up records only. The signed-in `demo-user` owns the organization `demo-org`, with products, data connections, service accounts, trusts and memberships. Add the records a new demo needs to `fixtures.json`, keeping the names obviously fake. +- **It answers only what's been needed so far.** Anything else gets a 404 saying `The demo stub has no METHOD PATH`, so a missing endpoint is plain from the recording. Add a branch to `handle()` for each new path. +- **Match the API's shapes, not the CLI's guesses.** Take paths, bodies, status codes and response shapes from the OpenAPI spec (`https://source.coop/api/openapi`) or source.coop's `src/app/api/v1` routes. For example, membership lists come back as plain JSON arrays, while product lists come in pages (`{"items", "next_cursor"}`). A stub that answers in a shape the API doesn't use would make the demo show behaviour real users never see. +- **It checks almost nothing.** It returns the API's error shape (`{"error": {"code", "message", "field_errors"?}}`) only where a demo needs to show a refusal, such as a 409 for an ID that's taken. Add those per demo rather than reimplementing the API's rules. From 98169783735386ac757343d1df6a27d348858962 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 16 Oct 2026 23:36:00 -0700 Subject: [PATCH 2/3] docs: demos are GIFs on demo-assets; listings come in pages asciinema.org's SVG previews show as broken images on GitHub, whose image proxy can't fetch them, so demos are GIFs on the demo-assets branch linked to the player, as #59's record.py now prints. Membership lists are pages on source.coop since its #673, like every other listing. The stub's mechanics are left to demo/README.md; this keeps the rules for extending it. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 16f98f7..11ac601 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,21 +123,20 @@ A reviewer should see what a change is like to use without building it. A PR tha ### Demos -Record with `demo/` (see `demo/README.md`). Recordings are asciicasts uploaded to asciinema.org, where a reviewer can pause, rewind and copy the text. +Record with `demo/`: `demo/README.md` says how to write a demo, record it and get it into the PR. - **One demo per command or flow,** each with a bold title. Show the interactive paths: prompts, ← back, the review, and `-i` screens with their keys. Show the failure paths too: a refusal, a typed confirmation typed wrong, and an exit code (`; echo "exit $?"`). Pipe `--json` through `jq -c` to keep it short. -- **Paste what `record.py --upload` prints:** `[![name](https://asciinema.org/a/ID.svg)](https://asciinema.org/a/ID)`. GitHub shows the SVG as a still of the last frame, and it links to the player. Don't commit GIFs or video: GitHub plays video only when it's dragged into the web editor, so scripts can't add it. +- **Show each as a GIF linked to its player.** Paste what `record.py --upload --gif-base …` prints: `[![name](…/raw/demo-assets/pr-N/name.gif)](https://asciinema.org/a/ID)`, and push the GIFs to the `demo-assets` branch under `pr-N/`. The GIF plays inline. The player is where a reviewer can pause, rewind and copy text. Don't embed asciinema.org's own SVG or PNG previews: GitHub's image proxy can't fetch them, so they show as broken images. GIFs live only on `demo-assets`, never on a branch that gets merged. - **Say in the PR that the demos run against the stub,** so the `http://localhost:PORT` URLs in them aren't mistaken for real ones. - **Uploads are public to anyone with the link.** Record only the stub's made-up data, never a real account or token. Uploads belong to the machine's asciinema install ID, and asciinema.org may delete those not linked to an account. Run `asciinema auth` once to link yours. -- **Recording is a review pass.** An awkward message or a screen titled wrongly in a demo is a bug: fix it in the PR and re-record. +- **Recording is a review pass.** An awkward message, or a screen titled wrongly, is a bug: fix it in the PR and re-record. Check what each recording shows, not only that it finished. ### The stub server -`demo/stub.py` stands in for the source.coop API when recording. It isn't for tests, which mock the API with `wiremock`. +`demo/stub.py` stands in for the source.coop API when recording, serving the made-up records in `demo/fixtures.json`. `demo/README.md` explains how it runs. It isn't for tests, which mock the API with `wiremock`. When a demo needs more of it: -- **What it is:** stdlib Python, `python3 -I demo/stub.py PORT [FIXTURES]`, listening only on 127.0.0.1 and serving under `/api/v1`. `record.py` starts a fresh one on a free port for each demo, so every demo starts from `fixtures.json` as written. Within a demo, what one command creates, edits or deletes is what the next command sees, and it's gone when the stub stops. -- **Isolation:** each session gets `SOURCE_TOKEN=demo` (any token works), `SOURCE_API_URL` pointing at the stub, and a throwaway `HOME`. So the CLI never reads the real login session or the OS keychain, and drafts land in the throwaway `HOME`. Don't point a demo at a real environment instead. -- **Fixtures:** made-up records only. The signed-in `demo-user` owns the organization `demo-org`, with products, data connections, service accounts, trusts and memberships. Add the records a new demo needs to `fixtures.json`, keeping the names obviously fake. -- **It answers only what's been needed so far.** Anything else gets a 404 saying `The demo stub has no METHOD PATH`, so a missing endpoint is plain from the recording. Add a branch to `handle()` for each new path. -- **Match the API's shapes, not the CLI's guesses.** Take paths, bodies, status codes and response shapes from the OpenAPI spec (`https://source.coop/api/openapi`) or source.coop's `src/app/api/v1` routes. For example, membership lists come back as plain JSON arrays, while product lists come in pages (`{"items", "next_cursor"}`). A stub that answers in a shape the API doesn't use would make the demo show behaviour real users never see. -- **It checks almost nothing.** It returns the API's error shape (`{"error": {"code", "message", "field_errors"?}}`) only where a demo needs to show a refusal, such as a 409 for an ID that's taken. Add those per demo rather than reimplementing the API's rules. +- **Never point a demo at a real environment instead.** Every session runs with `SOURCE_TOKEN=demo` and a throwaway `HOME`, so it can't read a real login or the OS keychain. Keep it that way. +- **Keep fixtures obviously fake,** under `demo-user` and its organization `demo-org`. Add the records a new demo needs there. +- **Add endpoints as demos need them.** Anything else gets a 404 saying `The demo stub has no METHOD PATH`, so a missing endpoint is plain from the recording. Add a branch to `handle()` for each new path. +- **Match the API's shapes, not the CLI's guesses.** Take paths, bodies, status codes and response shapes from the OpenAPI spec (`https://source.coop/api/openapi`) or source.coop's `src/app/api/v1` routes. Listings come in pages (`{"items", "next_cursor"}`): products, data connections, memberships, service accounts, trusts and API keys. A stub that answers in a shape the API doesn't use makes the demo show behaviour real users never see, and hides the CLI bugs a demo should catch. +- **It checks almost nothing.** It returns the API's error shape (`{"error": {"code", "message", "field_errors"?}}`) only where a demo shows a refusal, such as a 409 for an ID that's taken or a key revoked twice. Add those per demo rather than reimplementing the API's rules. From 28b5fc63f4515ec574247bd31adf7d2115574c17 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 16 Oct 2026 23:55:36 -0700 Subject: [PATCH 3/3] docs: demo scripts are written per PR, in demo/scripts/ Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 11ac601..d198dac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,6 +125,7 @@ A reviewer should see what a change is like to use without building it. A PR tha Record with `demo/`: `demo/README.md` says how to write a demo, record it and get it into the PR. +- **Write the demo script for your change:** one per PR, in `demo/scripts/` (git ignores it). None are kept and none are waiting to be updated: you know what your change does, so script that. Grow `demo/stub.py` and `demo/fixtures.json` with what your demos need, and commit those. - **One demo per command or flow,** each with a bold title. Show the interactive paths: prompts, ← back, the review, and `-i` screens with their keys. Show the failure paths too: a refusal, a typed confirmation typed wrong, and an exit code (`; echo "exit $?"`). Pipe `--json` through `jq -c` to keep it short. - **Show each as a GIF linked to its player.** Paste what `record.py --upload --gif-base …` prints: `[![name](…/raw/demo-assets/pr-N/name.gif)](https://asciinema.org/a/ID)`, and push the GIFs to the `demo-assets` branch under `pr-N/`. The GIF plays inline. The player is where a reviewer can pause, rewind and copy text. Don't embed asciinema.org's own SVG or PNG previews: GitHub's image proxy can't fetch them, so they show as broken images. GIFs live only on `demo-assets`, never on a branch that gets merged. - **Say in the PR that the demos run against the stub,** so the `http://localhost:PORT` URLs in them aren't mistaken for real ones.