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/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  Add a section on top with a new version to release it: pushing it to main publishes that version to npm.
4
4
  Prerelease versions like `0.1.0-prototype.1` go to the dist-tag named after them (`prototype`), plain versions to `latest`.
5
5
 
6
+ ## 0.1.0-prototype.2
7
+
8
+ - `bottleship review <pr>` brings a PR up in a box, with its base and head running side by side. An agent there builds a guided tour that opens in your browser: an overview, chapters that show the change in the running app, and the diff in tour order. Your comments go into your pending review on GitHub, and you submit it from the tour with a summary and a verdict. The Pull request page shows the checks and merges when you are allowed to.
9
+ - `bottleship review` with no PR does the same for your checkout's local changes, and leaves the checkout untouched.
10
+ - The tour has a chat with an agent. It knows the page you are on and takes selections, comments and screenshots as context. In a PR review the box agent answers, and commits fixes to the PR's branch when you ask. In a review of local changes your own agent answers, changes your checkout, and the tour updates.
11
+ - `bottleship init` also sets up the repo you run it in: your local agent works out how the repo runs in a box and writes `.bottleship/`, checking its work in real boxes. On a machine that is already set up, it goes straight to the repo.
12
+ - `bottleship box list` shows the boxes on this machine with their reviews, and `bottleship box rm` removes one.
13
+ - Breaking: the commands for coding agents moved under `bottleship agent`: `new`, `status`, `ask`, `exec` and `rm`, plus the new `list`, `review`, `review next` and `onboard`. `bottleship agent help` is their reference. To update your agent, replace the "## Remote agents with bottleship" section in its instructions file with the one in the README.
14
+ - Providers can have a `list` script, which `box list` uses. The builtin `docker` provider has one.
15
+
16
+ ## 0.1.0-prototype.1
17
+
18
+ - The last step of `bottleship init` opens your local agent (Claude Code, Codex, opencode or Cursor) to add bottleship to its global instructions, instead of showing text to copy.
19
+ - `bottleship init --reset` starts over: sets the config aside as `config.json.bak` and asks everything again, and on a terminal offers to forget saved logins.
20
+ - Fixed: a status check could read a box's state while it was being written and report a waiting login as running, or a finished agent as stopped.
21
+ - Fixed: removing a box that was still waiting for its device login left the login process running.
22
+
6
23
  ## 0.1.0-prototype.0
7
24
 
8
25
  First prototype. `bottleship init` sets up a provider and a coding agent once, then your local agent runs other agents in remote boxes with `new`, `status`, `ask`, `exec` and `rm`.
package/README.md CHANGED
@@ -1,134 +1,92 @@
1
1
  # bottleship (prototype)
2
2
 
3
- A small CLI your local coding agent uses to run other coding agents in remote sandboxes ("boxes"), like subagents. Your agent writes the prompt, starts a box, checks on it, follows up, and cleans up. The agent in the box clones the repo, works, commits, pushes, and opens a PR.
3
+ Review a pull request by watching it run. `bottleship review 17` starts the PR in a box, an isolated dev environment with base and head running side by side. An agent there builds a guided tour that opens in your browser: what the PR does, shown in the running app, then the diff in tour order. You comment, chat with the agent, and submit your review to GitHub from the tour.
4
4
 
5
- bottleship is stateless: no daemon, no local task list. The box id is the handle, and everything about a run lives in the box.
5
+ The same boxes let your coding agent hand work to other agents, like subagents that each have their own copy of the app.
6
6
 
7
- This README is written for agents. Humans: run `bottleship init` once, then tell your agent to read this file.
7
+ ## Quick start
8
8
 
9
- ## Install
10
-
11
- ```sh
12
- npm install -g <path or git URL of this package> # will be `npm i -g bottleship` once published
13
- ```
14
-
15
- Needs Node >= 22.18 on this machine. Installing from a git URL or a checkout builds the TypeScript sources (`npm install` runs `npm run build`).
16
-
17
- ## Onboarding
18
-
19
- Once per machine, in a terminal:
9
+ You need Node 22.18 or newer, Docker, and the GitHub CLI logged in (`gh auth login`).
20
10
 
21
11
  ```sh
12
+ npm install -g bottleship@prototype
13
+ cd your-repo
22
14
  bottleship init
15
+ bottleship review 17
23
16
  ```
24
17
 
25
- It asks where boxes run (provider), which agent runs in them (`claude`, `codex`, `opencode`, `cursor`), how that agent logs in, and the git name and email for their commits, then collects the secrets boxes need and runs a live check: create a box, run a command, set up the agent, get one reply, destroy the box. Re-running it re-checks everything and only asks for what is missing.
18
+ `bottleship init` asks a few questions, checks that a box can run an agent, then has your local coding agent set up the repo with you. The first box builds the base image, which takes a few minutes.
26
19
 
27
- An agent can run it non-interactively; it never prompts and prints JSON:
20
+ ## Commands
28
21
 
29
22
  ```sh
30
- bottleship init --yes --provider docker --agent claude --git-name "Ada Lovelace" --git-email ada@example.com
23
+ bottleship init [--reset] # set up this machine, then the repo you are in
24
+ bottleship review [<pr>] # review a PR, or this checkout's changes without a PR
25
+ bottleship review --base <ref> # review the changes against another base
26
+ bottleship box list # the boxes on this machine, with their reviews
27
+ bottleship box rm [<id>] # remove a box; without an id, pick one
28
+ bottleship help
31
29
  ```
32
30
 
33
- `--provider ssh` also needs `--host <host>`; `--provider custom --name <name>` sets up a custom provider (see below). `local` and `ssh` run agents with no isolation, so init needs the user's explicit OK: a confirm in the terminal, or `--i-understand-no-isolation`. Only pass that flag when the user said so.
31
+ A PR is a number in the current repo, `owner/name#17`, or a link to the PR.
34
32
 
35
- For an agent on the device login route (codex by default), the live check stops at the login with `login_needs_human`, since someone has to approve it in a browser. Ask the user to run `bottleship init` in their terminal.
33
+ Your coding agent has its own commands under `bottleship agent`. See [For your coding agent](#for-your-coding-agent).
36
34
 
37
- If a secret is missing it fails with `secret_missing`. Then ask the user to run `bottleship init` in their own terminal, where it asks with a hidden prompt. Never ask the user to paste a secret to you.
35
+ ## Reviewing a PR
38
36
 
39
- ### Secrets
37
+ `bottleship review 17` reads the PR from GitHub, starts a box, opens the review in your browser, and shows progress in the terminal until the tour is ready. Ctrl-C stops watching, and the review keeps going.
40
38
 
41
- Box env vars are configured under `env` in `~/.bottleship/config.json` (and per agent). A value is a plain string or a secret source:
39
+ The tour has four parts, in the rail on the left:
42
40
 
43
- | Source | Meaning |
44
- |---|---|
45
- | `{ "store": "claude" }` | bottleship's store: macOS Keychain, or a 0600 file under `~/.bottleship/secrets` |
46
- | `{ "command": "gh auth token" }` | the output of a command on this machine |
47
- | `{ "env": "CORP_KEY" }` | an environment variable of the bottleship process |
41
+ - **Overview.** What the PR does in one sentence, before and after, where it happens, and what deserves a close look. Every claim in the description, comments and reviews is checked in the box.
42
+ - **Chapters.** The change in the running app. Each chapter says what should happen on base and on head, runs a step, and shows the evidence: live instances, screenshots side by side, and flows traced through the code.
43
+ - **Changes.** The diff grouped by chapter, each group with why it exists, then the files no chapter covers.
44
+ - **Finish review** and **Pull request.** Your comments, a summary and a verdict, then the PR's checks and a merge button when you are allowed to merge.
48
45
 
49
- Defaults: `GH_TOKEN` (git pushes and `gh` both use it) from the store, plus the agent's login (below). Init asks for a [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) limited to the repos boxes work on, with Contents and Pull requests read and write (Metadata read comes with them), so a box cannot touch the rest of the account. `gh auth token` (every repo the user can reach) is used only if the user picks it in init and confirms a warning. Either way, turn on branch protection for `main` so a box can only reach it through a reviewed PR.
46
+ Comments work as on GitHub, and land there as you write them. Press the + next to a line, or drag over several lines, to comment or suggest a change. Add single comment posts at once. Start a review puts the comment in your pending review, which you submit from Finish review.
50
47
 
51
- ### Agent logins
48
+ The Agent button in the footer opens a chat with the agent in the box. It knows the page you are on, and takes what you select, any comment, and any screenshot as context. It answers questions, and commits fixes to the PR's branch when you ask. It leaves comments on GitHub only when you ask, and only in your pending review.
52
49
 
53
- Each agent preset lists the login routes it offers (`login.routes`) and the one boxes use (`login.use`). Init asks on a first pick; pick another for one agent in `~/.bottleship/config.json`, e.g. `"agents": { "codex": { "login": { "use": "token" } } }`.
50
+ Only review PRs you trust. The box runs the PR's code with your GitHub token, so code in the PR could push, comment or approve as you, in every repo the token reaches.
54
51
 
55
- - `token`: a long-lived token or API key in bottleship's store, set as an env var in every box.
56
- - `device`: the agent logs in inside each box. `new` (and `ask`, if the box's login expired) starts the login and returns its URL and code for the user to approve in a browser; the task starts once they do. Nothing is stored on this machine.
57
- - `none`: no login.
52
+ ## Reviewing local changes
58
53
 
59
- | Agent | Routes (default first) | Why |
60
- |---|---|---|
61
- | `claude` | token: `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` | Anthropic's documented way to use a Claude subscription headless ([docs](https://code.claude.com/docs/en/authentication)). Boxes never get `ANTHROPIC_API_KEY` unless you configure it, as it would win over the token. |
62
- | `codex` | device: `codex login --device-auth` per box; token: `CODEX_API_KEY` (an OpenAI API key) | One ChatGPT login (`auth.json`) must not be shared across machines or parallel runs ([docs](https://learn.chatgpt.com/docs/auth/ci-cd-auth)), so each box logs in itself. Once, first: in ChatGPT, Settings, Security, turn on device code login for Codex. |
63
- | `cursor` (no reply tested yet) | token: `CURSOR_API_KEY` from https://cursor.com/dashboard/api (bills to your Cursor plan); device: `cursor-agent login` per box | API keys are Cursor's documented headless login ([docs](https://cursor.com/docs/cli/reference/authentication)). |
64
- | `opencode` | none: free models; device: a ChatGPT plan via `opencode auth login openai --method chatgpt-headless` per box; token: `OPENCODE_API_KEY` (OpenCode Zen), `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` | Per-box logins and provider API keys ([docs](https://opencode.ai/docs/)). Never a Claude subscription: Anthropic allows those only in Claude Code. |
65
-
66
- On `new`, the `.env*` files at the root of the current repo (not `*.example`, `*.sample`, `*.template`) are copied into the box workspace at the same paths.
54
+ Run `bottleship review` without a PR to review your checkout's changes: commits, uncommitted and untracked files, against where your branch left the default branch. The box gets a copy, and your checkout stays as it is. `.env` files never go into the diff.
67
55
 
68
- Real tokens are in the box, so the agent there can do anything those tokens allow: keep them narrow (a fine-grained `GH_TOKEN`, branch protection on `main`). A credential proxy that keeps them out of boxes is not built yet.
56
+ Your coding agent starts these reviews too, after a big change it made. Then the chat in the tour goes to that agent: it changes your checkout, the box picks up the new state, and the tour updates. When you press Looks good, your agent does what your note says.
69
57
 
70
- ## Providers
58
+ ## Boxes
71
59
 
72
- Where boxes run. Pick one with `bottleship init` (or `--provider <name>`, and `new --provider <name>` per box):
60
+ A box is a Docker container by default. Each one has the repo, its dependencies and services, and the app running. `bottleship box list` shows every box with its age and, for reviews, the review link. Boxes stay until you remove them with `bottleship box rm`.
73
61
 
74
- - `docker`: containers on this machine, from the box template (see Templates); without one, from `image` (default exeuntu).
75
- - `exe`: [exe.dev](https://exe.dev) VMs over ssh; needs `ssh exe.dev` working. No box template: exe.dev boots VMs only from registry images, so boxes install the agent at start.
76
- - `e2b` (untested): [e2b.dev](https://e2b.dev) cloud sandboxes; API key from https://e2b.dev/dashboard.
77
- - `daytona` (untested): [daytona.io](https://daytona.io) cloud sandboxes; API key from https://app.daytona.io/dashboard/keys.
78
- - `modal` (untested): [modal.com](https://modal.com) cloud sandboxes; token id and secret from `modal token new` or https://modal.com/settings/tokens.
79
- - `ssh` (untested on a real host): any machine you can `ssh` into without a password prompt; each box is a directory `~/bottleship-boxes/<id>` there, used as its `$HOME`. No isolation: agents run as that user with permission prompts off. Options: `host` (required, anything ssh accepts, e.g. a `~/.ssh/config` alias; `init --host`), `root`.
80
- - `local`: git worktrees on this machine. The box workspace is a `git worktree` of your local repo on branch `bottleship/<id>` (from your `HEAD` commit, without uncommitted changes); `rm` removes the worktree and keeps the branch. No isolation: agents run as you, with your files and logins, with permission prompts off. Boxes stop when the laptop sleeps.
81
- - `custom`: your company's sandboxes, see below.
62
+ ## Setting up a repo
82
63
 
83
- `local` and `ssh` need the user's OK in `bottleship init` (stored as `acceptNoIsolation` on the provider); without it `new` refuses them. `rm` there kills the box's processes: those marked with the box (`BOTTLESHIP_BOX`), and on macOS those in the box's process groups whose working directory is in the box.
64
+ A repo describes its box in `.bottleship/`, committed with the repo. `bottleship init` in the repo has your coding agent write it with you and check it in real boxes.
84
65
 
85
- Provider API keys are read on this machine only and never reach a box; `bottleship init` asks for them.
86
-
87
- "Untested" means built from the provider's docs and SDK and type-checked, with live tests in `test/live/` that run when the provider's keys are set, but never run against the real service yet.
88
-
89
- ## Custom providers
90
-
91
- For a company sandbox system with only an API or a CLI. A human runs `bottleship init` and picks `custom` (an agent: `bottleship init --yes --provider custom --name <name>`). That copies [presets/providers/http.example.ts](presets/providers/http.example.ts) to `~/.bottleship/providers/<name>.ts` and adds `"<name>": { "module": "providers/<name>.ts" }` under `providers` in `~/.bottleship/config.json`. Then, as the agent:
92
-
93
- 1. Replace every TODO in `~/.bottleship/providers/<name>.ts` with the real API calls, or, if the system has a CLI, run its commands with `kit.run`. Find out how the system creates a sandbox, runs a command in it, and deletes it (its docs, its CLI's `--help`, or ask the user).
94
- 2. Put the module's options in its config entry, with secrets as secret sources, never as plain values:
95
-
96
- ```json
97
- "providers": {
98
- "corp": { "module": "providers/corp.ts", "baseUrl": "https://sandboxes.corp.example/api", "apiKey": { "store": "corp-sandbox" } }
99
- }
100
- ```
66
+ | File | What it does |
67
+ |---|---|
68
+ | `.bottleship/Dockerfile` | Optional. Lines added to bottleship's base image (Debian bookworm, Node 22, git, gh, curl, ripgrep, a user `box` without sudo), run as root. No `FROM`, `COPY` or `ADD`. `EXPOSE <port>` marks the ports to open; bottleship publishes them on 127.0.0.1 and waits for them. |
69
+ | `.bottleship/setup.sh` | Runs once as `box` in `~/workspace`: dependencies, databases, seed data, build. Must not prompt. |
70
+ | `.bottleship/start.sh` | Starts the services, then the app in the foreground. It must listen on 0.0.0.0. The log is in `~/.bottleship/app.log`. |
101
71
 
102
- 3. Ask the user to run `bottleship init` in their terminal. It asks for missing secrets with a hidden prompt, then checks the provider end to end: create a box, run a command, set up the agent, get one reply, destroy the box. Fix what fails and run it again.
72
+ Boxes also get the `.env` files at the root of your checkout, but not `.env.example` and the like. The image is built once per Dockerfile and reused after that.
103
73
 
104
- The contract is `Provider` in [src/types.ts](src/types.ts): `create` (resolves once the box can run commands), `exec` (one-shot `bash -c`, with stdin), `destroy`, and optionally `status`, `openStream` (a binary-clean long-lived exec, for `exec -i`) and `template` (build or find a box template from a `TemplateSpec`, see Templates; `create` then gets its reference). Boxes must stay up until `rm`, since the agent runs in the background between calls. Node runs the `.ts` module directly, no build step. For a system whose CLI fits in shell templates, `"module": "builtin:command"` needs no code, see [presets/providers/command.ts](presets/providers/command.ts).
74
+ ## Configuration
105
75
 
106
- ## Usage
76
+ `bottleship init` writes `~/.bottleship/config.json`. Run it again to fill in anything missing, or with `--reset` to start over. The old config is kept as `config.json.bak`.
107
77
 
108
- Every command prints JSON; errors are `{ "error", "message", "fix" }` with exit code 1.
78
+ ### Agents
109
79
 
110
- ```sh
111
- bottleship new "<task>" [--repo owner/name | --no-repo] [--provider P] [--agent A]
112
- # { "id": "docker:bs-fix-login-bug-3fa2", "provider": "docker", "agent": "claude", "repo": "acme/web", "next": "..." }
113
- # device login route: { "id", "provider", "agent", "repo", "login": { "url", "code"?, "agent" }, "next": "show the user this URL and code; ..." }
114
-
115
- bottleship status <id> [--wait 600] [--lines 40]
116
- # { "id", "box": "running", "agent": "running" | "exited" | "stopped", "exitCode"?, "log": "<last lines>", "next" }
117
- # { "id", "box": "running", "agent": "waiting_for_login", "login": { "url", "code"?, "agent" }, "log", "next" }
118
- # { "id", "box": "running", "agent": "login_failed", "exitCode", "log": "<the login's output>", "next" }
119
- # { "id", "box": "gone", "next" }
120
-
121
- bottleship ask <id> "<follow-up>" # continues the same conversation; refused while the agent runs
122
- # { "id", "agent": "running", "next" }, or with an expired device login { "id", "agent": "waiting_for_login", "login", "next" }
123
- bottleship exec <id> -- git log --oneline -5
124
- bottleship exec <id> -- "npm test 2>&1 | tail -20" # one argument is a script, several are argv
125
- bottleship exec -i <id> -- <cmd> # streams stdin/stdout, e.g. for an IDE attaching over ACP
126
- bottleship rm <id>
127
- ```
80
+ The agent in a box is `claude`, `codex`, `cursor` or `opencode`. Each gets one long-lived login, which init asks for:
128
81
 
129
- `new` returns as soon as the agent has started, or, on the device route, once the login shows its URL: show the user the URL and code, and the task starts after they approve. A login nobody approves within 15 minutes fails (`login_failed`); `ask` then logs in again and, if the task never ran, runs the message as the task. The repo defaults to the current directory's `origin` (with the `local` provider, a worktree of this checkout). The box gets the env, the agent's login, the git identity, a git credential helper that uses `$GH_TOKEN`, and the agent's install commands; then the agent runs detached in `~/workspace` with your prompt.
82
+ | Agent | Env var | Where it comes from |
83
+ |---|---|---|
84
+ | `claude` | `CLAUDE_CODE_OAUTH_TOKEN` | `claude setup-token`, to use a Claude subscription headless ([docs](https://code.claude.com/docs/en/authentication)). Boxes never get `ANTHROPIC_API_KEY` unless you configure it, since it would win over the token. |
85
+ | `codex` | `CODEX_API_KEY` | an OpenAI API key, billed to the API, not a ChatGPT plan |
86
+ | `cursor` | `CURSOR_API_KEY` | an API key from https://cursor.com/dashboard/api, billed to your Cursor plan |
87
+ | `opencode` | `OPENCODE_API_KEY` | an OpenCode Zen key from https://opencode.ai/console |
130
88
 
131
- Agents are configured as shell templates (`{prompt}` is filled in shell-quoted):
89
+ An agent is a set of shell templates. The presets are in [presets/agents](presets/agents), and an entry under `agents` in the config overrides only the fields it names:
132
90
 
133
91
  ```json
134
92
  "agents": {
@@ -138,47 +96,94 @@ Agents are configured as shell templates (`{prompt}` is filled in shell-quoted):
138
96
  "resume": "claude -p {prompt} --continue --dangerously-skip-permissions",
139
97
  "env": { "IS_SANDBOX": "1" },
140
98
  "unset": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
141
- "login": { "routes": ["token"], "use": "token", "token": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token" } }
99
+ "login": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token" }
142
100
  }
143
101
  }
144
102
  ```
145
103
 
146
- `unset` drops env vars from the box unless you configure them. A device login is `"device": { "command", "status", "setup"?, "timeout"? }`: `command` prints a URL (and code) and exits 0 once approved, `status` exits 0 when the box is logged in. An entry for a builtin agent only needs the fields it changes. The presets are in [presets/agents](presets/agents).
104
+ ### Secrets and tokens
147
105
 
148
- If a box has no Node, bottleship installs it into `~/.bottleship/node` without sudo.
106
+ Boxes get `GH_TOKEN`, which git and gh both use, and the agent's login. Other env vars go under `env` in the config, as a plain string or one of these sources:
149
107
 
150
- ## Templates
108
+ | Source | Meaning |
109
+ |---|---|
110
+ | `{ "store": "claude" }` | bottleship's store: the macOS Keychain, or a 0600 file under `~/.bottleship/secrets` |
111
+ | `{ "command": "gh auth token" }` | the output of a command on this machine |
112
+ | `{ "env": "CORP_KEY" }` | an environment variable of the bottleship process |
113
+
114
+ Init asks for a [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) limited to the repos boxes work on, with Contents and Pull requests set to read and write. Your full gh login (`gh auth token`) is used only if you pick it and confirm a warning. Either way, protect `main` so a box reaches it only through a reviewed PR.
115
+
116
+ The agent in a box can do anything its tokens allow, so keep them narrow. A box that only runs the repo's scripts, such as a setup check, gets no secrets. A proxy that keeps tokens out of boxes is not built yet.
151
117
 
152
- Boxes start from a prebuilt template when the provider supports one (`docker` builds a local image, `e2b` a template, `daytona` a snapshot, `modal` a named image; `exe`, `local`, `ssh` and custom ones have none). `bottleship init` builds it in the live check ("build the box template", minutes the first time) and records it under `templates.<provider>` in `~/.bottleship/config.json` as `{ "name", "ref" }`.
118
+ ## Providers
119
+
120
+ A provider decides where boxes run. It is a directory of bash scripts. `docker` ships in [presets/providers/docker](presets/providers/docker). For anything else, such as a company's sandboxes, a cloud sandbox service or a VM over ssh, put the same scripts in `~/.bottleship/providers/<name>/` and pick it in `bottleship init`. A provider there with a builtin's name replaces the builtin.
153
121
 
154
- The template is one Dockerfile: `node:22-bookworm-slim` with git, curl, ca-certificates, gh and ripgrep, a non-root user `box` (uid 1000, home `/home/box`) whose npm globals go to `~/.bottleship/npm`, plus the configured agent's `install` commands run as `box`. Its name is `bottleship-<hash of the Dockerfile>`, so changing the agent, its install commands or bottleship's Dockerfile makes a new one, built on the next `init`; a rerun with no changes finds the existing one. Templates never hold secrets or logins; those go into each box at start.
122
+ | Script | Contract |
123
+ |---|---|
124
+ | `create <id> <dockerfile> <image>` | Build the box from the Dockerfile, cached as `<image>`, which changes whenever the Dockerfile does. Start it with every `EXPOSE`d port reachable from this machine, and exit 0 once it runs commands. The box stays up until `destroy`. |
125
+ | `exec <id> <script>` | Run `bash -c <script>` in the box as its non-root user, with `$HOME` its home. Pass stdin, stdout, stderr and the exit code through, binary-clean and streaming. |
126
+ | `status <id>` | Print `running` or `gone`. |
127
+ | `destroy <id>` | Remove the box and everything it runs. |
128
+ | `url <id> <port>` | Optional. Print a URL on this machine for a port the box exposes. |
129
+ | `list` | Optional. Print the boxes bottleship made, one per line: the id, then optionally a space and the creation time in ISO 8601. Without it, `box list` shows none of this provider's boxes. |
155
130
 
156
- Only the configured agent is baked in. `new --agent <other>` still uses the template and installs the other agent at start. When the recorded template does not match the current config, `new` starts the box without one (setup installs what it lacks, only slower) and says so in `warning`; run `bottleship init` to rebuild. Providers without `template()` (`local`, `ssh`, custom ones) set boxes up at start as before.
131
+ The scripts run on this machine with bottleship's environment. They read the credentials they need themselves, from an env var, the keychain or a CLI's own login, and never hardcode them.
157
132
 
158
- ## Instructions for your agent
133
+ ## For your coding agent
159
134
 
160
- `bottleship init` prints this block; add it to your agent's global instructions (e.g. `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`).
135
+ `bottleship init` adds this section to your agent's global instructions, such as `~/.claude/CLAUDE.md` or `~/.codex/AGENTS.md`. If you skipped that step, add it yourself:
161
136
 
162
137
  ```md
163
138
  ## Remote agents with bottleship
164
139
 
165
- You can run other coding agents in remote sandboxes ("boxes") with the `bottleship` CLI, like subagents. Every command prints JSON; errors carry a `fix`.
140
+ The `bottleship` CLI runs coding agents in boxes, isolated dev environments for a repo (Docker containers by default), and builds guided reviews of PRs and of local changes, which the user opens in their browser. Your commands are under `bottleship agent`; run `bottleship agent help` before you first use them in a session, for what each does and how to use it.
166
141
 
167
- - Start remote work with `bottleship new "<prompt>"` from inside the repo (or `--repo owner/name`). Write a complete prompt: the task, the context it needs, and what done means, including "work on branch <name>, commit and push as you go, and open a PR when done". The box agent sees nothing of this conversation.
168
- - Treat each box as a subagent you manage. `new` returns an id; keep track of your ids yourself, bottleship keeps no list.
169
- - If `new` or `ask` returns a `login`, show the user its URL and code; the box agent starts once they approve it in their browser.
170
- - `bottleship status <id>` shows whether the agent is still running and the tail of its output; `--wait <seconds>` blocks until it finishes.
171
- - `bottleship ask <id> "<follow-up>"` continues the same conversation once the agent has finished.
172
- - `bottleship exec <id> -- <cmd>` inspects the box (git log, test output, files).
173
- - `bottleship rm <id>` when the work is merged or abandoned. If a box is gone, start a new one on the pushed branch.
174
- - If the user wants to watch or steer a box from their IDE, find out how their IDE attaches to an agent over a command (ACP) and give it `bottleship exec -i <id> -- <the agent's ACP command>`.
142
+ - Use a box for work that can run on its own while you do something else, and treat it as a subagent you manage.
143
+ - When the user wants to review a PR, start the review for them and give them its url.
144
+ - Reviews of your own work: after a big change you made on your own (many files, or behaviour that is hard to see in the diff), start a local review before you hand over and give the user its url, unless the user has said otherwise. Skip it for small changes.
145
+ - If the user stops or interrupts you while you start a review, or turns one down, ask them once when they want reviews (after every big change, only when they ask, never, or something else). Then rewrite the line above in this section of your instructions file to say exactly what they chose, and follow it from then on.
175
146
  - Secrets: never ask the user to paste a secret to you. If bottleship reports one missing, ask them to run `bottleship init` in their terminal.
176
147
  ```
177
148
 
149
+ `bottleship agent help` is the full reference for agents. Each command prints JSON, errors are `{ "error", "message", "fix" }` with exit code 1, and most results say in `next` what to do next. In short:
150
+
151
+ - `agent new`, `status`, `ask`, `exec`, `list` and `rm` run another agent in a box and manage it.
152
+ - `agent review` starts a review and returns its link. For local changes, `agent review next` waits for what you send from the tour's chat and sends back the agent's updates.
153
+ - `agent onboard` gives the instructions for setting up a repo, and `agent onboard --check` tries them in a box.
154
+ - `agent init --yes` sets up the machine without prompts.
155
+
156
+ ## How a review works
157
+
158
+ These details are for working on bottleship or on a tour.
159
+
160
+ In a review box, `~/workspace` is the PR's head and `~/base` is the commit it branched off. The box environment is the PR's `.bottleship/` if it has one, otherwise your local checkout's, otherwise none and the agent works it out.
161
+
162
+ The review server listens on port 7777 from the start, so the link works at once and shows that the review is being set up. Ports 7781 to 7784 are free for extra instances, such as base. The server keeps the review's state in the box: `~/review/comments.json`, `verdict.json` and `updates.json`.
163
+
164
+ `~/review/app` is a copy of [review-app](review-app), a Vite and React app with the PR's data in `src/data`. The agent writes `src/review.tsx`, step scripts in `~/review/steps/<id>.sh`, and screenshots in `~/review/shots`. [review-app/KIT.md](review-app/KIT.md) tells it how to build a tour from the kit, and [review-app/WRITING.md](review-app/WRITING.md) how to write one. `examples/rply-16.tsx` is a complete tour.
165
+
166
+ Steps and review hooks, which are temporary code in the box's copy of the app, report what happens by posting events to `$BOTTLESHIP_REVIEW_URL/api/events`:
167
+
168
+ ```json
169
+ { "instance": "base", "kind": "hook", "at": "push/index.ts:47", "text": "setPresence key=…/ada-7f3c",
170
+ "flow": { "id": "presence", "edge": ["sync", "pres"], "node": "pres", "status": "fail", "label": "…/ada-7f3c → deploys" },
171
+ "overlay": { "title": "No push for Ada", "status": "fail" } }
172
+ ```
173
+
174
+ `kind` is `request`, `hook`, `ok` or `fail`. `flow` moves a dot along an edge and sets a node's state and label. `overlay` shows a note over that instance. The server tags each event with the step that was running, and answers 400 with the problems when an event is malformed.
175
+
176
+ For GitHub Enterprise, log gh in to that host (`gh auth login --hostname <host>`) and make `GH_TOKEN` a token for it.
177
+
178
178
  ## Development
179
179
 
180
180
  ```sh
181
181
  npm test # build, then all tests
182
- npx vitest run test/cli.test.ts # one file (needs a build first)
183
- grep -rn 'ponytail:' src presets # deliberate shortcuts and their upgrade paths
182
+ npx vitest run test/cli.test.ts # one file, after a build
183
+ grep -rn 'ponytail:' src # deliberate shortcuts and their upgrade paths
184
+ cd review-app && npm run playground # the review kit against recorded reviews, gallery at #/gallery
184
185
  ```
186
+
187
+ Tests run boxes with the `dir` provider in [test/fixtures/providers/dir](test/fixtures/providers/dir), which uses directories on this machine, so no Docker is needed.
188
+
189
+ To release, add a section with the new version on top of [CHANGELOG.md](CHANGELOG.md) and set the same version in `package.json`. Merging it to `main` publishes it to npm.
@@ -1,64 +1,66 @@
1
- import { BsError } from "./config.ts";
2
- import type { Config, LoadedProvider, TemplateSpec } from "./types.ts";
3
- export interface Repo {
4
- url: string;
5
- label: string;
6
- }
1
+ import { type Provider } from "./provider.ts";
2
+ import type { Repo, RepoEnv } from "./repo.ts";
3
+ import type { Config } from "./types.ts";
7
4
  export interface Box {
8
5
  id: string;
9
- provider: LoadedProvider;
6
+ provider: Provider;
10
7
  box: string;
11
- warning?: string;
12
- login?: Login;
13
- }
14
- export interface Login {
15
- url: string;
16
- code?: string;
17
- agent: string;
18
8
  }
19
9
  export type AgentState = {
20
10
  agent: "running";
11
+ env?: string;
21
12
  log: string;
22
13
  } | {
23
14
  agent: "exited";
24
15
  exitCode: number;
16
+ env?: string;
25
17
  log: string;
26
18
  } | {
27
19
  agent: "stopped";
20
+ env?: string;
28
21
  log: string;
29
- } | {
30
- agent: "waiting_for_login";
31
- login?: Login;
32
- log: string;
33
- } | {
34
- agent: "login_failed";
35
- exitCode: number;
36
- log: string;
37
22
  };
38
- export declare const goneFix = "the box is gone. Start a new one with `bottleship new \"<task>\"` and tell its agent to continue on the branch the old box pushed";
39
- export declare function templateSpec(cfg: Config, agentName: string): TemplateSpec;
40
- export declare function currentTemplate(cfg: Config, provider: string): string | undefined;
23
+ export declare const goneFix = "the box is gone. Start a new one with `bottleship agent new \"<task>\"` and tell its agent to continue on the branch the old box pushed";
24
+ export declare function boxImage(cfg: Config, agentName: string, repoEnv?: RepoEnv, extra?: string): {
25
+ dockerfile: string;
26
+ image: string;
27
+ };
41
28
  export declare function parseId(cfg: Config, handle: string): {
42
29
  provider: string;
43
30
  box: string;
44
31
  };
45
- export declare function newBoxId(prompt: string): string;
32
+ export declare function newBoxId(label: string): string;
46
33
  export declare function openBox(cfg: Config, handle: string): Promise<Box>;
47
- export interface SetupOptions {
48
- agent: string;
49
- prompt: string;
50
- repo?: Repo;
34
+ export interface BoxOptions {
35
+ provider: string;
36
+ label: string;
37
+ agent?: string;
38
+ workspace?: {
39
+ clone: Repo;
40
+ } | {
41
+ tar: Buffer;
42
+ };
43
+ repoEnv?: RepoEnv;
51
44
  dotenv?: Record<string, string>;
45
+ pr?: {
46
+ number: number;
47
+ base: string;
48
+ };
49
+ env?: Record<string, string>;
50
+ dockerfile?: string;
52
51
  }
53
- export declare function setupAndStart(cfg: Config, b: Box, { agent: agentName, prompt, repo, dotenv }: SetupOptions): Promise<void>;
54
- export declare function missingSecrets(names: string[]): BsError;
55
- export declare function newBox(cfg: Config, opts: SetupOptions & {
56
- provider: string;
57
- source?: string;
58
- }): Promise<Box>;
59
- export declare function parseLogin(output: string, agent: string): Login | undefined;
52
+ export declare function createBox(cfg: Config, opts: BoxOptions): Promise<Box>;
53
+ export declare function putTar(b: Box, dir: string, tar: Buffer): Promise<void>;
54
+ export declare function putFiles(b: Box, files: Record<string, string>): Promise<void>;
55
+ export declare const runIn: (b: Box, script: string) => Promise<import("./types.ts").ExecResult>;
56
+ export type Stage = "setup" | "start" | "ready" | "failed";
57
+ export declare function prepare(b: Box, seconds?: number): Promise<{
58
+ stage: Stage;
59
+ message?: string;
60
+ log?: string;
61
+ }>;
62
+ export declare function startAgent(b: Box, prompt: string): Promise<void>;
60
63
  export declare function agentState(b: Box, lines?: number): Promise<AgentState>;
61
- export declare function loginState(b: Box, setup?: string, seconds?: number): Promise<AgentState>;
62
64
  export declare function waitAgent(b: Box, seconds: number, lines?: number): Promise<AgentState>;
63
- export declare function ask(b: Box, prompt: string): Promise<Login | undefined>;
65
+ export declare function ask(b: Box, prompt: string): Promise<void>;
64
66
  export declare const inBox: (script: string) => string;