Public: anyone can read this; the crew can change it.
Crew briefs point at docs/PRINCIPLES.md instead of restating it #8784
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/8737-crew-briefs-point-at-docs-principles-md → main 2 commits fork 3 threads · 3 unresolved
3 files
changed +36
−26
CLAUDE.md +17 −25
| @@ -8,14 +8,10 @@ rulebook: the invariants you must not break, and how to work here. | ||
| 8 | 8 | |
| 9 | 9 | **Read docs/VISION.md first: it says what the forge is for and the order of work.** |
| 10 | 10 | |
| 11 | **Done means reachable** — a person reaches it from the feed in two taps, an | |
| 12 | agent through the MCP, a test proves it, a look shows it. A function nobody can | |
| 13 | call is not done. | |
| 14 | ||
| 15 | The whole point of forge is that **one person can hold it in their head**. Every | |
| 16 | change should leave it that way. When something fights you, step up and | |
| 17 | understand the system before adding code — a workaround usually means the system | |
| 18 | isn't understood yet. | |
| 11 | **Done means reachable**, and the rest of what the forge holds to is in | |
| 12 | docs/PRINCIPLES.md with what enforces each; read it. This file does not repeat it. When | |
| 13 | something fights you, step up and understand the system before adding code — a | |
| 14 | workaround usually means the system isn't understood yet. | |
| 19 | 15 | |
| 20 | 16 | ## Invariants — do not break these |
| 21 | 17 | |
| @@ -30,9 +26,8 @@ isn't understood yet. | ||
| 30 | 26 | 3. **Real git, thin wrapper.** Never reimplement pack negotiation, diff, blame, |
| 31 | 27 | or merge. Spawn the `git` binary and plumb bytes carefully. Correctness comes |
| 32 | 28 | from git; our job is safe arguments and clean I/O. |
| 33 | 4. **Git is truth; the DB is a derived index.** SQLite holds nothing that can't | |
| 34 | be rebuilt from the bare repos. `core/reconcile.ts` self-heals the index from | |
| 35 | git on boot. Don't invent state that lives only in the DB (or only in memory). | |
| 29 | 4. **Derived index** (docs/PRINCIPLES.md). Don't invent state that lives only | |
| 30 | in the DB (or only in memory). | |
| 36 | 31 | 5. **State changes go through the event outbox.** Every meaningful mutation |
| 37 | 32 | `bus.append(...)`s a durable event (`core/events.ts`). Webhooks and SSE ride |
| 38 | 33 | that outbox; delivery is retried. Never fire-and-forget something important. |
| @@ -49,9 +44,9 @@ canAdmin`), token scope (`readForm` floor + `minScope`), and branch | ||
| 49 | 44 | 9. **`not_found` is deliberately ambiguous.** A missing resource and a resource |
| 50 | 45 | you can't see both return `not_found`, so private repos never leak their |
| 51 | 46 | existence. Keep that conflation when you add routes. |
| 52 | 10. **The crew proposes; a person lands.** An agent (`user.kind === "agent"`) | |
| 53 | never moves the default branch — the pre-receive hook refuses it, | |
| 54 | protected or not — and `mergePull` refuses an agent actor. Its only grant | |
| 47 | 10. **The crew proposes; a person lands** (docs/PRINCIPLES.md). An agent | |
| 48 | (`user.kind === "agent"`) is refused by the pre-receive hook, protected | |
| 49 | or not, and by `mergePull`. Its only grant | |
| 55 | 50 | is `repo_crew` (read + write, never admin); its only credential is a |
| 56 | 51 | token minted for a run with an expiry, deleted when the run ends, and |
| 57 | 52 | bound to the run's repository and the fork it pushes to — every other |
| @@ -106,20 +101,17 @@ bun run src/main.ts # boots on :3000, data in ./data | ||
| 106 | 101 | `EXCLUDED`, so never hand-edit that move. Never add `retry` — fix the |
| 107 | 102 | cause: a shared port, a real clock, order dependence, a leaked server, a |
| 108 | 103 | timeout near the real duration, a wall-clock bound under 200 ms on a |
| 109 | request or a spawn (`test/flake.test.ts` refuses one). A speed is never bounded by a clock: count work | |
| 110 | instead (SQL statements with `test/helpers/statements.ts`, spawns, bytes, | |
| 111 | phases), assert it with `expectWork` (`test/helpers/work.ts`), or use a fake | |
| 112 | clock (#7789, #7824); `test/flake.test.ts` refuses a raw `under N ms` title | |
| 113 | or bound. | |
| 104 | request or a spawn (`test/flake.test.ts` refuses one). Speed is counted in work (docs/PRINCIPLES.md): SQL | |
| 105 | statements with `test/helpers/statements.ts`, spawns, bytes, phases, | |
| 106 | asserted with `expectWork` (`test/helpers/work.ts`), or a fake clock | |
| 107 | (#7789, #7824). | |
| 114 | 108 | - **Every non-trivial change ships with a test.** Prefer protocol-level tests |
| 115 | 109 | that boot a real app (`createApp` + `serve`) over unit mocks. Adversarial |
| 116 | 110 | tests (can this be bypassed?) are worth more than happy-path ones. |
| 117 | - **Done means reachable.** A new action is called from a route, a page or an | |
| 118 | MCP tool in the same pull, and a new screen is linked from a page a person | |
| 119 | can get to. `scripts/hollow.ts` (run by `test/hollow.test.ts` in the gate, | |
| 120 | or `bun scripts/hollow.ts`) names an exported function in `src/actions/` | |
| 121 | that nothing that runs reaches (a page function no route names does not | |
| 122 | count as a caller) and a screen no page links to. | |
| 111 | - **Reachable** (docs/PRINCIPLES.md). A new action is called from a route, a | |
| 112 | page or an MCP tool in the same pull, and a new screen is linked from a page | |
| 113 | a person can get to. `bun scripts/hollow.ts` names what is not (a page | |
| 114 | function no route names does not count as a caller). | |
| 123 | 115 | `scripts/hollow-allow.ts` is for hollow work an issue already owns, never |
| 124 | 116 | for new work. |
| 125 | 117 | - **TypeScript is strict**: `noUncheckedIndexedAccess` (index access is `T | |
src/crew/brief.ts +1 −1
| @@ -121,7 +121,7 @@ export function systemPrompt(input: BriefInput): string { | ||
| 121 | 121 | agent.brief.trim() === "" ? "" : `\n${agent.brief.trim()}\n`, |
| 122 | 122 | `You are working in a clone of the repository on the branch \`${input.branch}\`. The remote \`origin\` is the forge; \`git push\` is already authenticated as you and the branch already tracks \`origin/${input.branch}\`. Git configuration is read-only here — do not try to change it, a plain \`git push\` is all you need.`, |
| 123 | 123 | "", |
| 124 | "Read docs/VISION.md first: it says what the forge is for and the order of work. Done means reachable — a person reaches it from the feed in two taps, an agent through the MCP, a test proves it, a look shows it. A function nobody can call is not done. Where the repository has docs/PRINCIPLES.md, read it too: what it holds to, and what enforces each.", | |
| 124 | "Read docs/VISION.md first: it says what the forge is for and the order of work. Done means reachable: a function nobody can call is not done. Where the repository has docs/PRINCIPLES.md, read it too: what it holds to, and what enforces each.", | |
| 125 | 125 | "", |
| 126 | 126 | "How to finish:", |
| 127 | 127 | "- Commit your work with clear messages and push: `git push`.", |
test/principles.test.ts +18
| @@ -64,4 +64,22 @@ describe("who reads it", () => { | ||
| 64 | 64 | expect(prompt).toContain("docs/PRINCIPLES.md"); |
| 65 | 65 | expect(prompt).toContain("Done means reachable"); |
| 66 | 66 | }); |
| 67 | ||
| 68 | const builder = systemPrompt({ | |
| 69 | agent: { user: { username: "bot", displayName: "" }, brief: "" }, | |
| 70 | repo: { ownerName: "a", name: "forge", defaultBranch: "main" }, | |
| 71 | issue: { number: 1 }, | |
| 72 | branch: "b", | |
| 73 | forgeUrl: "http://f", | |
| 74 | isApp: true, | |
| 75 | } as unknown as BriefInput); | |
| 76 | ||
| 77 | test("the builder brief is at least 100 bytes shorter than on main before #8737 (7303 bytes, an app on the forge)", () => { | |
| 78 | expect(Buffer.byteLength(builder)).toBeLessThanOrEqual(7303 - 100); | |
| 79 | }); | |
| 80 | ||
| 81 | test("the brief points at the file, and no longer spells out the two taps", () => { | |
| 82 | expect(builder).toContain("docs/PRINCIPLES.md"); | |
| 83 | expect(builder).not.toContain("two taps"); | |
| 84 | }); | |
| 67 | 85 | }); |
new above
offline