bottleship 0.1.0-prototype.0 → 0.1.0-prototype.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +118 -113
  3. package/dist/src/boxes.d.ts +41 -39
  4. package/dist/src/boxes.js +181 -179
  5. package/dist/src/cli.js +298 -83
  6. package/dist/src/config.d.ts +3 -6
  7. package/dist/src/config.js +19 -85
  8. package/dist/src/init.d.ts +3 -4
  9. package/dist/src/init.js +120 -205
  10. package/dist/src/local.d.ts +8 -0
  11. package/dist/src/local.js +24 -0
  12. package/dist/src/onboard.d.ts +5 -0
  13. package/dist/src/onboard.js +25 -0
  14. package/dist/src/paths.d.ts +0 -1
  15. package/dist/src/paths.js +0 -1
  16. package/dist/src/provider.d.ts +16 -0
  17. package/dist/src/provider.js +81 -0
  18. package/dist/src/repo.d.ts +21 -0
  19. package/dist/src/repo.js +75 -0
  20. package/dist/src/review/comments.d.ts +66 -0
  21. package/dist/src/review/comments.js +344 -0
  22. package/dist/src/review/contract.d.ts +82 -0
  23. package/dist/src/review/contract.js +189 -0
  24. package/dist/src/review/github.d.ts +120 -0
  25. package/dist/src/review/github.js +160 -0
  26. package/dist/src/review/local.d.ts +14 -0
  27. package/dist/src/review/local.js +70 -0
  28. package/dist/src/review/loop.d.ts +12 -0
  29. package/dist/src/review/loop.js +119 -0
  30. package/dist/src/review/pr.d.ts +14 -0
  31. package/dist/src/review/pr.js +68 -0
  32. package/dist/src/review/prompt.d.ts +15 -0
  33. package/dist/src/review/prompt.js +48 -0
  34. package/dist/src/review/serve.d.ts +1 -0
  35. package/dist/src/review/serve.js +8 -0
  36. package/dist/src/review/server.d.ts +14 -0
  37. package/dist/src/review/server.js +484 -0
  38. package/dist/src/review/setting-up.d.ts +4 -0
  39. package/dist/src/review/setting-up.js +74 -0
  40. package/dist/src/review/start.d.ts +32 -0
  41. package/dist/src/review/start.js +140 -0
  42. package/dist/src/review/target.d.ts +20 -0
  43. package/dist/src/review/target.js +66 -0
  44. package/dist/src/review/template.d.ts +3 -0
  45. package/dist/src/review/template.js +13 -0
  46. package/dist/src/review/terminal.d.ts +4 -0
  47. package/dist/src/review/terminal.js +91 -0
  48. package/dist/src/secrets.d.ts +1 -0
  49. package/dist/src/secrets.js +11 -1
  50. package/dist/src/types.d.ts +1 -71
  51. package/dist/src/ui.d.ts +6 -1
  52. package/dist/src/ui.js +22 -23
  53. package/dist/src/util.d.ts +11 -0
  54. package/dist/src/util.js +40 -0
  55. package/package.json +7 -14
  56. package/presets/agents/claude.json +1 -5
  57. package/presets/agents/codex.json +1 -10
  58. package/presets/agents/cursor.json +1 -9
  59. package/presets/agents/opencode.json +1 -14
  60. package/presets/providers/docker/create +12 -0
  61. package/presets/providers/docker/destroy +2 -0
  62. package/presets/providers/docker/exec +2 -0
  63. package/presets/providers/docker/list +4 -0
  64. package/presets/providers/docker/status +2 -0
  65. package/presets/providers/docker/url +4 -0
  66. package/review-app/KIT.md +106 -0
  67. package/review-app/WRITING.md +93 -0
  68. package/review-app/examples/rply-16.tsx +98 -0
  69. package/review-app/index.html +15 -0
  70. package/review-app/package-lock.json +2539 -0
  71. package/review-app/package.json +24 -0
  72. package/review-app/playground/fixtures/excalidraw-12602/data/box.json +1 -0
  73. package/review-app/playground/fixtures/excalidraw-12602/data/pr.diff +91 -0
  74. package/review-app/playground/fixtures/excalidraw-12602/data/pr.json +62 -0
  75. package/review-app/playground/fixtures/excalidraw-12602/data/review-comments.json +44 -0
  76. package/review-app/playground/fixtures/excalidraw-12602/events.json +1 -0
  77. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-ar-zoom.png +0 -0
  78. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-en-US-zoom.png +0 -0
  79. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-IL-zoom.png +0 -0
  80. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-dark.png +0 -0
  81. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-hover.png +0 -0
  82. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-page.png +0 -0
  83. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-tip.png +0 -0
  84. package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-zen.png +0 -0
  85. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-ar-zoom.png +0 -0
  86. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-en-US-zoom.png +0 -0
  87. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-IL-zoom.png +0 -0
  88. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-dark.png +0 -0
  89. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-hover.png +0 -0
  90. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-page.png +0 -0
  91. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-tip.png +0 -0
  92. package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-zen.png +0 -0
  93. package/review-app/playground/fixtures/excalidraw-12602/instances.json +1 -0
  94. package/review-app/playground/fixtures/excalidraw-12602/review.tsx +103 -0
  95. package/review-app/playground/fixtures/rply-16/data/box.json +1 -0
  96. package/review-app/playground/fixtures/rply-16/data/hooks.diff +28 -0
  97. package/review-app/playground/fixtures/rply-16/data/pr.diff +114 -0
  98. package/review-app/playground/fixtures/rply-16/data/pr.json +68 -0
  99. package/review-app/playground/fixtures/rply-16/data/review-comments.json +1 -0
  100. package/review-app/playground/fixtures/rply-16/events.json +156 -0
  101. package/review-app/playground/fixtures/rply-16/instances.json +1 -0
  102. package/review-app/playground/fixtures/rply-16/review.tsx +98 -0
  103. package/review-app/playground/gallery.tsx +127 -0
  104. package/review-app/playground/index.html +15 -0
  105. package/review-app/playground/main.tsx +26 -0
  106. package/review-app/playground/mock.ts +215 -0
  107. package/review-app/playground/playground.css +18 -0
  108. package/review-app/playground/vite.config.ts +32 -0
  109. package/review-app/scripts/check.cjs +47 -0
  110. package/review-app/scripts/clip.cjs +104 -0
  111. package/review-app/scripts/shot.cjs +66 -0
  112. package/review-app/src/App.tsx +132 -0
  113. package/review-app/src/data/box.json +1 -0
  114. package/review-app/src/data/hooks.diff +28 -0
  115. package/review-app/src/data/pr.diff +114 -0
  116. package/review-app/src/data/pr.json +68 -0
  117. package/review-app/src/data/review-comments.json +1 -0
  118. package/review-app/src/kit/agent.tsx +287 -0
  119. package/review-app/src/kit/chapter.tsx +178 -0
  120. package/review-app/src/kit/code.tsx +217 -0
  121. package/review-app/src/kit/comments.tsx +377 -0
  122. package/review-app/src/kit/finish.tsx +274 -0
  123. package/review-app/src/kit/flow.tsx +184 -0
  124. package/review-app/src/kit/index.ts +10 -0
  125. package/review-app/src/kit/kit.css +696 -0
  126. package/review-app/src/kit/live.ts +91 -0
  127. package/review-app/src/kit/media.tsx +348 -0
  128. package/review-app/src/kit/overview.tsx +161 -0
  129. package/review-app/src/kit/pr.ts +100 -0
  130. package/review-app/src/kit/pullrequest.tsx +298 -0
  131. package/review-app/src/kit/review.ts +74 -0
  132. package/review-app/src/kit/theme.ts +30 -0
  133. package/review-app/src/kit/ui.tsx +127 -0
  134. package/review-app/src/main.tsx +6 -0
  135. package/review-app/src/review.tsx +22 -0
  136. package/review-app/tsconfig.json +17 -0
  137. package/review-app/vite.config.ts +12 -0
  138. package/dist/presets/providers/command.d.ts +0 -3
  139. package/dist/presets/providers/command.js +0 -39
  140. package/dist/presets/providers/daytona.d.ts +0 -3
  141. package/dist/presets/providers/daytona.js +0 -173
  142. package/dist/presets/providers/docker.d.ts +0 -3
  143. package/dist/presets/providers/docker.js +0 -31
  144. package/dist/presets/providers/e2b.d.ts +0 -3
  145. package/dist/presets/providers/e2b.js +0 -124
  146. package/dist/presets/providers/exe.d.ts +0 -3
  147. package/dist/presets/providers/exe.js +0 -28
  148. package/dist/presets/providers/http.example.d.ts +0 -3
  149. package/dist/presets/providers/http.example.js +0 -47
  150. package/dist/presets/providers/local.d.ts +0 -3
  151. package/dist/presets/providers/local.js +0 -52
  152. package/dist/presets/providers/modal.d.ts +0 -3
  153. package/dist/presets/providers/modal.js +0 -117
  154. package/dist/presets/providers/ssh.d.ts +0 -3
  155. package/dist/presets/providers/ssh.js +0 -49
  156. package/dist/presets/providers/util.d.ts +0 -13
  157. package/dist/presets/providers/util.js +0 -102
  158. package/presets/providers/command.ts +0 -43
  159. package/presets/providers/daytona.ts +0 -181
  160. package/presets/providers/docker.ts +0 -38
  161. package/presets/providers/e2b.ts +0 -130
  162. package/presets/providers/exe.ts +0 -35
  163. package/presets/providers/http.example.ts +0 -64
  164. package/presets/providers/local.ts +0 -56
  165. package/presets/providers/modal.ts +0 -121
  166. package/presets/providers/ssh.ts +0 -54
  167. package/presets/providers/util.ts +0 -112
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bottleship",
3
- "version": "0.1.0-prototype.0",
4
- "description": "Run coding agents in remote sandboxes, like subagents, from your local coding agent.",
3
+ "version": "0.1.0-prototype.2",
4
+ "description": "Coding agents in boxes: isolated dev environments for your repo, run by your local agent.",
5
5
  "license": "MIT",
