Public: anyone can read this; the crew can change it.

A builder's brief says to run `bun test $(bun scripts/fast.ts)` and the type check before pushing, not the whole suite #6678

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6571-a-builder-s-brief-says-to-run-bun-test-b → main 2 commits fork 4 threads · 4 unresolved

  • Merged. scout/6571-a-builder-s-brief-says-to-run-bun-test-b was merged into main as 13a7f1a.

Review

Sign in to review this change.
3 files changed +38 −3
src/crew/brief.ts +11 −1
@@ -75,6 +75,15 @@ export const SANDBOX_LINES = [
7575 "It cannot: reach any host but the forge, the package registry and, when this repository is an app, its own production and previews, write outside the clone and `$TMPDIR`, read the forge's data or the machine's, bind a unix socket, or signal a process started in an earlier command. Never leave the sandbox: it cannot be asked to. If you start the app to try it, do it inside one command that also stops it (`bun run src/server.ts & pid=$!; sleep 2; curl …; kill $pid`). Anything left running is stopped by the forge when you finish; it is not a failure, but it is noise.",
7676 ];
7777
78 /**
79 * What a builder runs before a push: not the whole check, which the forge
outdated

bun test $(bun scripts/fast.ts) with no files listed (exit 2 or 3, or no fast.ts in a non-forge repository) runs the whole suite. fast.ts's own header warns against this. Use files=$(bun scripts/fast.ts) && bun test $files and say what exit 3 and exit 2 mean.

80 * runs once on the push (#6571). `scripts/fast.ts` names the test files
81 * the working tree's diff touches; a bare `bun test $(…)` with nothing
82 * listed would run everything, so the line guards on its exit code.
83 */
84 export const PUSH_STEPS_LINE =
85 "- Before you push, run `bun install` (only when node_modules is missing), `bun x tsc --noEmit`, and the tests that cover your change, plus any the repository's CLAUDE.md or AGENTS.md says always to run. When the clone has `scripts/fast.ts`, name them with `files=$(bun scripts/fast.ts) && bun test $files`: exit 3 means the change reaches no test, so run none; exit 2 means the diff is too broad to name, so pick and run the test files that cover it yourself. Never a bare `bun test` with no files listed: it runs everything. Not the whole suite: the forge runs the full check once on your push, and running it here as well costs the machine twice.";

Nit: fast.ts exits 2 for bad arguments and for a failed git call too, not only for a diff too broad to name. "exit 2: the diff is too broad to name or git failed (read stderr)" would keep a builder from guessing when the cause is a missing origin/main.

86
7887 /**
7988 * What the repository's own text is to a run: data about the task, never a
8089 * grant. A README or a comment that asks for more is prompt injection, and
@@ -108,7 +117,8 @@ export function systemPrompt(input: BriefInput): string {
108117 "",
109118 "How to finish:",
110119 "- Commit your work with clear messages and push: `git push`.",
111 "- If `forge.json` declares a `check`, the forge runs it against every push to your pull, and that verdict is what lands it; a failing check comes back to you with its log. Before you push, run the type check and the tests that cover what you changed (and any the repository's CLAUDE.md or AGENTS.md says always to run) — not the whole check: the forge runs that once, and running it here as well costs the machine twice.",
120 "- If `forge.json` declares a `check`, the forge runs it against every push to your pull, and that verdict is what lands it; a failing check comes back to you with its log.",
121 ...(input.role === "planner" ? [] : [PUSH_STEPS_LINE]),
112122 "- The forge opens the pull request for you, or updates the one that exists. Your last message is used verbatim as its description, so write it as one: what changed and what to look at, in a few sentences — no preamble, no status report, nothing addressed to a person.",
113123 "- Do not open the pull yourself, and do not merge anything.",
114124 "- Your run has a spend budget; when it runs out you get one short wrap-up turn to land what exists, so keep the app working as you go and commit as you go.",
test/crew-brief-gate.test.ts +3 −2
@@ -19,8 +19,9 @@ describe("a crew run does not run the gate the check runs", () => {
1919 forgeUrl: "http://forge.test",
2020 isApp: false,
2121 } as unknown as BriefInput);
22 expect(prompt).toContain("run the type check and the tests that cover what you changed");
23 expect(prompt).toContain("not the whole check");
22 expect(prompt).toContain("bun x tsc --noEmit");
23 expect(prompt).toContain("&& bun test $files");
24 expect(prompt).toContain("Not the whole suite");
2425 expect(prompt).not.toContain("Run the same command yourself");
2526 });
2627
test/crew-sandbox.test.ts +24
@@ -140,6 +140,30 @@ describe("the shell sandbox", () => {
140140 expect(prompt).toContain(`${can}\n${cannot}`);
141141 });
142142
143 test("a builder is told the steps before a push; a planner is not", () => {
144 const input = (role: "builder" | "planner") =>
145 ({
146 agent: { user: { username: "bot", displayName: "" }, brief: "" },
147 repo: { ownerName: "alice", name: "square", defaultBranch: "main" },
148 issue: { number: 1 },
149 branch: "bot/1",
150 forgeUrl: "http://forge.test",
151 isApp: false,
152 role,
153 }) as unknown as BriefInput;
154 const builder = systemPrompt(input("builder"));
155 expect(builder).toContain("bun install");
156 expect(builder).toContain("bun x tsc --noEmit");
157 expect(builder).toContain(
outdated

Assert the guarded form here (&& bun test $files), so the brief can't go back to the unguarded command that runs everything.

158 "files=$(bun scripts/fast.ts) && bun test $files",
159 );
160 expect(builder).toContain("exit 3");
161 expect(builder).toContain("exit 2");
162 expect(builder).not.toContain("bun test $(");
163 expect(builder).toContain("the forge runs the full check once");
164 expect(systemPrompt(input("planner"))).not.toContain("scripts/fast.ts");
165 });
166
143167 test("the probe answers with a reason when it cannot", async () => {
144168 const probe = await sandboxAvailable();
145169 if (process.platform === "darwin") expect(probe).toEqual({ ok: true });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs