Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/src/worker/workflow-source.ts
Snapshot at 34e3175
1import type { ContextResponse, WorkItem, ReadReviewResponse } from "../../../forkhorse-stable/src/index.ts";
2import type { WorkflowSource, WorkflowSubmission } from "./workflow.ts";
3
4/**
5 * Server-side transport seam, relative to one authenticated project. The adapter
6 * obtains bearer credentials through the accepted Stable provider in CoreClient.
7 */
8export interface WorkflowTransport {
9 project: string;
10 identity(): Promise<{ agent: string; work: WorkItem[]; forks: { repoName: string; createdAt: string }[] }>;
11 request(path: string, init?: RequestInit): Promise<unknown>;
12}
13
14export function workflowSource(transport: WorkflowTransport): WorkflowSource {
15 return {
16 async authorize() { return transport.identity(); },
17 async context() {
18 const identity = await transport.identity();
19 const result = await transport.request("/state") as ContextResponse;
20 // Match MCP context: /state plus the authenticated /me work summary.
21 // The dashboard's normalized view loses the core resume instructions.
22 if (!result || !result.project || !Array.isArray(identity.work)) {
23 throw new Error("Invalid project context.");
24 }
25 return { ...result, myWork: identity.work };
26 },
27 async submission(operation) {
28 const identity = await transport.identity();
29 let packets: WorkflowSubmission["packets"] = [];
30 if (operation.kind === "claim") {
31 // Core has GET /packets, but no single-packet GET. Never load /state
32 // just to choose the approval kind; apply/recovery still read full evidence.
33 const result = await transport.request("/packets") as Pick<WorkflowSubmission, "packets">;
34 if (!result || !Array.isArray(result.packets)) throw new Error("Invalid packet response.");
35 const packet = result.packets.find(candidate => candidate.id === operation.packet);
36 if (packet) {
37 if (packet.mode !== undefined && packet.mode !== "standard" && packet.mode !== "race") {
38 throw new Error("Invalid packet response.");
39 }
40 packets = [{ id: packet.id, mode: packet.mode }];
41 }
42 }
43 return {
44 project: { id: transport.project }, packets,
45 forks: operation.kind === "claim" && !operation.repoName
46 ? identity.forks.map(fork => ({ repoName: fork.repoName, createdAt: fork.createdAt, agent: identity.agent }))
47 : [],
48 };
49 },
50 async readReview(review) {
51 if (!/^[a-zA-Z0-9-]{1,128}$/.test(review)) throw new Error("Invalid review ID.");
52 return transport.request(`/reviews/${encodeURIComponent(review)}`) as Promise<ReadReviewResponse>;
53 },
54 async readReceipts(review) {
55 const result = await this.readReview(review);
56 return { receipt: result.review.receipt ?? null };
57 },
58 async execute(operation) {
59 const identity = await transport.identity();
60 let path: string;
61 let method = "POST";
62 let body: Record<string, unknown> | undefined;
63 switch (operation.kind) {
64 case "join":
65 path = "/join";
66 body = { agent: identity.agent, ttlSeconds: 60 };
67 break;
68 case "claim":
69 path = `/packets/${encodeURIComponent(operation.packet)}/claim`;
70 body = operation.repoName === undefined ? {} : { repoName: operation.repoName };
71 break;
72 case "requestReview":
73 path = "/reviews";
74 body = { packet: operation.packet, repoName: operation.repoName, sha: operation.sha };
75 break;
76 case "release":
77 path = `/packets/${encodeURIComponent(operation.packet)}/claim`;
78 method = "DELETE";
79 break;
80 }
81 // In particular, join's token-bearing response never leaves this function,
82 // is never persisted, and is never included in queue metadata or errors.
83 try {
84 await transport.request(path, { method, ...(body ? { body: JSON.stringify(body) } : {}) });
85 } catch {
86 throw new Error("Workflow request failed. Inspect context before retrying.");
87 }
88 },
89 };
90}
91