6
6
  "author": "Joshua Knauber",
7
7
  "type": "module",
@@ -11,21 +11,17 @@
11
11
  "files": [
12
12
  "bin",
13
13
  "dist/src",
14
- "dist/presets",
15
14
  "presets",
15
+ "review-app",
16
+ "!review-app/node_modules",
17
+ "!review-app/dist",
16
18
  "README.md",
17
19
  "CHANGELOG.md",
18
20
  "LICENSE"
19
21
  ],
20
- "exports": {
21
- ".": {
22
- "types": "./dist/src/types.d.ts",
23
- "default": "./dist/src/types.js"
24
- }
25
- },
26
22
  "scripts": {
27
23
  "build": "tsc -p tsconfig.json",
28
- "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p review-app/tsconfig.json",
29
25
  "test": "npm run build && vitest run",
30
26
  "prepare": "npm run build",
31
27
  "release": "node scripts/release.mjs"
@@ -39,10 +35,7 @@
39
35
  "vitest": "^3.2.7"
40
36
  },
41
37
  "dependencies": {
42
- "@clack/prompts": "^0.11.0",
43
- "@daytonaio/sdk": "^0.231.1",
44
- "e2b": "^2.63.0",
45
- "modal": "^0.11.1"
38
+ "@clack/prompts": "^0.11.0"
46
39
  },
47
40
  "publishConfig": {
48
41
  "access": "public"
@@ -4,9 +4,5 @@
4
4
  "resume": "claude -p {prompt} --continue --dangerously-skip-permissions",
5
5
  "env": { "IS_SANDBOX": "1" },
6
6
  "unset": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
7
- "login": {
8
- "routes": ["token"],
9
- "use": "token",
10
- "token": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token", "hint": "paste the long-lived token `claude setup-token` prints; it uses your Claude subscription" }
11
- }
7
+ "login": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token", "hint": "paste the long-lived token `claude setup-token` prints; it uses your Claude subscription" }
12
8
  }
@@ -2,14 +2,5 @@
2
2
  "install": ["command -v codex >/dev/null || npm install -g --silent @openai/codex"],
3
3
  "run": "codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check -- {prompt}",
4
4
  "resume": "codex exec resume --last --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check -- {prompt}",
5
- "login": {
6
- "routes": ["device", "token"],
7
- "use": "device",
8
- "device": {
9
- "command": "codex login --device-auth",
10
- "status": "codex login status",
11
- "setup": "in ChatGPT, open Settings, Security and turn on device code login for Codex"
12
- },
13
- "token": { "env": "CODEX_API_KEY", "store": "openai", "hint": "create an OpenAI API key at https://platform.openai.com/api-keys and paste it; it bills to the API, not your ChatGPT plan" }
14
- }
5
+ "login": { "env": "CODEX_API_KEY", "store": "openai", "hint": "create an OpenAI API key at https://platform.openai.com/api-keys and paste it; it bills to the API, not your ChatGPT plan" }
15
6
  }
@@ -2,13 +2,5 @@
2
2
  "install": ["command -v cursor-agent >/dev/null || { curl -fsS https://cursor.com/install | bash; command -v cursor-agent >/dev/null; }"],
3
3
  "run": "cursor-agent -p --force --trust --sandbox disabled --output-format text -- {prompt}",
4
4
  "resume": "cursor-agent -p --continue --force --trust --sandbox disabled --output-format text -- {prompt}",
5
- "login": {
6
- "routes": ["token", "device"],
7
- "use": "token",
8
- "token": { "env": "CURSOR_API_KEY", "store": "cursor", "hint": "create an API key at https://cursor.com/dashboard/api and paste it; it bills to your Cursor plan" },
9
- "device": {
10
- "command": "NO_OPEN_BROWSER=1 cursor-agent login",
11
- "status": "! cursor-agent status 2>&1 | grep -qi 'not logged in'"
12
- }
13
- }
5
+ "login": { "env": "CURSOR_API_KEY", "store": "cursor", "hint": "create an API key at https://cursor.com/dashboard/api and paste it; it bills to your Cursor plan" }
14
6
  }
@@ -2,18 +2,5 @@
2
2
  "install": ["command -v opencode >/dev/null || npm install -g --silent @opencode/cli"],
3
3
  "run": "opencode run --standalone --auto -- {prompt}",
4
4
  "resume": "opencode run --standalone --continue --auto -- {prompt}",
