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.
- package/CHANGELOG.md +17 -0
- package/README.md +118 -113
- package/dist/src/boxes.d.ts +41 -39
- package/dist/src/boxes.js +181 -179
- package/dist/src/cli.js +298 -83
- package/dist/src/config.d.ts +3 -6
- package/dist/src/config.js +19 -85
- package/dist/src/init.d.ts +3 -4
- package/dist/src/init.js +120 -205
- package/dist/src/local.d.ts +8 -0
- package/dist/src/local.js +24 -0
- package/dist/src/onboard.d.ts +5 -0
- package/dist/src/onboard.js +25 -0
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +0 -1
- package/dist/src/provider.d.ts +16 -0
- package/dist/src/provider.js +81 -0
- package/dist/src/repo.d.ts +21 -0
- package/dist/src/repo.js +75 -0
- package/dist/src/review/comments.d.ts +66 -0
- package/dist/src/review/comments.js +344 -0
- package/dist/src/review/contract.d.ts +82 -0
- package/dist/src/review/contract.js +189 -0
- package/dist/src/review/github.d.ts +120 -0
- package/dist/src/review/github.js +160 -0
- package/dist/src/review/local.d.ts +14 -0
- package/dist/src/review/local.js +70 -0
- package/dist/src/review/loop.d.ts +12 -0
- package/dist/src/review/loop.js +119 -0
- package/dist/src/review/pr.d.ts +14 -0
- package/dist/src/review/pr.js +68 -0
- package/dist/src/review/prompt.d.ts +15 -0
- package/dist/src/review/prompt.js +48 -0
- package/dist/src/review/serve.d.ts +1 -0
- package/dist/src/review/serve.js +8 -0
- package/dist/src/review/server.d.ts +14 -0
- package/dist/src/review/server.js +484 -0
- package/dist/src/review/setting-up.d.ts +4 -0
- package/dist/src/review/setting-up.js +74 -0
- package/dist/src/review/start.d.ts +32 -0
- package/dist/src/review/start.js +140 -0
- package/dist/src/review/target.d.ts +20 -0
- package/dist/src/review/target.js +66 -0
- package/dist/src/review/template.d.ts +3 -0
- package/dist/src/review/template.js +13 -0
- package/dist/src/review/terminal.d.ts +4 -0
- package/dist/src/review/terminal.js +91 -0
- package/dist/src/secrets.d.ts +1 -0
- package/dist/src/secrets.js +11 -1
- package/dist/src/types.d.ts +1 -71
- package/dist/src/ui.d.ts +6 -1
- package/dist/src/ui.js +22 -23
- package/dist/src/util.d.ts +11 -0
- package/dist/src/util.js +40 -0
- package/package.json +7 -14
- package/presets/agents/claude.json +1 -5
- package/presets/agents/codex.json +1 -10
- package/presets/agents/cursor.json +1 -9
- package/presets/agents/opencode.json +1 -14
- package/presets/providers/docker/create +12 -0
- package/presets/providers/docker/destroy +2 -0
- package/presets/providers/docker/exec +2 -0
- package/presets/providers/docker/list +4 -0
- package/presets/providers/docker/status +2 -0
- package/presets/providers/docker/url +4 -0
- package/review-app/KIT.md +106 -0
- package/review-app/WRITING.md +93 -0
- package/review-app/examples/rply-16.tsx +98 -0
- package/review-app/index.html +15 -0
- package/review-app/package-lock.json +2539 -0
- package/review-app/package.json +24 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/box.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/pr.diff +91 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/pr.json +62 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/review-comments.json +44 -0
- package/review-app/playground/fixtures/excalidraw-12602/events.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-ar-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-en-US-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-IL-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-dark.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-hover.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-page.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-tip.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-zen.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-ar-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-en-US-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-IL-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-dark.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-hover.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-page.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-tip.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-zen.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/instances.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/review.tsx +103 -0
- package/review-app/playground/fixtures/rply-16/data/box.json +1 -0
- package/review-app/playground/fixtures/rply-16/data/hooks.diff +28 -0
- package/review-app/playground/fixtures/rply-16/data/pr.diff +114 -0
- package/review-app/playground/fixtures/rply-16/data/pr.json +68 -0
- package/review-app/playground/fixtures/rply-16/data/review-comments.json +1 -0
- package/review-app/playground/fixtures/rply-16/events.json +156 -0
- package/review-app/playground/fixtures/rply-16/instances.json +1 -0
- package/review-app/playground/fixtures/rply-16/review.tsx +98 -0
- package/review-app/playground/gallery.tsx +127 -0
- package/review-app/playground/index.html +15 -0
- package/review-app/playground/main.tsx +26 -0
- package/review-app/playground/mock.ts +215 -0
- package/review-app/playground/playground.css +18 -0
- package/review-app/playground/vite.config.ts +32 -0
- package/review-app/scripts/check.cjs +47 -0
- package/review-app/scripts/clip.cjs +104 -0
- package/review-app/scripts/shot.cjs +66 -0
- package/review-app/src/App.tsx +132 -0
- package/review-app/src/data/box.json +1 -0
- package/review-app/src/data/hooks.diff +28 -0
- package/review-app/src/data/pr.diff +114 -0
- package/review-app/src/data/pr.json +68 -0
- package/review-app/src/data/review-comments.json +1 -0
- package/review-app/src/kit/agent.tsx +287 -0
- package/review-app/src/kit/chapter.tsx +178 -0
- package/review-app/src/kit/code.tsx +217 -0
- package/review-app/src/kit/comments.tsx +377 -0
- package/review-app/src/kit/finish.tsx +274 -0
- package/review-app/src/kit/flow.tsx +184 -0
- package/review-app/src/kit/index.ts +10 -0
- package/review-app/src/kit/kit.css +696 -0
- package/review-app/src/kit/live.ts +91 -0
- package/review-app/src/kit/media.tsx +348 -0
- package/review-app/src/kit/overview.tsx +161 -0
- package/review-app/src/kit/pr.ts +100 -0
- package/review-app/src/kit/pullrequest.tsx +298 -0
- package/review-app/src/kit/review.ts +74 -0
- package/review-app/src/kit/theme.ts +30 -0
- package/review-app/src/kit/ui.tsx +127 -0
- package/review-app/src/main.tsx +6 -0
- package/review-app/src/review.tsx +22 -0
- package/review-app/tsconfig.json +17 -0
- package/review-app/vite.config.ts +12 -0
- package/dist/presets/providers/command.d.ts +0 -3
- package/dist/presets/providers/command.js +0 -39
- package/dist/presets/providers/daytona.d.ts +0 -3
- package/dist/presets/providers/daytona.js +0 -173
- package/dist/presets/providers/docker.d.ts +0 -3
- package/dist/presets/providers/docker.js +0 -31
- package/dist/presets/providers/e2b.d.ts +0 -3
- package/dist/presets/providers/e2b.js +0 -124
- package/dist/presets/providers/exe.d.ts +0 -3
- package/dist/presets/providers/exe.js +0 -28
- package/dist/presets/providers/http.example.d.ts +0 -3
- package/dist/presets/providers/http.example.js +0 -47
- package/dist/presets/providers/local.d.ts +0 -3
- package/dist/presets/providers/local.js +0 -52
- package/dist/presets/providers/modal.d.ts +0 -3
- package/dist/presets/providers/modal.js +0 -117
- package/dist/presets/providers/ssh.d.ts +0 -3
- package/dist/presets/providers/ssh.js +0 -49
- package/dist/presets/providers/util.d.ts +0 -13
- package/dist/presets/providers/util.js +0 -102
- package/presets/providers/command.ts +0 -43
- package/presets/providers/daytona.ts +0 -181
- package/presets/providers/docker.ts +0 -38
- package/presets/providers/e2b.ts +0 -130
- package/presets/providers/exe.ts +0 -35
- package/presets/providers/http.example.ts +0 -64
- package/presets/providers/local.ts +0 -56
- package/presets/providers/modal.ts +0 -121
- package/presets/providers/ssh.ts +0 -54
- package/presets/providers/util.ts +0 -112
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// A local review and the developer's agent that asked for it: the agent waits for the reviewer's comments, changes
|
|
2
|
+
// the checkout, then updates the review, which copies the new state into the box and has the box agent refresh the tour.
|
|
3
|
+
import { BsError } from "../config.js";
|
|
4
|
+
import { ask, putFiles, putTar, runIn, waitAgent } from "../boxes.js";
|
|
5
|
+
import { dotenvFiles } from "../repo.js";
|
|
6
|
+
import { shq } from "../util.js";
|
|
7
|
+
import { localChanges } from "./local.js";
|
|
8
|
+
import { REVIEW_PORT } from "./start.js";
|
|
9
|
+
const api = `http://127.0.0.1:${REVIEW_PORT}/api`;
|
|
10
|
+
// the review server in the box, which only listens there
|
|
11
|
+
async function call(b, path, body, seconds = 30) {
|
|
12
|
+
const r = await b.provider.exec(b.box, `curl -s --max-time ${seconds} -w '\\n%{http_code}' -X POST -H 'content-type: application/json' --data-binary @- ${shq(api + path)}`, JSON.stringify(body));
|
|
13
|
+
const out = r.stdout.trimEnd();
|
|
14
|
+
const code = Number(out.slice(out.lastIndexOf("\n") + 1));
|
|
15
|
+
const text = out.slice(0, out.lastIndexOf("\n"));
|
|
16
|
+
// nothing answered: the server is restarting or gone, which a wait rides out
|
|
17
|
+
if (!code)
|
|
18
|
+
throw new BsError("review_server_down", `the review server in ${b.id} did not answer ${path}`, `bottleship agent exec ${b.id} -- tail ~/.bottleship/review-server.log`);
|
|
19
|
+
if (r.code !== 0 || code >= 400) {
|
|
20
|
+
let errors = text;
|
|
21
|
+
try {
|
|
22
|
+
errors = JSON.parse(text).errors.join(", ");
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
throw new BsError("review_server", `the review server in ${b.id} answered ${code || "nothing"} to ${path}: ${errors || r.stderr.trim()}`, `bottleship agent exec ${b.id} -- tail ~/.bottleship/review-server.log`);
|
|
26
|
+
}
|
|
27
|
+
return (text ? JSON.parse(text) : {});
|
|
28
|
+
}
|
|
29
|
+
// what the reviewer sent, as soon as they send it; nothing after seconds
|
|
30
|
+
// how long a wait keeps asking a review server that does not answer, e.g. while it restarts
|
|
31
|
+
const DOWN_FOR = 60_000;
|
|
32
|
+
const RETRY = 2_000;
|
|
33
|
+
export async function waitForComments(b, seconds, retry = RETRY) {
|
|
34
|
+
const until = seconds === undefined ? Infinity : Date.now() + seconds * 1000;
|
|
35
|
+
let down;
|
|
36
|
+
for (;;) {
|
|
37
|
+
const ms = Math.max(0, Math.min(25_000, until - Date.now()));
|
|
38
|
+
let w;
|
|
39
|
+
try {
|
|
40
|
+
w = await call(b, "/wait", { ms }, 40);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (!(e instanceof BsError && e.code === "review_server_down"))
|
|
44
|
+
throw e;
|
|
45
|
+
down ??= Date.now();
|
|
46
|
+
if (Date.now() - down > DOWN_FOR)
|
|
47
|
+
throw e;
|
|
48
|
+
await new Promise((r) => setTimeout(r, retry));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
down = undefined;
|
|
52
|
+
if (w.comments.length || w.done || Date.now() >= until)
|
|
53
|
+
return w;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function readJson(b, path) {
|
|
57
|
+
const r = await runIn(b, `cat "$HOME"/${shq(path)}`);
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(r.stdout);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new BsError("not_a_review", `${b.id} has no review data at ~/${path}`, "pass the id bottleship agent review returned");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// the checkout as it is now becomes the box's head: a new commit on top of the last, with the box agent's review
|
|
66
|
+
// hooks put back; then the tour notes the update, the comments get the agent's answers, and the box agent refreshes the tour
|
|
67
|
+
export async function updateReview(b, root, o) {
|
|
68
|
+
const pr = await readJson(b, "review/app/src/data/pr.json");
|
|
69
|
+
if (pr.number)
|
|
70
|
+
throw new BsError("bad_request", `${b.id} is the review of PR #${pr.number}`, "update is for reviews of local changes; in a PR review the box agent pushes to the PR itself");
|
|
71
|
+
const box = await readJson(b, "review/app/src/data/box.json");
|
|
72
|
+
const dotenv = dotenvFiles(root);
|
|
73
|
+
const c = await localChanges(root, { base: box.base, exclude: Object.keys(dotenv) });
|
|
74
|
+
await runIn(b, 'rm -rf "$HOME/.bottleship/update-tree"');
|
|
75
|
+
await putTar(b, ".bottleship/update-tree", c.head);
|
|
76
|
+
const id = "-c user.name=bottleship -c user.email=review@bottleship.local";
|
|
77
|
+
const r = await runIn(b, `set -e
|
|
78
|
+
U="$HOME/.bottleship/update-tree"; I="$HOME/.bottleship/update.index"
|
|
79
|
+
cd "$HOME/workspace"
|
|
80
|
+
hooks=none
|
|
81
|
+
if [ -n "$(git status --porcelain)" ]; then git stash push -u -q -m "review hooks"; hooks=kept; fi
|
|
82
|
+
rm -f "$I"
|
|
83
|
+
GIT_INDEX_FILE="$I" GIT_WORK_TREE="$U" git add -A
|
|
84
|
+
c=$(git ${id} commit-tree "$(GIT_INDEX_FILE="$I" git write-tree)" -p HEAD -m ${shq(`update: ${o.note}`)})
|
|
85
|
+
git merge -q --ff-only "$c"
|
|
86
|
+
if [ "$hooks" = kept ] && ! git stash pop -q >/dev/null 2>&1; then hooks=conflict; fi
|
|
87
|
+
rm -rf "$U" "$I"
|
|
88
|
+
echo "$hooks"`);
|
|
89
|
+
if (r.code !== 0)
|
|
90
|
+
throw new BsError("update_failed", `could not bring the changes into ${b.id}`, "see stderr", { stderr: r.stderr.slice(-2000) });
|
|
91
|
+
const hooks = r.stdout.trim().split("\n").pop();
|
|
92
|
+
await putFiles(b, {
|
|
93
|
+
"review/app/src/data/pr.diff": c.diff,
|
|
94
|
+
"review/app/src/data/pr.json": JSON.stringify({ ...pr, files: c.raw.files }, null, 2),
|
|
95
|
+
});
|
|
96
|
+
const u = await call(b, "/updates", { note: o.note, refreshed: false });
|
|
97
|
+
for (const [cid, body] of Object.entries(o.answers))
|
|
98
|
+
await call(b, `/comments/${cid}/answer`, { body });
|
|
99
|
+
// the box agent may still be refreshing the last update
|
|
100
|
+
await waitAgent(b, 900);
|
|
101
|
+
await ask(b, refreshPrompt(u.n, o.note, hooks));
|
|
102
|
+
return { update: u.n, hooks };
|
|
103
|
+
}
|
|
104
|
+
export function refreshPrompt(n, note, hooks) {
|
|
105
|
+
const kept = {
|
|
106
|
+
kept: "Your review hooks were stashed and put back.",
|
|
107
|
+
conflict: "Your review hooks were stashed but did not go back cleanly: resolve them (git status, git stash show -p), then git stash drop.",
|
|
108
|
+
none: "There were no review hooks to keep.",
|
|
109
|
+
}[hooks];
|
|
110
|
+
return `The developer's agent updated the changes after the reviewer's comments: ${note}
|
|
111
|
+
bottleship committed the new state of their checkout on top of ~/workspace (git show --stat HEAD is what changed) and rewrote src/data/pr.diff and the files in pr.json. ~/base is unchanged. ${kept}
|
|
112
|
+
Refresh the tour for this update; do not rebuild it:
|
|
113
|
+
- restart head if the update needs it
|
|
114
|
+
- fix the groups' line ranges and whatever in src/review.tsx the update makes wrong, and nothing else
|
|
115
|
+
- rerun the steps the update affects and check they still show what their chapters say, then reset events
|
|
116
|
+
- npm run check until it passes
|
|
117
|
+
- then mark the update refreshed: curl -s -X POST -H 'content-type: application/json' -d '{}' ${api}/updates/${n}/refreshed
|
|
118
|
+
End with one line on what you changed in the tour.`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PrRepo } from "./target.ts";
|
|
2
|
+
export interface PullRequest {
|
|
3
|
+
number: number;
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
body: string;
|
|
7
|
+
headRefOid: string;
|
|
8
|
+
baseRefName: string;
|
|
9
|
+
raw: Record<string, unknown>;
|
|
10
|
+
diff: string;
|
|
11
|
+
reviewComments: unknown[];
|
|
12
|
+
}
|
|
13
|
+
export declare function fetchPr(repo: PrRepo, number: number): Promise<PullRequest>;
|
|
14
|
+
export declare function prEnvFiles(repo: PrRepo, ref: string): Promise<Record<string, string> | undefined>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// A pull request, read with the gh CLI on this machine when a review starts.
|
|
2
|
+
import { BsError } from "../config.js";
|
|
3
|
+
import { run } from "../util.js";
|
|
4
|
+
import { reviewThreads } from "./github.js";
|
|
5
|
+
const FIELDS = "number,title,url,body,author,state,headRefName,headRefOid,baseRefName,baseRefOid,comments,reviews,files,labels";
|
|
6
|
+
async function gh(args, stdin) {
|
|
7
|
+
try {
|
|
8
|
+
return await run("gh", args, { check: false, stdin });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new BsError("gh_missing", "the gh CLI is not installed on this machine", "install it from https://cli.github.com, then run `gh auth login`");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// gh api calls for an Enterprise host name it
|
|
15
|
+
const api = (repo, path, ...rest) => gh(["api", ...(repo.hostname ? ["--hostname", repo.hostname] : []), `${repo.api}/${path}`, ...rest]);
|
|
16
|
+
export async function fetchPr(repo, number) {
|
|
17
|
+
const view = await gh(["pr", "view", String(number), "--repo", repo.label, "--json", FIELDS]);
|
|
18
|
+
if (view.code !== 0)
|
|
19
|
+
throw new BsError("pr_not_found", `gh could not read PR #${number} in ${repo.label}: ${view.stderr.trim().slice(0, 300)}`, `check the PR number and repo, and that \`gh auth status${repo.hostname ? ` --hostname ${repo.hostname}` : ""}\` is logged in to an account that can read it`);
|
|
20
|
+
const raw = JSON.parse(view.stdout);
|
|
21
|
+
const diff = await gh(["pr", "diff", String(number), "--repo", repo.label]);
|
|
22
|
+
const comments = await api(repo, `pulls/${number}/comments`, "--paginate");
|
|
23
|
+
let reviewComments = [];
|
|
24
|
+
try {
|
|
25
|
+
if (comments.code === 0)
|
|
26
|
+
reviewComments = JSON.parse(comments.stdout);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
// REST has no resolved state; GraphQL has it per thread, keyed here by the thread's first comment
|
|
30
|
+
// a review still reads without it
|
|
31
|
+
const resolved = new Set((await reviewThreads({ owner: repo.label.split("/")[0], name: repo.label.split("/")[1], hostname: repo.hostname }, number).catch(() => [])).filter((t) => t.resolved).map((t) => t.root));
|
|
32
|
+
for (const c of reviewComments)
|
|
33
|
+
if (resolved.has(c.id))
|
|
34
|
+
c.resolved = true;
|
|
35
|
+
return {
|
|
36
|
+
number: raw.number,
|
|
37
|
+
title: raw.title,
|
|
38
|
+
url: raw.url,
|
|
39
|
+
body: raw.body ?? "",
|
|
40
|
+
headRefOid: raw.headRefOid,
|
|
41
|
+
baseRefName: raw.baseRefName,
|
|
42
|
+
raw,
|
|
43
|
+
diff: diff.code === 0 ? diff.stdout : "",
|
|
44
|
+
reviewComments,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// the PR's own box environment, if it has one: every file in .bottleship/ at the PR's head commit
|
|
48
|
+
export async function prEnvFiles(repo, ref) {
|
|
49
|
+
const list = await api(repo, `contents/.bottleship?ref=${ref}`);
|
|
50
|
+
if (list.code !== 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = JSON.parse(list.stdout);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(entries))
|
|
60
|
+
return undefined;
|
|
61
|
+
const files = {};
|
|
62
|
+
for (const e of entries.filter((x) => x.type === "file")) {
|
|
63
|
+
const r = await api(repo, `contents/${e.path}?ref=${ref}`, "-H", "Accept: application/vnd.github.raw");
|
|
64
|
+
if (r.code === 0)
|
|
65
|
+
files[e.name] = r.stdout;
|
|
66
|
+
}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ReviewPromptInput {
|
|
2
|
+
repo: string;
|
|
3
|
+
number?: number;
|
|
4
|
+
changes?: {
|
|
5
|
+
branch: string;
|
|
6
|
+
note?: string;
|
|
7
|
+
};
|
|
8
|
+
title: string;
|
|
9
|
+
base: string;
|
|
10
|
+
reviewUrl: string;
|
|
11
|
+
instancePorts: number[];
|
|
12
|
+
appPorts: number[];
|
|
13
|
+
envSource: "pr" | "local" | "none";
|
|
14
|
+
}
|
|
15
|
+
export declare function reviewPrompt(p: ReviewPromptInput): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// What the agent in a review box is asked to do: build a guided tour of one PR as a small React app, not review it.
|
|
2
|
+
export function reviewPrompt(p) {
|
|
3
|
+
const local = p.changes;
|
|
4
|
+
const env = {
|
|
5
|
+
pr: "The PR's own .bottleship/ set up this box.",
|
|
6
|
+
local: local ? "The checkout's .bottleship/ set up this box; it is in ~/workspace/.bottleship and ~/base/.bottleship." : "The PR has no .bottleship/ yet, so this box uses the reviewer's local copy; it is in ~/workspace/.bottleship and ~/base/.bottleship, uncommitted.",
|
|
7
|
+
none: "Neither the PR nor the reviewer has a .bottleship/ for this repo, so nothing was set up for you: work out how to install and run the app yourself.",
|
|
8
|
+
}[p.envSource];
|
|
9
|
+
const subject = local ? `the local changes on ${local.branch} in ${p.repo}` : `PR #${p.number} in ${p.repo}, "${p.title}",`;
|
|
10
|
+
const where = local
|
|
11
|
+
? `- ~/workspace: the changes as they are in the developer's checkout (commits, uncommitted and untracked files), as one commit on top of ~/base. ~/base: where ${local.branch} left ${p.base}. Each is its own git repo, so git diff in either shows only what you change there. Throughout, "the PR" means these changes: they have no PR, no comments and no reviews yet.${local.note ? `\n- The author's note: ${local.note}\n It is the author's account of what they changed and why; check its claims like any other.` : ""}`
|
|
12
|
+
: `- ~/workspace: the PR's head, checked out. ~/base: the PR's base (${p.base}) where the PR branched off, a git worktree of the same clone. ${p.base} may have moved on since; compare head with ~/base, not with the tip of ${p.base}.`;
|
|
13
|
+
return `You are setting up the review of ${subject} for a human reviewer. You do not review it and you do not judge it. You build a guided tour of it: a small React app the reviewer opens in their browser, which first tells them what the PR is about, then walks them through what it does in the running app, then shows the code in the same order.
|
|
14
|
+
|
|
15
|
+
Where things are:
|
|
16
|
+
${where}
|
|
17
|
+
- ~/review/app: the review app, Vite with React and TypeScript, dependencies installed.
|
|
18
|
+
- src/data: the PR from GitHub. pr.json has the description, comments, reviews and files; review-comments.json has the inline review comments, where review bots say most of what they say; pr.diff is the diff. The kit reads them; you read them to understand the PR.
|
|
19
|
+
- KIT.md: how a review is built and the rules that keep every review consistent. Read it first, all of it.
|
|
20
|
+
- WRITING.md: how to write every piece of text in the review, and your answers in the chat later. Read it before you write the tour.
|
|
21
|
+
- src/review.tsx: yours to write. It holds a starter now. examples/rply-16.tsx is a complete review to learn from.
|
|
22
|
+
- src/kit: the building blocks: Overview, Lead, BeforeAfter, Attention, Card, Chapter, Live, Outcome, Shot, Compare, Clip, Flow, Diff, Rows and Row. Use them first; write your own components next to review.tsx only when the PR needs something they do not do, with the kit's classes and tokens.
|
|
23
|
+
- src/App.tsx and the kit are the frame every review shares: the rail, the routes, the keys, the look. Leave them as they are unless this PR genuinely needs something else, and if you change them, say what and why in your summary.
|
|
24
|
+
- npm run check typechecks, builds, opens every page in Chromium as the reviewer will see it, and fails on script errors, failed requests and images that do not load. It saves a screenshot of every page to ~/review/checks. Look at them.
|
|
25
|
+
- ${env} If ~/.bottleship/stage says ready, the head's setup.sh ran and its start.sh is running (log: ~/.bottleship/app.log; setup: ~/.bottleship/setup.log). Ports the app EXPOSEs: ${p.appPorts.join(", ") || "none"}.
|
|
26
|
+
- A review server runs at ${p.reviewUrl} (env BOTTLESHIP_REVIEW_URL). The reviewer already has its link. It serves ~/review/app/dist as soon as you build it, and a page showing your progress until then: when you start a stage, report it with curl -s -X POST -H 'content-type: application/json' -d '{"stage":"<stage>"}' ${p.reviewUrl}/api/progress, stage one of read (step 1), instances (3), hooks (4), steps (5), shots (screenshots), tour (6), check (7); files under ~/review at files/<path>; events at /api/events; step scripts at /api/steps/<id>/run.
|
|
27
|
+
- A headless Chromium with Playwright: \`const { chromium } = require("playwright")\` works in any CommonJS script. For screenshots, use node ~/review/app/scripts/shot.cjs (its options are at the top of the file): it saves ~/review/shots/<name>.png, which <Shot src="shots/<name>.png"> and <Compare base=... head=...> show. It can crop to an element, outline it, click and type first, and set the viewport, color scheme and locale. For a flow that takes several steps, moves or depends on timing, record a clip with node ~/review/app/scripts/clip.cjs instead: it records the page while it clicks, types and drags at a person's pace, and <Clip> plays base and head side by side (KIT.md, Clips, says when a clip beats a screenshot).
|
|
28
|
+
- gh and git are logged in with the reviewer's GitHub token (GH_TOKEN). Read whatever helps you understand the change: CI results (gh pr checks), linked issues, earlier PRs. While you build the tour, do not comment, review, approve or push.
|
|
29
|
+
${p.number ? "- Later, the reviewer may send you comments from the tour as a follow-up; it says how to handle them. Never comment, review or approve on GitHub unless the reviewer explicitly asks you to; the follow-up says how." : "- The reviewer's comments go to the developer's own agent, which changes their checkout. After it has, bottleship brings the new state into ~/workspace as a new commit and sends you a follow-up to refresh the tour for it; it says how."}
|
|
30
|
+
|
|
31
|
+
What to do:
|
|
32
|
+
1. Understand the change: the description, the diff, the code around it, the tests it adds. Note what the reviewer has to believe for the PR to be right.
|
|
33
|
+
2. Decide how to show it. Prefer the running app and pictures of it over prose. When the change is about a difference in behaviour, run base and head side by side so both can be seen doing the same thing. When the PR changes what people see, the reviewer must see it without hunting: screenshots of the changed part in base and head, cropped and outlined, in the overview and in the chapter about it, next to the live app to try it, in every state the PR touches (hover, open, empty, error, narrow, dark, another language). When the behaviour is invisible (auth, caching, concurrency, background jobs, performance), use a flow: a small diagram of the parts involved that lights up as review hooks fire, and overlays that say what came out.
|
|
34
|
+
3. Run the instances. Every instance must listen on a port the reviewer's browser can reach: the app's ports above (for head) or ${p.instancePorts.join(", ")} (for base and any others). Bind to 0.0.0.0. Start each with BOTTLESHIP_REVIEW_INSTANCE=<instance id> in its environment. List them in review.tsx's instances by their port in the box. Seed whatever state the tour needs (users, data, settings) so each chapter works from a fresh start.
|
|
35
|
+
4. Add temporary review hooks to the box's copies of the code (both instances), at the points that decide the behaviour under review. A hook posts an event and never breaks the app: fire and forget, errors swallowed. In JavaScript:
|
|
36
|
+
fetch(process.env.BOTTLESHIP_REVIEW_URL + "/api/events", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ instance: process.env.BOTTLESHIP_REVIEW_INSTANCE, kind: "hook", at: "file.ts:47", text: "what happened", flow: { id: "flow-id", edge: ["from", "to"], node: "to", status: "ok", label: "short value" } }) }).catch(() => {})
|
|
37
|
+
In other languages do the same with their HTTP client. Every POST to the review server needs content-type: application/json. Keep hooks few, at most one per decision, and never commit them.
|
|
38
|
+
5. Write the steps: ~/review/steps/<id>.sh, one per chapter that does something, each doing what a person would do (clicks through Playwright, HTTP calls, a script against the API, a CLI command) against every instance it applies to, then exiting. The reviewer presses Run; the server runs the script in ~/review and tags every event that arrives meanwhile with the step. A step script may post events itself: kind "request" for what it did, "ok" or "fail" with an overlay for what came out on each instance.
|
|
39
|
+
6. Write src/review.tsx, the tour:
|
|
40
|
+
- The overview: one sentence on what the PR does for the people who use it (Lead); before and after in plain behaviour (BeforeAfter); where it happens, as a flow with the changed parts marked or the key before and after pictures; 2 to 4 things that deserve a close look, each linked to the chapter or code that shows it (Attention). Then go through the description and every comment and review in pr.json and review-comments.json that makes a claim about this code (from people and from bots) and check it in the box. A claim that does not fully hold goes into Attention with who said it, what they said and what you found, linked to the chapter that shows it; the claims that hold go into its held list. Leave out CI chatter and praise.
|
|
41
|
+
- 3 to 6 chapters, the core behaviour first, then edge cases, then that nothing else broke. Each says why it matters, what should happen on each instance before the reviewer runs it, and shows the evidence (KIT.md says which evidence fits which change).
|
|
42
|
+
- No chapter just to run the tests: CI does that. Run the PR's new or changed tests in the box against base as well as head. That they fail on base and pass on head is a claim that holds (or does not); what they actually check, if it is less than it looks, is a thing to look at closely. Make a chapter for tests only when the PR has no CI that runs them, or when their output is the evidence (a benchmark, a snapshot diff).
|
|
43
|
+
- groups: the diff split into the parts the tour talks about, in tour order, each with one sentence on why it exists and its new-file line ranges, linked to its chapter. Tests and glue last. Files no group covers are listed after them on their own.
|
|
44
|
+
7. Check it: npm run check in ~/review/app until it passes, and look at the screenshots in ~/review/checks the way the reviewer will see the pages; fix what reads badly. Run every step with curl -s -X POST -H 'content-type: application/json' -d '{}' ${p.reviewUrl}/api/steps/<id>/run, wait for it, and read the events with curl -sN --max-time 3 ${p.reviewUrl}/api/events. Every step should finish and produce the events and overlays its chapter promises, for every instance. When everything works, reset with curl -s -X POST -H 'content-type: application/json' -d '{}' ${p.reviewUrl}/api/reset so the reviewer starts fresh.
|
|
45
|
+
8. If part of the app cannot run in this box, say so in the overview and show what can be shown (tests, a script against one service, screenshots), rather than inventing results.
|
|
46
|
+
|
|
47
|
+
End with three short lines: what the tour shows, anything that could not be set up and why, and anything you noticed that the reviewer should look at closely. Leave out URLs and ports (the reviewer already has the link, and ${p.reviewUrl} only works inside this box), and do not ask anyone to revert your hooks: they exist only in this box and go away with it.`;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Runs the review server in a box: node serve.js --dir ~/review [--port N] [--host H] [--urls <file>] [--ask <script>] [--state <script>]
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { startReviewServer } from "./server.js";
|
|
4
|
+
const { values } = parseArgs({ options: { dir: { type: "string" }, port: { type: "string" }, host: { type: "string" }, urls: { type: "string" }, ask: { type: "string" }, state: { type: "string" } } });
|
|
5
|
+
if (!values.dir)
|
|
6
|
+
throw new Error("usage: serve --dir <review dir> [--port N] [--host H] [--urls <file>]");
|
|
7
|
+
const server = await startReviewServer({ dir: values.dir, port: Number(values.port ?? 7777), host: values.host ?? "127.0.0.1", urls: values.urls, ask: values.ask, state: values.state });
|
|
8
|
+
console.log(`review server on ${server.url}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ReviewServer {
|
|
2
|
+
url: string;
|
|
3
|
+
close(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface ReviewServerOptions {
|
|
6
|
+
dir: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
host?: string;
|
|
9
|
+
urls?: string;
|
|
10
|
+
ask?: string;
|
|
11
|
+
state?: string;
|
|
12
|
+
watchMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function startReviewServer({ dir: rawDir, port, host, urls, ask, state, watchMs }: ReviewServerOptions): Promise<ReviewServer>;
|