Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
267 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1/**
2 * The context hub: one place to ask about a workspace. The context service
3 * keeps a **catalog** of what the workspace builds and runs (projects,
4 * their apps, APIs, packages, languages, owners, environments, integrations
5 * and docs, and how they relate), built by itself from the repositories and
6 * from projects, deployments and integrations; **search** across the
7 * catalog, docs, issues, pull requests and memory; and **scorecards** for
8 * each project. Memory itself, candidates and their review, is kept by
9 * the work service: see `memoryReviewClient`.
10 */
11
12import type { Memory, MemoryKind } from "./agents";
13import type { ServiceBinding } from "./clients";
14import type { User, Viewer } from "./identity";
15import type { RepoPath } from "./repos";
16import type { Result } from "./result";
17
18export type EntityKind =
19 | "project"
20 | "app"
21 | "api"
22 | "package"
23 | "language"
24 | "owner"
25 | "environment"
26 | "integration"
27 | "doc";
28
29export const ENTITY_KINDS: EntityKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc"];
30
31export type RelationKind = "depends_on" | "owned_by" | "deploys_to" | "documented_by" | "exposes" | "uses";
32
33export type Entity = {
34 id: string;
35 workspace: string;
36 kind: EntityKind;
37 /** Unique for its kind in the workspace: a project's slug, `npm:<name>`, a username. */
38 key: string;
39 name: string;
40 summary: string | null;
41 /** The project it belongs to, by slug; null for what the workspace shares (owners, languages, integrations). */
42 project: string | null;
43 /** Whether only members see it: it comes from a private repository. */
44 private: boolean;
45 /** What it is, by kind: a package's version and dependencies, an API's routes, an app's address. */
46 data: Record<string, unknown>;
47 /** Where it was found: `scan` (the repository), `projects`, `deployments`, `integrations`. */
48 source: string;
49 /** Where to see it: a path on g1t.sh, or an address. */
50 ref: string | null;
51 /** RFC 3339. */
52 updatedAt: string;
53};
54
55export type Relation = { from: string; kind: RelationKind; to: string };
56
57export type Catalog = {
58 entities: Entity[];
59 relations: Relation[];
60 /** When the catalog was last built from the repositories. */
61 builtAt: string | null;
62};
63
64export type EntityDetail = {
65 entity: Entity;
66 /** Each relation with the entity at its other end. */
67 relations: { kind: RelationKind; direction: "out" | "in"; entity: Entity }[];
68};
69
70/** What search can return: a catalog entity's kind, or one of these. */
71export type SearchKind = EntityKind | "memory" | "issue" | "pull";
72export const SEARCH_KINDS: SearchKind[] = [...ENTITY_KINDS, "memory", "issue", "pull"];
73
74export type SearchHit = {
75 kind: SearchKind;
76 id: string;
77 title: string;
78 snippet: string;
79 /** The project it is about, by slug. */
80 project: string | null;
81 /** Where to see it on g1t.sh, or an address. */
82 url: string | null;
83 /** Higher is closer; text matches score below semantic ones. */
84 score: number;
85 /** Where it came from, such as `AGENTS.md`, `review on #12`, `catalog`. */
86 source: string;
87 /** Who wrote it, for memory, issues and pull requests. */
88 by: string | null;
89 /** RFC 3339. */
90 updatedAt: string | null;
91};
92
93export type SearchResult = {
94 query: string;
95 hits: SearchHit[];
96 /** `semantic` when the search index answered; `text` when it fell back to matching words. */
97 mode: "semantic" | "text";
98};
99
100export type SearchOptions = { project?: string | null; kinds?: SearchKind[] | null; limit?: number | null };
101
102export type ScoreRule = "has_owner" | "has_readme" | "has_agents_md" | "tests_in_ci" | "production_green" | "no_secret_findings";
103
104export type RuleResult = {
105 rule: ScoreRule;
106 title: string;
107 /** `na` when the rule does not apply, such as production for a project that does not deploy. */
108 status: "pass" | "fail" | "na";
109 detail: string;
110 /** For a failing rule: an issue an agent can fix it from. */
111 fix: { title: string; body: string; checks: string[] } | null;
112};
113
114export type Scorecard = {
115 project: string;
116 name: string;
117 repo: RepoPath;
118 passed: number;
119 /** The rules that apply. */
120 total: number;
121 rules: RuleResult[];
122};
123
124export type Backfill = {
125 workspace: string;
126 status: "running" | "done" | "failed";
127 by: string;
128 projects: number;
129 done: number;
130 entities: number;
131 /** Memory candidates added, and how many of them were kept at once. */
132 candidates: number;
133 kept: number;
134 /** Things put in the search index. */
135 indexed: number;
136 error: string | null;
137 startedAt: string;
138 finishedAt: string | null;
139};
140
141export type ContextStatus = {
142 backfill: Backfill | null;
143 counts: Partial<Record<EntityKind, number>>;
144 /** What the search index cost the workspace this month. */
145 usage: { month: string; tokens: number; costMicros: number };
146 /** Whether semantic search is available: set up, and on a paid plan or the trial. Text search always is. */
147 semantic: boolean;
148};
149
150export type RunContext = {
151 /** The Context section for an agent's prompt; null when there is nothing to say. */
152 text: string | null;
153 /** What it drew on, such as `catalog:web`, `memory:mem_…`. */
154 sources: string[];
155};
156
157export interface ContextApi {
158 /** The workspace's catalog, by kind and project. Members, or anyone for public projects' entries. */
159 catalog(workspace: string, viewer: Viewer, filter?: { kind?: EntityKind | null; project?: string | null }): Promise<Result<Catalog>>;
160 /** One entity, by its id or its key, with its relations. */
161 entity(workspace: string, viewer: Viewer, kind: EntityKind, id: string): Promise<Result<EntityDetail>>;
162 /** One search across the catalog, docs, issues, pull requests and, for members, memory. */
163 search(workspace: string, viewer: Viewer, query: string, options?: SearchOptions): Promise<Result<SearchResult>>;
164 /** Each project's scorecard. Members only. */
165 scorecards(workspace: string, viewer: Viewer): Promise<Result<Scorecard[]>>;
166 /** Builds the catalog for every project and seeds memory from docs and merged pull requests. Members only; once at a time. */
167 backfill(actor: User, workspace: string): Promise<Result<Backfill>>;
168 /** The hub's state for a workspace. Starts its first backfill when it has none. Members only. */
169 status(workspace: string, viewer: Viewer): Promise<Result<ContextStatus>>;
170 /**
171 * For the runner: the Context section for an agent starting work in a
172 * repository, within `budget` characters. `requester` is the person the
173 * run acts for: it holds only what they may read. One who is not a member
174 * of the workspace gets the project's memory only, never the workspace's,
175 * and only the dependencies whose projects they can read.
176 */
177 runContext(repoId: string, task: string, budget?: number, requester?: Viewer): Promise<RunContext>;
178}
179
180async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
181 const response = await service.fetch(`https://service/rpc/${method}`, {
182 method: "POST",
183 headers: { "content-type": "application/json" },
184 body: JSON.stringify(args),
185 });
186 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
187 return (await response.json()) as T;
188}
189
190export function contextClient(service: ServiceBinding): ContextApi {
191 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
192 return {
193 catalog: (workspace, viewer, filter = {}) => call("catalog", { workspace, viewer, ...filter }),
194 entity: (workspace, viewer, kind, id) => call("entity", { workspace, viewer, kind, id }),
195 search: (workspace, viewer, query, options = {}) => call("search", { workspace, viewer, query, ...options }),
196 scorecards: (workspace, viewer) => call("scorecards", { workspace, viewer }),
197 backfill: (actor, workspace) => call("backfill", { actor, workspace }),
198 status: (workspace, viewer) => call("status", { workspace, viewer }),
199 runContext: (repoId, task, budget, requester) => call("run_context", { repoId, task, budget, requester: requester ?? null }),
200 };
201}
202
203// --- Memory capture and review (the work service) ---------------------------
204
205/** Where a captured memory came from. */
206export type CaptureSource = "run" | "review" | "pr" | "doc" | "manual";
207
208export type CaptureItem = {
209 scope: "project" | "workspace";
210 /** For a project's memory: its repository's id. */
211 repoId?: string | null;
212 kind?: MemoryKind;
213 text: string;
214 confidence?: number | null;
215 source: CaptureSource;
216 /** One source: `doc:<repo id>:<path>`, `run:<id>`, `comment:<id>`, `pull:<repo id>#<n>`. */
217 reference: string;
218 evidence?: string | null;
219 number?: number | null;
220 runId?: string | null;
221};
222
223export type Captured = { added: number; merged: number; kept: number; refused: number };
224
225export type ReviewDecision = "keep" | "dismiss";
226
227export interface MemoryReviewApi {
228 /** Candidates waiting for review in a workspace, or for one project. Members only. */
229 listCandidates(viewer: Viewer, workspace: string, repo?: RepoPath | null): Promise<Result<Memory[]>>;
230 /** Keep a candidate, edited or as it is, or dismiss it. Members only. */
231 reviewMemory(
232 actor: User,
233 workspace: string,
234 id: string,
235 decision: ReviewDecision,
236 change?: { text?: string; kind?: MemoryKind },
237 ): Promise<Result<Memory>>;
238 /** For services: candidates from docs and backfills. */
239 captureMemories(workspace: string, items: CaptureItem[], by?: string): Promise<Captured>;
Merge memory from docs: only docs on how to work here, whole sentences, no near-duplicates, stale doc suggestions cleared240 /**
241 * For services: removes a project's candidates that came only from its
242 * docs and are still waiting, unless one of `texts` (what its docs suggest
243 * now) says the same thing. Kept and dismissed memory is never touched.
244 */
245 pruneDocCandidates(workspace: string, repoId: string, texts: string[]): Promise<{ removed: number }>;
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor246 /** For services: memories by id, in any status. */
247 memoriesById(workspace: string, ids: string[]): Promise<Memory[]>;
248 /** For services: kept memories with every word of `query`; the caller has checked the viewer may read them. */
249 searchMemories(workspace: string, query: string | null, options?: { repoIds?: string[] | null; limit?: number }): Promise<Memory[]>;
250 /** For services: decision and convention candidates from a repository's last merged pull requests. */
251 seedFromPulls(repoId: string, limit?: number): Promise<Captured>;
252}
253
254/** Memory capture and review: methods of the work service. */
255export function memoryReviewClient(service: ServiceBinding): MemoryReviewApi {
256 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
257 return {
258 listCandidates: (viewer, workspace, repo) => call("list_candidates", { viewer, workspace, repo: repo ?? null }),
259 reviewMemory: (actor, workspace, id, decision, change = {}) => call("review_memory", { actor, workspace, id, decision, ...change }),
260 captureMemories: (workspace, items, by) => call("capture_memories", { workspace, items, by: by ?? null }),
Merge memory from docs: only docs on how to work here, whole sentences, no near-duplicates, stale doc suggestions cleared261 pruneDocCandidates: (workspace, repoId, texts) => call("prune_doc_candidates", { workspace, repoId, texts }),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor262 memoriesById: (workspace, ids) => call("memories_by_id", { workspace, ids }),
263 searchMemories: (workspace, query, options = {}) =>
264 call("search_memories", { workspace, query, repoIds: options.repoIds ?? null, limit: options.limit ?? 20 }),
265 seedFromPulls: (repoId, limit) => call("seed_from_pulls", { repoId, limit: limit ?? null }),
266 };
267}

This file's history is long; its oldest lines are credited to the oldest commit read.