5
- "login": {
6
- "routes": ["none", "device", "token"],
7
- "use": "none",
8
- "device": {
9
- "command": "opencode auth login openai --method chatgpt-headless --standalone",
10
- "status": "opencode auth list --standalone --format json | grep -q '\"openai\"'"
11
- },
12
- "token": { "env": "OPENCODE_API_KEY", "store": "opencode", "hint": "sign in at https://opencode.ai/console, copy your API key (OpenCode Zen) and paste it" },
13
- "tokens": [
14
- { "env": "OPENCODE_API_KEY", "store": "opencode", "hint": "OpenCode Zen: sign in at https://opencode.ai/console, copy your API key and paste it" },
15
- { "env": "ANTHROPIC_API_KEY", "store": "anthropic", "hint": "an Anthropic API key (not a Claude subscription) from https://console.anthropic.com/settings/keys" },
16
- { "env": "OPENAI_API_KEY", "store": "openai", "hint": "an OpenAI API key from https://platform.openai.com/api-keys" }
17
- ]
18
- }
5
+ "login": { "env": "OPENCODE_API_KEY", "store": "opencode", "hint": "sign in at https://opencode.ai/console, copy your API key (OpenCode Zen) and paste it" }
19
6
  }
@@ -0,0 +1,12 @@
1
+ # create <id> <dockerfile> <image>: builds the image once per Dockerfile, then runs the box as a container that lives until destroy.
2
+ # Ports the image EXPOSEs are published on 127.0.0.1 only, each on a free port of this machine.
3
+ set -euo pipefail
4
+ id=$1 dockerfile=$2 image=$3
5
+ if ! docker image inspect "$image" >/dev/null 2>&1; then
6
+ docker build -q -t "$image" - < "$dockerfile" >/dev/null
7
+ fi
8
+ publish=()
9
+ for p in $(docker image inspect -f '{{range $p, $_ := .Config.ExposedPorts}}{{$p}} {{end}}' "$image"); do
10
+ publish+=(-p "127.0.0.1::${p%/*}")
11
+ done
12
+ docker run -d --name "$id" --label bottleship=1 ${publish[@]+"${publish[@]}"} --entrypoint sleep "$image" infinity >/dev/null
@@ -0,0 +1,2 @@
1
+ # destroy <id>: removes the container; the image stays as the cache for the next box
2
+ docker rm -f "$1" >/dev/null 2>&1 || true
@@ -0,0 +1,2 @@
1
+ # exec <id> <script>: runs the script in the box as its user, stdin passed through
2
+ exec docker exec -i "$1" bash -c "$2"
@@ -0,0 +1,4 @@
1
+ # list: the boxes bottleship made here, one per line: <id> <created, ISO 8601>
2
+ set -euo pipefail
3
+ ids=$(docker ps -aq --filter label=bottleship=1)
4
+ [ -z "$ids" ] || docker inspect -f '{{.Name}} {{.Created}}' $ids | sed 's|^/||'
@@ -0,0 +1,2 @@
1
+ # status <id>: running or gone
2
+ if [ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" = true ]; then echo running; else echo gone; fi
@@ -0,0 +1,4 @@
1
+ # url <id> <port>: where the box's port is published on this machine
2
+ set -euo pipefail
3
+ hostport=$(docker port "$1" "$2/tcp" | head -1)
4
+ echo "http://127.0.0.1:${hostport##*:}"
@@ -0,0 +1,106 @@
1
+ # The review kit
2
+
3
+ A review is a tour of one change: the **overview** says what it is about, the **chapters** show it happening, and **changes** show the code in the same order. You write `src/review.tsx`; the frame (`src/App.tsx`) and the kit (`src/kit`) are shared by every review. Every review should look like the same product, so build from the kit and keep to the rules below.
4
+
5
+ `examples/rply-16.tsx` is a backend change shown with flows; read it before you start.
6
+
7
+ ## The shape
8
+
9
+ ```tsx
10
+ export default defineReview({
11
+ instances: [
12
+ { id: "base", label: "Base", detail: "main", port: 7781 },
13
+ { id: "head", label: "Head", detail: "this PR", port: 8080 },
14
+ ],
15
+ overview: () => <Overview>…</Overview>,
16
+ chapters: [{ id: "blocks-loopback", title: "A loopback page is blocked", page: () => <Chapter …/> }],
17
+ groups: [{ id: "core", title: "Embedded IPv4 is judged as IPv4", why: "…", chapter: "blocks-loopback", file: "src/unfurl.ts", lines: [[79, 107]] }],
18
+ });
19
+ ```
20
+
21
+ - `label` is always short: `Base`, `Head`, or the name of a variant. Put what sets an instance apart in `detail` ("main", "this PR, Hebrew"), never in the label.
22
+ - `path` opens the instance on a page other than `/`, e.g. `/?lng=he-IL`.
23
+
24
+ ## Overview
25
+
26
+ In this order, inside `<Overview>` (it adds the title, the PR line, the files and the way into the tour):
27
+
28
+ 1. `<Lead>`: one sentence on what the change does for the people who use it. Not how.
29
+ 2. `<BeforeAfter before after>`: the behaviour before and after, in two or three plain sentences each.
30
+ 3. One picture of where it happens: a `<Compare title>` of the changed part for a visible change, or a `<Card caption><Flow def/></Card>` for an invisible one.
31
+ 4. `<Attention items held>`: 2 to 4 things that deserve a close look, each linked with `to` to the chapter or group that shows it. Check every claim made about the change (description, comments, reviews, bots) in the box. A claim that does not fully hold is an item too, with `claim: { source, quote, status }` instead of `text`: `status` is `fail` (does not hold) or `neutral` (partly, or out of scope; `label` names it, e.g. "Known, out of scope"), and `detail` says what you found. Claims that hold go in `held` as `{ source, quote, to }`.
32
+
33
+ ## Chapters
34
+
35
+ 3 to 6, the core behaviour first, then edge cases, then that nothing else broke. A chapter title says what happens ("A loopback page is blocked"), not what you test ("Test the guard").
36
+
37
+ No chapter just to run the tests; CI does that. Run the PR's tests against base too and put what you find in `<Attention>`: that they fail on base is a held claim, and a test that checks less than it looks is an item. A test chapter is for a PR without CI, or when the output itself is the evidence.
38
+
39
+ ```tsx
40
+ <Chapter title="…" step="blocks-loopback" expect={{ base: "Returns the internal page.", head: "Returns no title." }} flow={ssrf} here="guard" group="core" evidence={…}>
41
+ One or two sentences: what the step does and why it matters.
42
+ </Chapter>
43
+ ```
44
+
45
+ - `expect` says what should happen on each instance before the reviewer presses Run. Each instance's last `ok` or `fail` event is shown under it as the result, so make that event's `text` (or its overlay `title`) a short statement of what came out: "Order is − 100% +", "Test fails".
46
+ - `step` is `~/review/steps/<step>.sh`. Leave it out when there is nothing to run.
47
+ - `flow` and `here` list the flow's parts beside the chapter, with a column per instance showing what the step's events did at each part; `here` is the part the chapter is about.
48
+ - `group` links the code behind the chapter.
49
+
50
+ ### Evidence
51
+
52
+ `evidence` is what fills the right side. Pick by what the change is:
53
+
54
+ | The change | Evidence |
55
+ |---|---|
56
+ | Visible in the UI | `<Live />` first, then a `<Compare title>` per state that matters (each locale, theme, hover, empty, error, narrow). |
57
+ | Behaviour you cannot see (auth, caching, jobs) | Leave `evidence` out: the live apps fill the side, overlays show what came out, and the chapter's `flow` shows the path. |
58
+ | Tests, scripts, API calls | `<Outcome />`: each instance's output, from the overlay `body`. Use it only when that output is worth reading; the result itself already shows in the guide. |
59
+ | Mixed | `<Live />` or `<Outcome />` first, then screenshots. |
60
+
61
+ Without `evidence` the live instances fill the side. With it, the side scrolls and keeps the order you give.
62
+
63
+ ## Screenshots
64
+
65
+ Take them with `node scripts/shot.cjs` (options at the top of the file), into `~/review/shots`.
66
+
67
+ - Crop to the part that changed (`--selector`, `--pad 24`) and outline it (`--highlight`). Whole pages only when the layout is the point.
68
+ - Take base and head with the same viewport, scheme, locale and actions, so a comparison only shows the change.
69
+ - Name them `base-<what>.png` and `head-<what>.png`.
70
+ - `<Compare base head title caption>`: `title` names the state in two to four words ("Arabic", "Hovering −", "Dark theme"). `caption` is one sentence on what to notice. Use `mode="diff"` to prove nothing changed.
71
+ - `<Shot src title caption>` for a single picture.
72
+
73
+ ## Clips
74
+
75
+ A clip is a short recording, for what a still cannot show: a flow of several steps (a form across pages, a drag, an undo), motion (a transition, an animation, a loading state), or timing (a debounce, a toast that disappears). If one screenshot of the end state says it, take the screenshot.
76
+
77
+ Record with `node scripts/clip.cjs` (options at the top of the file): the page, then each action at a person's pace, with a cursor drawn in. It prints the `src` to use.
78
+
79
+ - 3 to 10 seconds: start where the flow starts, end once the result has shown.
80
+ - Record base and head with the same viewport, scheme, locale and actions, then `<Clip base head title caption>` plays them side by side, in step. `<Clip src title caption>` for one.
81
+ - Name them `base-<what>.webm` and `head-<what>.webm`. Pair a clip with a screenshot of the end state when the reviewer needs to look closely.
82
+
83
+ ## Flows
84
+
85
+ Nodes sit on a grid (`col`, `row` from 0). Keep a flow to 3 to 6 nodes, in one or two rows, read left to right. Titles are the names in the code (`assertPublic`); `sub` is a detail in mono (`isPrivate(address)`). Mark the nodes the change touches with `changed`. Review hooks light it with events: `flow: { id, edge: [from, to] }` moves a dot, `flow: { id, node, status, label }` colors a node and labels it.
86
+
87
+ ## Groups
88
+
89
+ The diff, split into the parts the tour talks about, in tour order: the core change first, glue next, tests last. `title` names the part; `why` is one sentence on why it exists; `lines` are new-file line ranges. Files you do not group are listed after the groups on their own.
90
+
91
+ ## Writing
92
+
93
+ [WRITING.md](WRITING.md) says how to write every piece of text in a review: the overview, chapters, `expect`, step results, captions, Attention, groups, and your answers in the chat. Read it before you write the tour. In short:
94
+
95
+ - Sentence case everywhere. No exclamation marks, no emoji.
96
+ - Short: a lead is one sentence, a `why` is one sentence, a caption is one sentence.
97
+ - Plain words about behaviour for the people who use it. The real code names, in `<code>`, when the reviewer needs them.
98
+ - Show, do not grade: your view belongs only in Attention.
99
+
100
+ ## When the kit does not fit
101
+
102
+ Write a component next to `review.tsx` with the kit's classes and tokens (`.card`, `.fig`, `.list`, `.rows`, `var(--line)`). Type has six sizes (`--fs-xl` titles, `--fs-lg` lead, `--fs` body, `--fs-sm` secondary, `--fs-mono` code, `--fs-xs` badge glyphs), two weights (400, 500) and three text colors (`--text`, `--text-2`, `--text-3`); dim with a color, never with opacity. No inline colors, sizes or one-off spacing. If you change `src/App.tsx` or `src/kit`, say what and why in your summary.
103
+
104
+ ## Check
105
+
106
+ `npm run check` typechecks, builds, opens every page in Chromium and fails on errors and broken images. Look at the page screenshots it saves in `~/review/checks`, as the reviewer will see them, and fix what reads badly.
@@ -0,0 +1,93 @@
1
+ # Writing the review
2
+
3
+ Everything you write in a review is read by a reviewer between other work: the overview, chapters, captions, step results, group reasons, your answers in the chat, and any comment the reviewer asks you to leave. Write so they understand it on the first read.
4
+
5
+ Three rules sit above the rest:
6
+
7
+ - **Cut every word that does no work.** If the sentence survives without a word, the word goes. "In order to" is "to". "It is important to note that" is nothing.
8
+ - **Use the short, everyday word.** "Use", not "utilize". "Help", not "facilitate". "Do", not "perform".
9
+ - **When a rule makes a sentence worse, fix the sentence another way or leave it alone.** A sentence that follows every rule and sounds machine-written has failed.
10
+
11
+ ## Name things as the code does
12
+
13
+ The codebase is the word list. Write the real function, file, flag, table, or command, in code font: `emailUnread`, `RPLY_UNREAD_EMAILS`, `unread_emails`. Do not describe it ("the email helper") or rename it.
14
+
15
+ Do not invent jargon. Use the words a developer says out loud. Call each thing by one name everywhere: if a chapter says "the sweep", the groups and the chat say "the sweep" too.
16
+
17
+ For the people who use the product (the lead, before and after), describe behaviour in their words instead: "you get one email", not "`emailUnread` sends".
18
+
19
+ ## Give each part of the tour one job
20
+
21
+ The tour shows the change. It does not grade it. Say what is true, what happens, and what to look at. Your view belongs only in Attention, as what deserves a close look and why.
22
+
23
+ - **Lead.** One sentence on what the change does for the people who use it. Behaviour, not mechanism: "Users who never turned on notifications get one email a day about unread agent messages."
24
+ - **Before and after.** Two or three plain sentences each, about what a person sees or gets. No code.
25
+ - **Chapter text.** Why this matters and what the step does, in one or two sentences. Name the condition that makes it interesting: "Two messages are 31 minutes old and one is fresh. Only the old ones count."
26
+ - **What should happen (`expect`).** Dry and checkable, one per instance, so the reviewer can compare it with the result: "No email." "One email, subject `You have 2 unread messages in rply`." No hedging and no explanation here.
27
+ - **Step results (overlay title and body).** The result as a fact: the title in a few words ("Blocked, nothing fetched"), the body with what you saw. Quote real output instead of describing it.
28
+ - **Captions and figure titles.** The title names the state in two to four words ("Arabic", "Dark theme"). The caption says what to notice in one sentence: "Base mirrors the controls. Head keeps − 100% +."
29
+ - **Attention.** Specific and concrete: the place, the condition, and the consequence. "A silent message counts as unread, but the app's badge skips it, so the email can say 2 where the badge says 1." For a claim: who said it, what they said, and what you found in the box.
30
+ - **Group `why`.** One sentence on why that part of the diff exists.
31
+
32
+ ## Answer in the chat like a briefing
33
+
34
+ - Answer first, in the first sentence. Then the evidence: what you ran, what came out, the file and line.
35
+ - Say what you changed and where, with the commit. Say plainly when you changed nothing.
36
+ - Keep it short enough to read in under a minute. Link logs and long output; do not paste them.
37
+ - When the answer is a change to lines in the diff, give it as a suggestion block for those lines.
38
+ - A comment the reviewer asks you to leave on GitHub is in their name: one point per comment, written as they would say it to the author.
39
+
40
+ ## Write sentences to the reader
41
+
42
+ - Use the present tense. "Will" only for things that happen later.
43
+ - Say who does what: "the sweep skips the user", not "the user is skipped".
44
+ - Put the condition before the result: "If the user has a push subscription, no email goes out."
45
+ - Put the common case first and exceptions after.
46
+ - One thought per sentence. Split a sentence that carries two. Split anything over about 25 words.
47
+ - Keep "the" and "a", and the small words that show structure. Do not compress into fragments or arrows: "Parser rejects bad date → exit 2" is "The parser rejects a bad date and exits with code 2."
48
+ - Keep "only" and "not" next to the word they change.
49
+ - Make every "it", "this", and "they" point at one obvious thing. Repeat the noun when in doubt.
50
+ - Use periods, not semicolons or dashes. No slashes ("a, b, or both", not "a/b"). No "etc.": say up front that a list is partial.
51
+ - Sentence case everywhere: headings, titles, labels. No exclamation marks, no emoji.
52
+
53
+ ## Vary the rhythm and be specific
54
+
55
+ A text can obey every rule and still read machine-written: every sentence clipped short, nothing specific.
56
+
57
+ - Mix sentence lengths. Short sentences land a point. Longer ones carry a fact with its condition.
58
+ - Be specific over sterile. Not "edge cases may cause issues" but "a user who reads everything between two sweeps gets no email".
59
+ - Prefer a number to an adverb: "after 31 minutes", "one email a day", "3 of 4 checks pass".
60
+ - If a sentence could appear unchanged in another review, it says nothing about this one. Cut it.
61
+
62
+ ## Cut these on sight
63
+
64
+ - **AI vocabulary:** additionally, crucial, delve, enhance, robust, seamless, comprehensive, leverage, utilize, pivotal, showcase, underscore, ensure (as filler), landscape, tapestry, testament.
65
+ - **Fancy ways to say "is":** "serves as", "stands as", "boasts", "features". Say "is" or "has".
66
+ - **"Not just X, but Y"**, forced groups of three, and false ranges ("from X to Y" with no scale between them).
67
+ - **Filler and hedging:** "it is worth noting", "basically", "essentially", "could potentially", "might possibly".
68
+ - **Chatbot phrases:** "Great question", "I hope this helps", "Let me know if", "Certainly". Answer directly.
69
+ - **Abstract metaphors:** surface (for API), wedge, vector, primitive, harness, scaffolding, north star, flywheel, ratchet. Name the concrete thing.
70
+ - **Feelings instead of mechanisms:** "the flow feels solid" says nothing. "Each step finished and posted its result on both instances" does.
71
+ - **Bold labels that restate the line** ("**Performance:** Performance improved") and bold on every term.
72
+
73
+ ## Example
74
+
75
+ Before:
76
+
77
+ > This chapter essentially showcases how the new unread email functionality robustly handles various edge cases, ensuring that users are only notified when appropriate.
78
+
79
+ After:
80
+
81
+ > A user with two 31-minute-old unread messages gets one email. A user whose messages are 5 minutes old, or who has push turned on, gets none.
82
+
83
+ ---
84
+
85
+ Adapted from the `technical-writing` and `unslop` skills in [cursor/plugins](https://github.com/cursor/plugins) (pstack), under the MIT License:
86
+
87
+ > Copyright (c) 2026 Lauren Tan
88
+ >
89
+ > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
90
+ >
91
+ > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
92
+ >
93
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,98 @@
1
+ // A complete review, for joshuaKnauber/rply#16: a backend fix with no visible UI, so the evidence is flows and overlays.
2
+ // To try it with the sample data in src/data, copy it over src/review.tsx and change the imports to "./kit".
3
+ import { Attention, BeforeAfter, Card, Chapter, defineReview, Flow, Lead, Outcome, Overview, type FlowDef } from "../src/kit";
4
+
5
+ const UNFURL = "packages/server/src/routes/unfurl.ts";
6
+
7
+ const ssrf: FlowDef = {
8
+ id: "ssrf",
9
+ title: "Link preview request path",
10
+ nodes: [
11
+ { id: "route", title: "GET /v1/unfurl", sub: "authenticated member", col: 0, row: 0 },
12
+ { id: "guard", title: "assertPublic", sub: "isPrivate(address)", col: 1, row: 0 },
13
+ { id: "internal", title: "Loopback / metadata", sub: "127.0.0.1:9199", col: 2, row: 0 },
14
+ ],
15
+ edges: [["route", "guard"], ["guard", "internal"]],
16
+ changed: ["guard"],
17
+ };
18
+
19
+ export default defineReview({
20
+ instances: [
21
+ { id: "base", label: "Base", detail: "main", port: 7781 },
22
+ { id: "head", label: "Head", detail: "this PR", port: 8080 },
23
+ ],
24
+
25
+ overview: () => (
26
+ <Overview>
27
+ <Lead>Link previews can no longer reach the server's own network through IPv6 spellings of private addresses.</Lead>
28
+ <BeforeAfter
29
+ before={<>The guard only stripped a dotted <code>::ffff:</code> prefix. URL parsing turns <code>[::ffff:127.0.0.1]</code> into <code>[::ffff:7f00:1]</code>, which looked public, so the server fetched its own loopback and returned the page.</>}
30
+ after="The guard reads every address as eight 16-bit groups, judges IPv6 that embeds IPv4 as that IPv4, and blocks private ranges before any fetch. Addresses it cannot parse are blocked too."
31
+ />
32
+ <Card caption="Dashed: changed in this PR. The tour lights it up on base and head.">
33
+ <Flow def={ssrf} />
34
+ </Card>
35
+ <Attention
36
+ items={[
37
+ { text: "Addresses the parser cannot read now count as private", detail: "Failing closed is right for a guard, but any odd resolver output now blocks a preview.", to: { group: "core" } },
38
+ { claim: { source: "PR description", quote: "The guard now parses each address and rejects the full loopback, link-local, unique-local, multicast, mapped, and NAT64 ranges before it fetches.", status: "neutral", label: "Mostly true" }, detail: "True for those ranges, but 6to4 (2002::/16), SIIT and 2001:db8::/32 are still treated as public, so \u201creserved\u201d in the title is broader than what is checked. None reach loopback on a normal host.", to: { chapter: "other-forms" } },
39
+ { text: "DNS rebinding still bypasses the guard", detail: "assertPublic and fetch resolve the host separately. The PR says so and leaves it for later." },
40
+ ]}
41
+ held={[
42
+ { source: "PR description", quote: "A URL whose host is an IPv4-mapped IPv6 address such as ::ffff:7f00:1 passed the private-address check, so the server connected to loopback.", to: { chapter: "mapped-loopback" } },
43
+ { source: "PR description", quote: "Parsing the address into groups and checking the full ranges classifies all 15 private test addresses as private and keeps the 5 public ones public.", to: { chapter: "unit-tests" } },
44
+ ]}
45
+ />
46
+ </Overview>
47
+ ),
48
+
49
+ chapters: [
50
+ {
51
+ id: "mapped-loopback",
52
+ title: "A loopback page through ::ffff:7f00:1",
53
+ page: () => (
54
+ <Chapter
55
+ title="A loopback page through ::ffff:7f00:1"
56
+ step="mapped-loopback"
57
+ expect={{ base: "Returns the internal page, titled “internal”.", head: "Returns no title. The guard blocks before any fetch." }}
58
+ flow={ssrf}
59
+ here="guard"
60
+ group="core"
61
+ >
62
+ Signs in as the demo user on each instance and asks for a preview of <code>http://[::ffff:7f00:1]:9199/</code>, a page only reachable on the server's loopback.
63
+ </Chapter>
64
+ ),
65
+ },
66
+ {
67
+ id: "other-forms",
68
+ title: "Other spellings of private addresses",
69
+ page: () => (
70
+ <Chapter
71
+ title="Other spellings of private addresses"
72
+ step="other-forms"
73
+ expect={{ base: "The mapped and NAT64 spellings pass the guard.", head: "Every spelling is blocked." }}
74
+ flow={ssrf}
75
+ here="guard"
76
+ group="parse"
77
+ >
78
+ Tries the metadata address in mapped hex form, dotted-mapped, the long zero form, <code>fe90::</code>, multicast <code>ff02::</code> and NAT64.
79
+ </Chapter>
80
+ ),
81
+ },
82
+ {
83
+ id: "unit-tests",
84
+ title: "The PR's own tests",
85
+ page: () => (
86
+ <Chapter title="The PR's own tests" step="unit-tests" expect={{ head: "15 private and 5 public addresses classified right, and the mapped-loopback test passes." }} group="tests" evidence={<Outcome empty="Press Run: the PR's test file runs against head." />}>
87
+ Runs the PR's test file against head: the address table and an end-to-end test against a real server on loopback.
88
+ </Chapter>
89
+ ),
90
+ },
91
+ ],
92
+
93
+ groups: [
94
+ { id: "core", title: "Embedded IPv4 is judged as IPv4", why: "The fix itself. isPrivate first asks for the IPv4 inside an IPv6 address, then checks the IPv6 ranges.", chapter: "mapped-loopback", file: UNFURL, lines: [[79, 107]] },
95
+ { id: "parse", title: "Addresses become numbers", why: "Prefix string checks are replaced by parsing into eight groups, so every spelling of one address is the same address.", chapter: "other-forms", file: UNFURL, lines: [[109, 128]] },
96
+ { id: "tests", title: "Tests", why: "An address table, and an end-to-end test against a real server on loopback.", chapter: "unit-tests", file: "packages/server/test/unfurl.test.ts", lines: [[1, 8], [49, 76]] },
97
+ ],
98
+ });
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Review</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>