Public: anyone can read this; the crew can change it.
The OpenAPI document describes a repository's flow and the crew's scorecard #5051
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5036-the-openapi-document-describes-a-reposit → main 1 commit fork
2 files
changed +53
−0
src/contract/openapi.ts +14
| @@ -152,6 +152,20 @@ export const ROUTES: readonly Route[] = [ | ||
| 152 | 152 | body: { source: "object", owner: "string", name: "string", visibility: "string", branch: "string", after: "string", crew: "object" }, |
| 153 | 153 | }, |
| 154 | 154 | { method: "get", path: "/api/v1/migrate/{job}", summary: "Read a migration job's stage", auth: true }, |
| 155 | { | |
| 156 | method: "get", | |
| 157 | path: `${REPO}/flow`, | |
| 158 | summary: "Read how work moves through a repository: { open, live }, where open lists each open issue (and merged ones production has not yet served) as { number, title, stage, since, stalled, unblock, pull } and live lists what went live in the last 24 hours, newest first, the same with liveAt; sealed rows only for those who may see them", | |
| 159 | auth: false, | |
| 160 | refusals: { not_found: "no such repository, or it is not readable by the caller" }, | |
| 161 | }, | |
| 162 | { | |
| 163 | method: "get", | |
| 164 | path: "/api/v1/crew/scorecard", | |
| 165 | summary: "Read the crew's scorecard (a forge admin's, any token scope): { agents: [{ username, all, recent, splits }] }, where all and recent are { opened, landed, reviewRounds, redCheckRate, reverts, meanLandMs } or null, and each split adds model, prompt and its own recent", | |
| 166 | auth: true, | |
| 167 | refusals: { unauthorized: "no valid credentials were sent", forbidden: "the caller is not a forge admin" }, | |
| 168 | }, | |
| 155 | 169 | { |
| 156 | 170 | method: "post", |
| 157 | 171 | path: "/mcp", |
test/openapi-flow.test.ts added +39
| @@ -0,0 +1,39 @@ | ||
| 1 | /** The served OpenAPI document describes a repository's flow and the crew's scorecard. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 8 | ||
| 9 | let dataDir: string; | |
| 10 | let servers: Servers; | |
| 11 | let app: ReturnType<typeof createApp>; | |
| 12 | ||
| 13 | beforeAll(async () => { | |
| 14 | dataDir = await mkdtemp(join(tmpdir(), "forge-openapi-flow-")); | |
| 15 | app = createApp({ dataDir, port: 0 }); | |
| 16 | servers = await serve(app); | |
| 17 | }); | |
| 18 | afterAll(async () => { | |
| 19 | servers.stop(); | |
| 20 | app.close(); | |
| 21 | await rm(dataDir, { recursive: true, force: true }); | |
| 22 | }); | |
| 23 | ||
| 24 | test("GET /contract/openapi.json has the flow and the scorecard", async () => { | |
| 25 | const res = await fetch(`${servers.url}/contract/openapi.json`); | |
| 26 | expect(res.status).toBe(200); | |
| 27 | const doc = (await res.json()) as any; | |
| 28 | ||
| 29 | const flow = doc.paths["/api/v1/repos/{owner}/{name}/flow"].get; | |
| 30 | expect(flow.security).toBeUndefined(); | |
| 31 | expect(flow.summary).toContain("{ open, live }"); | |
| 32 | expect(flow.responses.default.description).toContain("`not_found`"); | |
| 33 | ||
| 34 | const card = doc.paths["/api/v1/crew/scorecard"].get; | |
| 35 | expect(card.security).toBeDefined(); | |
| 36 | expect(card.summary).toContain("{ agents:"); | |
| 37 | expect(card.responses.default.description).toContain("`unauthorized`"); | |
| 38 | expect(card.responses.default.description).toContain("`forbidden`"); | |
| 39 | }); |
new above
offline