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
1,319 linesCodeBlameRaw
1import type { ChecksApi } from "./checks";
2import type { CodeownersReport, PullCodeOwners } from "./codeowners";
3import type { G1tEvent } from "./events";
4import type { User, Viewer } from "./identity";
5import type { PullBranchUpdate, RepoPath } from "./repos";
6import type { Result } from "./result";
7import type { MergeRules, RulesApi } from "./rules";
8
9/**
10 * The filter on lists of issues and pull requests. An open pull request is
11 * a draft or one ready for review; a closed one was merged or closed
12 * without merging.
13 */
14export type State = "open" | "closed";
15
16/** Why an issue was closed. */
17export type IssueReason = "completed" | "not_planned";
18
19/**
20 * Something that should change in a repository: a bug, a feature, a
21 * question. Opened by a person, an agent or an integration. Pull requests
22 * are made against it; the one that is merged resolves it.
23 *
24 * Issues and pull requests share one sequence of numbers per repository.
25 */
26export type Issue = {
27 id: string;
28 repoId: string;
29 /** Shown as `#12`. */
30 number: number;
31 title: string;
32 /** Markdown. Also what an agent is given to work from. */
33 body: string;
34 labels: string[];
35 state: State;
36 /** Set when closed. */
37 reason: IssueReason | null;
38 /** The number of the pull request whose merge closed this issue. */
39 resolvedBy: number | null;
40 /** Who opened it: a person, an integration, or g1t (`kind` `agent`) for one its agent filed while at work. */
41 author: User;
42 /**
43 * For an issue g1t's agent filed: the person it was working for. They may
44 * manage it as its author could. See `workOwner`.
45 */
46 requestedBy: User | null;
47 /** RFC 3339. */
48 createdAt: string;
49 /** RFC 3339. */
50 updatedAt: string;
51 /** RFC 3339. */
52 closedAt: string | null;
53 /** Pull requests made against this issue, in any state. */
54 pullCount: number;
55 commentCount: number;
56 /** Usernames of the people it is assigned to. */
57 assignees: string[];
58 /** The numbers of the issues that have to be merged before this one is worked on. */
59 blockedBy: number[];
60 /**
61 * The agent working on it now: the one behind its newest pull request
62 * that is still in progress in a fork, such as `g1t`.
63 */
64 agent: string | null;
65 /** The milestone it is in, if any. */
66 milestone?: MilestoneRef | null;
67};
68
69/**
70 * A label of a repository: a name, a color and what it means. Issues and
71 * pull requests carry labels by name; names are lowercase.
72 */
73export type Label = {
74 name: string;
75 /** Six hex digits, without `#`. */
76 color: string;
77 description: string;
78 /** How many issues carry it, open or closed. */
79 issues: number;
80 /** How many pull requests carry it, in any state. */
81 pulls: number;
82};
83
84/** A milestone, as an issue or pull request names it. */
85export type MilestoneRef = { number: number; title: string };
86
87/**
88 * A goal, with an optional due date, that issues and pull requests are
89 * gathered under. Its progress is how many of them are closed.
90 */
91export type Milestone = {
92 /** Numbered from 1 in each repository, apart from issues. */
93 number: number;
94 title: string;
95 /** Markdown. */
96 description: string;
97 /** `YYYY-MM-DD`. */
98 dueOn: string | null;
99 state: State;
100 /** Open issues and pull requests in it. */
101 openItems: number;
102 /** Closed issues, and merged or closed pull requests, in it. */
103 closedItems: number;
104 createdAt: string;
105 updatedAt: string;
106 closedAt: string | null;
107};
108
109/** One milestone and what is in it, newest first. */
110export type MilestoneDetail = { milestone: Milestone; issues: Issue[]; pulls: Pull[] };
111
112/** How `setLabels` changes an item's labels. */
113export type LabelChange = "set" | "add" | "remove";
114
115/**
116 * Whose an issue or a pull request is to answer for: whoever asked g1t for
117 * it, or its author. They may change, close and steer it, are never asked to
118 * review it and cannot approve it, and see it as theirs. Mirrors
119 * `Pull::owner` in the Rust contracts.
120 */
121export function workOwner(item: Pick<Pull, "author" | "requestedBy">): User {
122 return item.requestedBy ?? item.author;
123}
124
125/** `draft` is still being worked on; `open` is ready for review. */
126export type PullStatus = "draft" | "open" | "merged" | "closed";
127
128/** Where the agent runs: on g1t's sandboxes, or in someone's own session. */
129export type Runtime = "hosted" | "external";
130
131/**
132 * A proposed change. It is made either in a fork created for it, which is
133 * how agents work, or on a branch pushed to the repository itself.
134 */
135export type Pull = {
136 id: string;
137 repoId: string;
138 /** Shown as `#12`. */
139 number: number;
140 /** The number of the issue this is for, if any. */
141 issue: number | null;
142 title: string;
143 /** Markdown: what changed and why. Set when marked ready. */
144 body: string | null;
145 /** A label for the agent doing the work, e.g. `claude-code`. */
146 agent: string;
147 runtime: Runtime;
148 status: PullStatus;
149 /** The fork holding the change, unless it is on a branch. */
150 fork: RepoPath | null;
151 /** The fork's repository id. */
152 forkRepoId: string | null;
153 /** The branch of the repository holding the change, unless it is in a fork. */
154 branch: string | null;
155 headCommit: string | null;
156 /**
157 * For a merged pull request, what the branch pointed to before the merge.
158 * Comparing against it shows what the pull request changed.
159 */
160 mergeBase: string | null;
161 /** Username of whoever merged it. */
162 mergedBy: string | null;
163 /** RFC 3339. */
164 mergedAt: string | null;
165 /**
166 * Set on a pull request closed because another one for the same issue was
167 * merged: that one's number.
168 */
169 supersededBy: number | null;
170 /**
171 * Where the latest run of the issue's acceptance checks stands, if there
172 * has been one against the current head.
173 */
174 checkStatus: CheckStatus | null;
175 /** The files it changes, as of its latest push. */
176 files: ChangedFile[];
177 /** Usernames of the people it is assigned to. */
178 assignees: string[];
179 /**
180 * Those whose review was asked for: usernames, and `g1t` when a g1t
181 * agent was asked.
182 */
183 reviewers: string[];
184 /**
185 * Teams whose review was asked for, as `workspace/slug`. A team stays here
186 * after review assignment picks people from it, who are in `reviewers`.
187 */
188 teamReviewers?: string[];
189 /** The labels it carries, by name. */
190 labels?: string[];
191 /** The milestone it is in, if any. */
192 milestone?: MilestoneRef | null;
193 /**
194 * The branch it merges into. Lists and `getPull` name it; null only in
195 * what services pass between themselves, for the default branch.
196 */
197 base?: string | null;
198 /** Who opened it: a person, or g1t (`kind` `agent`, username `g1t`) for a change g1t made. */
199 author: User;
200 /**
201 * For a change g1t made: the person who asked for it, by assigning an issue
202 * or handing g1t the work. They answer for it as its author would. See
203 * `workOwner`.
204 */
205 requestedBy: User | null;
206 /** RFC 3339. */
207 createdAt: string;
208 /** RFC 3339. */
209 updatedAt: string;
210 /** Who last moved its head (a user id), and when; absent until a push after rulesets arrived. */
211 headPushedBy?: string;
212 headPushedAt?: string;
213};
214
215/** One file a pull request changes, and by how much. */
216export type ChangedFile = { path: string; additions: number; deletions: number };
217
218/**
219 * Another pull request in progress that changes some of the same files. Two
220 * for the same issue are alternatives; two for different issues are heading
221 * for a conflict.
222 */
223export type Overlap = {
224 number: number;
225 title: string;
226 /** The number of the issue the other pull request is for. */
227 issue: number | null;
228 /** The files both change. */
229 paths: string[];
230};
231
232/**
233 * On a pull request, `failed` means the merge queue took it out, until its
234 * head moves. The other states are from runs of commands written on issues,
235 * which g1t no longer runs.
236 */
237export type CheckStatus = "queued" | "running" | "passed" | "failed" | "errored";
238
239/** How one command went, in a run recorded before checks were workflows. */
240export type CheckResult = {
241 command: string;
242 passed: boolean;
243 /** Null when the command was stopped for taking too long. */
244 exitCode: number | null;
245 /** What the command printed; the end of it, when there was a lot. */
246 output: string;
247 durationMs: number;
248};
249
250/**
251 * A record against a pull request's head: the merge queue taking it out,
252 * with why, or an earlier run of commands written on its issue.
253 */
254export type CheckRun = {
255 id: string;
256 /** The commit that was checked. */
257 headCommit: string;
258 status: CheckStatus;
259 results: CheckResult[];
260 /** Why the checks could not be run, when `status` is `errored`. */
261 error: string | null;
262 /** RFC 3339. */
263 createdAt: string;
264 /** RFC 3339. */
265 finishedAt: string | null;
266};
267
268/** A reviewer's decision on a pull request. */
269export type Verdict = "approve" | "request_changes";
270
271/**
272 * A comment on an issue or a pull request. On a pull request it can sit on
273 * one line of the change, and it can carry a reviewer's verdict.
274 */
275export type Comment = {
276 id: string;
277 /**
278 * Something a person or an agent wrote, or something that happened: an
279 * assignment, a review asked for, a close.
280 */
281 kind: "comment" | "event";
282 author: User;
283 /**
284 * Markdown. For an event, what its author did, as the rest of a sentence
285 * that starts with their name: "assigned ana".
286 */
287 body: string;
288 /** The file commented on, for a comment on a line. */
289 path: string | null;
290 /** The line of that file, as numbered after the change. */
291 line: number | null;
292 verdict: Verdict | null;
293 /** RFC 3339. */
294 createdAt: string;
295 /** When its text was last edited, RFC 3339; absent if it never was. */
296 editedAt?: string;
297 /**
298 * Set when one of the workspace's agents wrote it, as itself. `author`
299 * is then the agent (its id, its handle as `username`, kind `agent`):
300 * show it by `agent.displayName` with an Agent badge, never as a person.
301 */
302 agent?: AgentRef;
303 /** Who the agent acted for, whose access capped it. Set with `agent`. */
304 actingFor?: User;
305 /**
306 * An agent's review. Its `verdict` (none for a review that only
307 * comments) is shown but advisory: it never counts toward required
308 * approvals or code owners, and never blocks a merge.
309 */
310 advisory?: boolean;
311};
312
313/**
314 * One of a workspace's own agents, as a comment or review it wrote before
315 * g1t's agents were removed shows it: Margo (@margo), with the face drawn
316 * from `avatarSeed`. Kept so those comments still render.
317 */
318export type AgentRef = {
319 id: string;
320 handle: string;
321 displayName: string;
322 avatarSeed: string;
323};
324
325export type NewComment = {
326 /** May be empty when approving. */
327 body: string;
328 path?: string;
329 line?: number;
330 verdict?: Verdict;
331};
332
333export type CheckReport = { results?: CheckResult[]; error?: string; skip?: boolean };
334
335export type SessionEntryKind = "prompt" | "message" | "tool_call" | "tool_result" | "note" | "skill";
336
337/** One step of an agent's session: the "why" behind a pull request's commits. */
338export type SessionEntry = {
339 seq: number;
340 kind: SessionEntryKind;
341 text: string;
342 /** For tool calls and results. */
343 tool: string | null;
344 /** The fork's head commit when this entry was recorded, if known. */
345 commit: string | null;
346 /** RFC 3339. */
347 at: string;
348};
349
350export type NewSessionEntry = Pick<SessionEntry, "kind" | "text"> &
351 Partial<Pick<SessionEntry, "tool" | "commit">>;
352
353export type IssueDetail = {
354 issue: Issue;
355 /** Every pull request made against it, oldest first. */
356 pulls: Pull[];
357 comments: Comment[];
358};
359
360export type PullDetail = {
361 pull: Pull;
362 /** The issue it is for, if any. */
363 issue: Issue | null;
364 comments: Comment[];
365 /** The latest record against its head: the merge queue taking it out. */
366 checks: CheckRun | null;
367 /** Other pull requests in progress that change the same files. */
368 overlaps: Overlap[];
369 /**
370 * Whether the branch it would merge into has moved on without it, so that
371 * it has to catch up before it can merge.
372 */
373 behind: boolean;
374 /**
375 * Where it stands on its way to being merged, for a pull request g1t is
376 * seeing through. Null on anyone else's.
377 */
378 lifecycle: Lifecycle | null;
379 /**
380 * A merge was asked for while it was behind: g1t is bringing it up to
381 * date and will then land it.
382 */
383 landing: boolean;
384 /** Why g1t stopped working on it, if it did. */
385 stalled: string | null;
386 /** What workflow runs said about its head commit, one per workflow. */
387 statuses?: CommitStatus[];
388 /**
389 * Whether it merges cleanly into the branch it targets, worked out ahead
390 * of time whenever either side moves.
391 */
392 mergeable?: Mergeable;
393 /** When `mergeable` is `conflicting`: the files that conflict. */
394 conflicts?: string[];
395 /** Earlier records like `checks`, newest first, without their output. */
396 earlierChecks?: CheckRun[];
397 /**
398 * The checks the default branch's protection requires, each as it stands
399 * on the head commit. Empty when none are required.
400 */
401 requiredChecks?: RequiredCheck[];
402 /**
403 * Who owns the files it changes (the CODEOWNERS file of the branch it
404 * merges into) and whose approval is still needed. Absent without one.
405 */
406 codeOwners?: PullCodeOwners | null;
407 /**
408 * The rules of the branch it merges into it does not meet yet, for whoever
409 * is looking: what refuses the merge, what they may bypass, and what rulesets
410 * in evaluate would refuse. Absent once it is closed or merged.
411 */
412 rules?: MergeRules | null;
413};
414
415/** Where a required check stands on a commit; `expected` when nothing has reported it yet. */
416export type RequiredState = "success" | "failure" | "pending" | "expected";
417
418/** One check a branch's protection requires, as it stands on a commit. */
419export type RequiredCheck = {
420 /** A workflow's name, such as `CI`, or another status's context. */
421 name: string;
422 state: RequiredState;
423 description: string | null;
424 /** Where to see more: the workflow run, for one a workflow reported. */
425 targetUrl: string | null;
426};
427
428/** A check name reported on a repository's commits lately, for choosing required checks. */
429export type SeenCheck = {
430 name: string;
431 /** The events it was reported for, such as `pull_request`; empty for a status that names none. */
432 events: string[];
433 /** RFC 3339. */
434 lastSeen: string;
435};
436
437/**
438 * A status context's check name: `CI / pull_request` is the `CI` check,
439 * reported for a `pull_request` event. A context that does not end in an
440 * event, such as `g1t / deploy`, is its own name. As the work service reads it.
441 */
442export function checkName(context: string): string {
443 const at = context.lastIndexOf(" / ");
444 if (at <= 0) return context;
445 return STATUS_EVENTS.has(context.slice(at + 3)) ? context.slice(0, at) : context;
446}
447
448const STATUS_EVENTS = new Set([
449 "push",
450 "pull_request",
451 "pull_request_target",
452 "pull_request_review",
453 "merge_group",
454 "workflow_dispatch",
455 "workflow_run",
456 "workflow_call",
457 "schedule",
458 "release",
459 "issues",
460 "issue_comment",
461 "repository_dispatch",
462]);
463
464/**
465 * Whether a pull request merges cleanly into the branch it targets:
466 * `unknown` when it was never worked out or could not be, `checking` while
467 * a probe merges the two.
468 */
469export type Mergeable = "clean" | "conflicting" | "unknown" | "checking";
470
471/** What a sandbox needs to find out whether a pull request merges cleanly. */
472export type MergecheckJob = {
473 pullId: string;
474 /** Lets the sandbox, and nothing else, report this probe. */
475 token: string;
476 repo: RepoPath;
477 number: number;
478 defaultBranch: string;
479 /** The default branch's commit to merge into. */
480 base: string;
481 /** The repository holding the change: its fork, or the repository. */
482 source: RepoPath;
483 /** The branch of `source` holding it. */
484 branch: string;
485 /** The change's commit. */
486 head: string;
487 /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */
488 author: User;
489};
490
491/** What a workflow run (or another tool) says about a commit. */
492export type CommitStatus = {
493 /** What reported it, such as `CI / push`. */
494 context: string;
495 state: "pending" | "success" | "failure" | "error";
496 description: string | null;
497 targetUrl: string | null;
498 updatedAt: string;
499 /** The integration that reported it: `actions`, `deployments`, `security`, `g1t` or `api`. */
500 source?: string;
501 /** Set on the status a check run stands as: the check run's id. */
502 checkRunId?: string;
503};
504
505/**
506 * A step on the way from an assigned issue to a pull request that is ready
507 * to merge. g1t takes each one without being asked: `working` (the agent is
508 * making the change), `checking`, `reviewing`, `revising` (the agent is
509 * addressing failed checks or a review), `catching_up` (merging in the
510 * branch it would land on), `answering` (woken to answer another agent),
511 * then `ready` for a person to merge. `needs_you`
512 * means g1t has stopped and a person decides what happens next.
513 */
514export type Stage =
515 | "working"
516 | "checking"
517 | "reviewing"
518 | "revising"
519 | "catching_up"
520 | "answering"
521 | "queued"
522 | "ready"
523 | "needs_you";
524
525export type Lifecycle = {
526 stage: Stage;
527 /** One sentence saying what is happening, or why it stopped. */
528 detail: string;
529 /** How many times the agent has been sent back to revise it. */
530 revisions: number;
531};
532
533/**
534 * How a repository wants its pull requests handled. A repository that has
535 * changed nothing has the defaults.
536 */
537export type RepoSettings = {
538 /**
539 * Land a g1t agent's pull request without a person once it is ready:
540 * checks passed and approved as the settings below require.
541 */
542 autoMerge: boolean;
543 /**
544 * The checks that must pass on a pull request's head before it may merge
545 * into the default branch, by name: a workflow's name (`CI`) or another
546 * status's context (`g1t / deploy`). The same for people and agents, and
547 * for the merge queue.
548 */
549 requiredChecks: string[];
550 /**
551 * Refuse to merge a pull request that does not contain the default
552 * branch's latest commits, so that what merges is what was checked. When
553 * off, merging one that is behind brings it up to date first.
554 */
555 requireUpToDate: boolean;
556 /**
557 * How many approving reviews a pull request needs before it may merge. A
558 * reviewer who has since asked for changes blocks it.
559 */
560 requiredApprovals: number;
561 /** Whether a g1t agent's approval counts towards `requiredApprovals`. */
562 countAgentApprovals: boolean;
563 /** Whether someone who may merge can bypass required checks that have not passed. */
564 allowIgnoringChecks: boolean;
565 /**
566 * Merge through a queue: pull requests are tested together with those
567 * ahead of them, and only a combination that passed reaches the default
568 * branch.
569 */
570 mergeQueue: boolean;
571 /**
572 * Refuse to merge until the code owners of every file it changes have
573 * approved, as many as each section asks. People only; `g1t` only where
574 * the file names `@g1t`.
575 */
576 requireCodeOwnerReview?: boolean;
577 /** Username of the member who last changed the settings, if anyone has. */
578 updatedBy: string | null;
579 /** RFC 3339. */
580 updatedAt: string | null;
581};
582
583/** The settings a member can change. */
584export type RepoSettingsInput = Omit<RepoSettings, "updatedBy" | "updatedAt">;
585
586/**
587 * What the runner is to do next for a pull request: always nothing now,
588 * since no step of a pull request is carried out in a sandbox any more.
589 */
590export type Advance = { action: "none" };
591
592export type OpenIssueInput = {
593 title: string;
594 body: string;
595 labels?: string[];
596 /** Deprecated: commands, added to the body under "Definition of done". */
597 checks?: string[];
598 /** The number of the milestone to put it in. Needs the Triage role. */
599 milestone?: number;
600};
601
602export type UpdateIssueInput = {
603 title?: string;
604 body?: string;
605 labels?: string[];
606 /** Usernames of the people it is assigned to; replaces the whole set. */
607 assignees?: string[];
608 /** The number of the milestone to put it in; 0 takes it out. */
609 milestone?: number;
610 /** The issues of the same repository that have to land first; replaces the whole set. */
611 blocked_by?: number[];
612};
613
614export type OpenPullInput = {
615 /** The number of the issue this is for. */
616 issue?: number;
617 /** Defaults to the issue's title; required without an issue. */
618 title?: string;
619 /** What changed and why. Usually set later, when a draft is marked ready. */
620 body?: string;
621 /**
622 * A branch of the repository that already holds the change. The pull
623 * request is then ready for review at once and has no fork.
624 */
625 branch?: string;
626 agent: string;
627 runtime: Runtime;
628 /** The branch to merge into: the default branch when left out. */
629 base?: string;
630 /**
631 * Opened from a branch as a draft, still being worked on: it can't merge
632 * until it is marked ready.
633 */
634 draft?: boolean;
635};
636
637/** One repository's pull requests from `pullsForRepos`, newest first. */
638export type RepoPulls = {
639 repoId: string;
640 /** Draft and open. */
641 open: Pull[];
642 /** Merged and closed. */
643 closed: Pull[];
644};
645
646/** Issues, pull requests, comments and sessions. */
647export interface WorkApi extends RulesApi, ChecksApi, CoordinationApi {
648 openIssue(actor: User, repo: RepoPath, input: OpenIssueInput): Promise<Result<Issue>>;
649 /** Newest first. */
650 listIssues(
651 repo: RepoPath,
652 viewer: Viewer,
653 filter?: { state?: State; label?: string; milestone?: number },
654 ): Promise<Result<Issue[]>>;
655 getIssue(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<IssueDetail>>;
656 /** The author or a member of the workspace may. */
657 updateIssue(actor: User, repo: RepoPath, number: number, input: UpdateIssueInput): Promise<Result<Issue>>;
658 closeIssue(actor: User, repo: RepoPath, number: number, reason?: IssueReason): Promise<Result<Issue>>;
659 reopenIssue(actor: User, repo: RepoPath, number: number): Promise<Result<Issue>>;
660 /** A repository's labels, by name, each with how many issues and pull requests carry it. */
661 listLabels(repo: RepoPath, viewer: Viewer): Promise<Result<Label[]>>;
662 /**
663 * Creates a label, or with `name` changes one; renaming it renames it on
664 * everything that carries it. Needs the Triage role.
665 */
666 saveLabel(
667 actor: User,
668 repo: RepoPath,
669 label: { name?: string; newName?: string; color?: string; description?: string },
670 ): Promise<Result<Label>>;
671 /** Removes a label from the repository and everything carrying it. */
672 deleteLabel(actor: User, repo: RepoPath, name: string): Promise<Result<boolean>>;
673 /** Adds the default labels the repository does not have yet; returns them all. */
674 addDefaultLabels(actor: User, repo: RepoPath): Promise<Result<Label[]>>;
675 /**
676 * The labels of an issue or a pull request, replaced, added to or taken
677 * from. Labels the repository lacks are created for someone with the
678 * Triage role. Returns its labels now.
679 */
680 setLabels(
681 actor: User,
682 repo: RepoPath,
683 number: number,
684 labels: string[],
685 change?: LabelChange,
686 ): Promise<Result<string[]>>;
687 /** Open ones by due date, then closed ones; both unless `state` says. */
688 listMilestones(repo: RepoPath, viewer: Viewer, state?: State): Promise<Result<Milestone[]>>;
689 getMilestone(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<MilestoneDetail>>;
690 /** Creates a milestone, or with `number` changes the fields given. Needs the Triage role. */
691 saveMilestone(
692 actor: User,
693 repo: RepoPath,
694 milestone: { number?: number; title?: string; description?: string; dueOn?: string; state?: State },
695 ): Promise<Result<Milestone>>;
696 deleteMilestone(actor: User, repo: RepoPath, number: number): Promise<Result<boolean>>;
697 /** How many issues and pull requests are open. */
698 counts(repo: RepoPath, viewer: Viewer): Promise<Result<{ issues: number; pulls: number }>>;
699
700 /**
701 * On an issue or a pull request. On a pull request it may name a line of
702 * the change and carry a verdict; nobody can give a verdict on their own.
703 */
704 addComment(actor: User, repo: RepoPath, number: number, comment: NewComment): Promise<Result<Comment>>;
705 /**
706 * Changes a comment's text. Its author may, and so may anyone with the
707 * Maintain role or higher; notes of what happened cannot be changed.
708 */
709 editComment(actor: User, repo: RepoPath, commentId: string, body: string): Promise<Result<Comment>>;
710 /**
711 * Deletes a comment: its author, or anyone with the Maintain role or
712 * higher. A review that gave a verdict cannot be deleted, only edited.
713 */
714 deleteComment(actor: User, repo: RepoPath, commentId: string): Promise<Result<boolean>>;
715
716 /**
717 * What a sandbox says about a run from before checks were workflows: so
718 * one still finishing is recorded.
719 */
720 reportChecks(runId: string, token: string, report: CheckReport): Promise<Result<CheckRun>>;
721 /**
722 * Claims the probe of whether a pull request merges cleanly that a
723 * `pull.mergecheck` event asked for. Refused when it is no longer wanted
724 * or the repository has as many running as it may. For the runner service.
725 */
726 startMergecheck(pullId: string): Promise<Result<MergecheckJob>>;
727 /** Records that a probe could not be carried out. For the runner service. */
728 failMergecheck(pullId: string, token: string, error: string): Promise<Result<Mergeable>>;
729
730 /**
731 * Works out the next step for a pull request g1t is seeing through and,
732 * if there is one to take now, claims it, so that it is taken once
733 * however often this is called. For the runner service.
734 */
735 advance(pullId: string): Promise<Advance>;
736 /** Ids of the open pull requests g1t is seeing through. */
737 managedPulls(repoId?: string): Promise<string[]>;
738
739 /** A repository's merge queue: what is in it, in order, and what recently left. */
740 queue(repo: RepoPath, viewer: Viewer): Promise<Result<QueueView>>;
741 /**
742 * The next batch of combined states to test for a repository, one per
743 * entry; empty while a batch is being tested or nothing waits.
744 */
745 queueBuild(repoId: string): Promise<QueueJob[]>;
746 /** Reports that a combined state could not be built or checked. */
747 failQueue(entryId: string, token: string, error: string): Promise<Result<QueueState>>;
748 /** Takes a pull request out of the merge queue. Members only. */
749 removeFromQueue(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
750
751 getSettings(repo: RepoPath, viewer: Viewer): Promise<Result<RepoSettings>>;
752 /** The check names reported on the repository's commits in the last 30 days, most recent first. */
753 seenChecks(repo: RepoPath, viewer: Viewer): Promise<Result<SeenCheck[]>>;
754 /** Members of the repository's workspace only. */
755 updateSettings(actor: User, repo: RepoPath, settings: RepoSettingsInput): Promise<Result<RepoSettings>>;
756 /** The CODEOWNERS file at a branch (the default when left out), checked. Needs Read. */
757 codeownersErrors(repo: RepoPath, viewer: Viewer, ref?: string | null): Promise<Result<CodeownersReport>>;
758
759 /**
760 * Opens a pull request: a draft with a fork to push to, or, given a
761 * branch, one ready for review.
762 */
763 openPull(actor: User, repo: RepoPath, input: OpenPullInput): Promise<Result<Pull>>;
764 /** Newest first. */
765 listPulls(
766 repo: RepoPath,
767 viewer: Viewer,
768 state?: State,
769 filter?: { label?: string; milestone?: number; base?: string },
770 ): Promise<Result<Pull[]>>;
771 /**
772 * Delivery for up to 100 repositories over UTC days `from` through
773 * `until`, over their whole history. Repositories the viewer cannot read,
774 * and forks, are left out of `projects`.
775 */
776 deliveryInsights(repoIds: string[], viewer: Viewer, from: string, until: string): Promise<DeliveryInsights>;
777 /**
778 * The newest `limit` open and closed pull requests of each repository,
779 * in one call. Repositories the viewer cannot read, and forks, are left
780 * out: ask those with `listPulls`.
781 */
782 pullsForRepos(repoIds: string[], viewer: Viewer, limit: number): Promise<RepoPulls[]>;
783 getPull(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<PullDetail>>;
784 /**
785 * Changes who a pull request is assigned to and whose review is asked
786 * for; each list given replaces the whole set. Asking for `g1t`'s
787 * review does not by itself start one: the runner's `review` does.
788 */
789 updatePull(
790 actor: User,
791 repo: RepoPath,
792 number: number,
793 changes: {
794 assignees?: string[];
795 reviewers?: string[];
796 labels?: string[];
797 /** 0 takes it out of its milestone. */
798 milestone?: number;
799 /** The branch it merges into. Needs the Write role. */
800 base?: string;
801 },
802 ): Promise<Result<Pull>>;
803 /**
804 * Brings a pull request up to date with the default branch in seconds,
805 * without a sandbox, when the two changed different files: the merge
806 * commit is pushed to its branch as `actor`, who must be whoever opened
807 * it (for a fork) or a member (for a branch). Otherwise `needs_agent`, and
808 * nothing is pushed: the runner's `update` is the way on.
809 */
810 catchUpPull(actor: User, repo: RepoPath, number: number): Promise<Result<PullBranchUpdate>>;
811 /** Marks a draft ready for review and sets its description. */
812 readyPull(actor: User, repo: RepoPath, number: number, summary: string): Promise<Result<Pull>>;
813 closePull(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
814 /** Opens a closed pull request again, as the draft it was if it was closed as one. Never a merged one. */
815 reopenPull(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
816 /** Turns an open pull request back into a draft; it leaves the merge queue. */
817 convertPullToDraft(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
818 /**
819 * Lands the pull request on the repository's default branch. Unless
820 * `keepIssueOpen`, that resolves the issue it was for: the issue closes
821 * naming this pull request, and the others still in progress for it close
822 * as superseded. Only members of the repository's workspace may merge.
823 *
824 * If the default branch has moved, the pull request is brought up to date
825 * first and lands when that is done; it comes back still open, and
826 * `PullDetail.landing` is true meanwhile. A repository that requires pull
827 * requests to be up to date refuses instead.
828 */
829 mergePull(
830 actor: User,
831 repo: RepoPath,
832 number: number,
833 options?: {
834 keepIssueOpen?: boolean;
835 /** Merge although required checks have not passed, where the rule requiring them allows it. */
836 ignoreChecks?: boolean;
837 /** Merge past rules a ruleset lets the actor bypass. Recorded as a bypass. */
838 bypassRules?: boolean;
839 },
840 ): Promise<Result<Pull>>;
841 /**
842 * Drafts and open pull requests the viewer started, most recently active
843 * first, each with where it stands if g1t is seeing it through.
844 */
845 listActivePulls(
846 viewer: Viewer,
847 ): Promise<{ pull: Pull; issue: Issue | null; lifecycle: Lifecycle | null }[]>;
848 /**
849 * The issues and pull requests a person opened, a page at a time, only
850 * on repositories the viewer may read. Not found for no such account.
851 */
852 byAuthor(username: string, viewer: Viewer, filter?: AuthoredFilter): Promise<Result<Authored>>;
853 /**
854 * What a person did each day of the last year (issues and pull requests
855 * opened, reviews given), only on repositories the viewer may read: the
856 * calendar on their profile. Not found for no such account.
857 */
858 contributions(username: string, viewer: Viewer): Promise<Result<Contributions>>;
859
860
861
862 appendSession(actor: User, repo: RepoPath, number: number, entries: NewSessionEntry[]): Promise<Result<{ count: number }>>;
863 readSession(repo: RepoPath, number: number, viewer: Viewer, afterSeq?: number): Promise<Result<SessionEntry[]>>;
864}
865
866/**
867 * What to pass `ReposApi.compare` to see what a pull request changes.
868 *
869 * A fork is compared as a whole. A branch is compared by name while the
870 * pull request is open, and by the commit it was merged or closed at
871 * afterwards, so later pushes to the branch do not change the record.
872 */
873export function pullComparison(pull: Pull): {
874 repoId: string;
875 base: string | null;
876 head: string | null;
877 /** The branch it merges into, which it is compared from. */
878 baseBranch: string | null;
879} {
880 const baseBranch = pull.base ?? null;
881 if (pull.forkRepoId) return { repoId: pull.forkRepoId, base: pull.mergeBase, head: null, baseBranch };
882 const settled = pull.status === "merged" || pull.status === "closed";
883 return {
884 repoId: pull.repoId,
885 base: pull.mergeBase,
886 head: (settled && pull.headCommit) || pull.branch,
887 baseBranch,
888 };
889}
890
891
892export type QueueState = "waiting" | "testing" | "passed" | "failed" | "landed" | "removed";
893
894/** One pull request's place in a merge queue. */
895export type QueueEntry = {
896 id: string;
897 number: number;
898 title: string;
899 agent: string;
900 state: QueueState;
901 /** The pull requests merged ahead of it in the state being tested, in order. */
902 ahead: number[];
903 baseCommit: string | null;
904 combinedCommit: string | null;
905 error: string | null;
906 results: CheckResult[];
907 /** Username of whoever merged it into the queue: a person, or `g1t`. */
908 enqueuedBy: string;
909 /** RFC 3339. */
910 createdAt: string;
911 /** RFC 3339. */
912 finishedAt: string | null;
913};
914
915/** A repository's merge queue: what is in it, in order, and what recently left. */
916export type QueueView = {
917 enabled: boolean;
918 active: QueueEntry[];
919 /** Newest first. */
920 recent: QueueEntry[];
921};
922
923export type QueueStackItem = {
924 number: number;
925 title: string;
926 /** The repository holding the change, and its branch. */
927 source: RepoPath;
928 branch: string;
929 commit: string;
930};
931
932/** What a sandbox needs to build and check one combined state. */
933export type QueueJob = {
934 entryId: string;
935 token: string;
936 repo: RepoPath;
937 defaultBranch: string;
938 baseCommit: string;
939 branch: string;
940 stack: QueueStackItem[];
941 /** Always empty: the state is checked by the merge_group workflows run on it. */
942 checks: string[];
943 /** Always empty, as `checks`. */
944 contractChecks: string[];
945 actor: User;
946};
947
948// --- A person's work ---------------------------------------------------------
949// Mirrors the `Authored*` types in `crates/contracts/src/work.rs`.
950
951export type AuthoredKind = "issue" | "pull";
952/** `closed` takes in merged pull requests too; `merged` is only those. */
953export type AuthoredState = "open" | "closed" | "merged";
954/** `created` is newest first, `updated` most recently changed, `oldest` oldest first. */
955export type AuthoredSort = "created" | "updated" | "oldest";
956
957/** The most items one `byAuthor` page holds. */
958export const AUTHORED_PAGE = 25;
959
960export type AuthoredFilter = {
961 kind?: AuthoredKind;
962 state?: AuthoredState;
963 /** `namespace/name`. */
964 repo?: string;
965 sort?: AuthoredSort;
966 /** The `next` of the page before. */
967 before?: string;
968 limit?: number;
969};
970
971export type AuthoredItem = {
972 kind: AuthoredKind;
973 repo: RepoPath;
974 number: number;
975 title: string;
976 /** A merged pull request is closed. */
977 state: State;
978 /** A pull request's own status; null on an issue. */
979 status: PullStatus | null;
980 /** Why an issue was closed. */
981 reason: IssueReason | null;
982 draft: boolean;
983 merged: boolean;
984 /** RFC 3339. */
985 createdAt: string;
986 /** RFC 3339. */
987 updatedAt: string;
988 /** RFC 3339. */
989 mergedAt: string | null;
990};
991
992/** Over every repository the viewer may read, whatever the filters. */
993export type AuthoredCounts = {
994 pullsMerged: number;
995 pullsOpen: number;
996 pulls: number;
997 issues: number;
998 issuesOpen: number;
999};
1000
1001export type Authored = {
1002 items: AuthoredItem[];
1003 /** Pass as `before` for the next page; null on the last. */
1004 next: string | null;
1005 counts: AuthoredCounts;
1006 /** The repositories they worked in that the viewer may read, most work first. */
1007 repos: { repo: RepoPath; count: number }[];
1008};
1009
1010// Mirrors `Contributions` in `crates/contracts/src/work.rs`.
1011
1012/** How many days `contributions` covers: today and the 364 before it. */
1013export const CONTRIBUTION_DAYS = 365;
1014
1015/** One day with something on it; days with nothing are left out. */
1016export type ContributionDay = {
1017 /** `YYYY-MM-DD`, UTC. */
1018 date: string;
1019 /** Everything that day, commits included. */
1020 count: number;
1021 /** How many of `count` are commits they pushed to a default branch. */
1022 commits?: number;
1023};
1024
1025/** A person's year, as far as the viewer may see. */
1026export type Contributions = {
1027 /** Oldest first. */
1028 days: ContributionDay[];
1029 total: number;
1030 /** The first day counted, `YYYY-MM-DD`; the last is today (UTC). */
1031 from: string;
1032};
1033
1034// --- Coordinating work: sessions, claims and dependencies -----------------
1035// Mirrors `crates/contracts/src/work/coordination.rs`.
1036
1037/** The harness a session runs in. */
1038export type Harness = "claude_code" | "claude_managed_agents" | "codex" | "cursor" | "copilot" | "other";
1039
1040/** Active: heard from in 15 minutes; idle: not since; ended: ended, or quiet for 2 hours. */
1041export type SessionState = "active" | "idle" | "ended";
1042
1043/** One run of someone's own harness on a repository. */
1044export type WorkSession = {
1045 /** `ses_…`. */
1046 id: string;
1047 repoId: string;
1048 issue: number | null;
1049 pull: number | null;
1050 branch: string | null;
1051 actor: { id: string; username: string };
1052 harness: Harness;
1053 harnessSessionId: string | null;
1054 title: string | null;
1055 state: SessionState;
1056 headCommit: string | null;
1057 startedAt: string;
1058 lastSeenAt: string;
1059 endedAt: string | null;
1060 /** What the harness said, or `timed_out`. */
1061 endReason: string | null;
1062 /** For a subagent: the session that started it. */
1063 parentId?: string | null;
1064 /** For a subagent: its kind (Explore, security-reviewer). */
1065 agentType?: string | null;
1066 /** For a subagent: the task it was given, in a few words. */
1067 task?: string | null;
1068 /** For a subagent: the prompt its parent gave it. */
1069 prompt?: string | null;
1070 /** What it reported when it finished: a subagent's final message. */
1071 result?: string | null;
1072 model?: string | null;
1073 /** How many subagents it started. */
1074 subagents?: number;
1075 /** In a list, for one with no title: the start of the first prompt it recorded. */
1076 firstPrompt?: string | null;
1077};
1078
1079/** What kind of thing a session touched. */
1080export type TouchKind = "file" | "commit" | "branch" | "pull" | "issue";
1081
1082/** Something a session touched, kept once per session. */
1083export type SessionTouch = { sessionId: string; kind: TouchKind; value: string; at: string };
1084
1085/** How a session was tied to an issue or pull request. */
1086export type LinkVia = "attached" | "claim" | "branch" | "commit" | "trailer" | "opened" | "ref";
1087
1088/** An issue or pull request a session, or one of its subagents, worked on. */
1089export type SessionLink = {
1090 kind: "issue" | "pull";
1091 number: number;
1092 title: string;
1093 /** The issue's state or the pull request's status. */
1094 state: string;
1095 via: LinkVia[];
1096 sessionIds: string[];
1097};
1098
1099/** A skill a session used, by name. */
1100export type SessionSkill = { sessionId: string; name: string; uses: number };
1101
1102export type ClaimState = "active" | "released" | "expired" | "handed_off";
1103
1104/** An advisory, expiring "I'm working on #N". Never a lock. */
1105export type Claim = {
1106 /** `clm_…`. */
1107 id: string;
1108 repoId: string;
1109 issue: number;
1110 sessionId: string | null;
1111 actor: { id: string; username: string };
1112 harness: Harness | null;
1113 note: string | null;
1114 state: ClaimState;
1115 createdAt: string;
1116 expiresAt: string;
1117 endedAt: string | null;
1118 endReason: string | null;
1119 /** For a hand-off: who it was handed to; null for anyone. */
1120 handoffTo: string | null;
1121};
1122
1123/** `session.started` and `session.ended`. */
1124export type SessionEventData = { repoId: string; number?: number; issue?: number; session: WorkSession };
1125
1126/** `work.claimed`, `work.released`, `work.claim_expired` and `work.handoff_requested`. */
1127export type ClaimEventData = {
1128 repoId: string;
1129 number: number;
1130 issue: number;
1131 claim: Claim;
1132 others?: Claim[];
1133 tookOver?: string;
1134 reason?: string;
1135 from?: string;
1136 to?: string;
1137 note?: string;
1138};
1139
1140/** `dependency.changed`. */
1141export type DependencyChangedData = {
1142 repoId: string;
1143 number: number;
1144 issue: number;
1145 blockedBy: number[];
1146 added: number[];
1147 removed: number[];
1148 cleared: number[];
1149 remaining: number[];
1150 mergedPull?: number;
1151};
1152
1153/** `overlap.detected` between two pull requests, sent once for each side of a new overlap. */
1154export type PullOverlapData = {
1155 repoId: string;
1156 number: number;
1157 pull: number;
1158 otherPull: number;
1159 files: string[];
1160 issue?: number;
1161 otherIssue?: number;
1162};
1163
1164/** `overlap.detected` between two sessions that touched the same files (siblings included), once for each side. */
1165export type SessionOverlapData = {
1166 repoId: string;
1167 number?: number;
1168 sessionId: string;
1169 otherSessionId: string;
1170 otherActor: string;
1171 files: string[];
1172 issue?: number;
1173 otherIssue?: number;
1174};
1175
1176/** `overlap.detected`: between pull requests, or between sessions (with `sessionId`). */
1177export type OverlapDetectedData = PullOverlapData | SessionOverlapData;
1178
1179/** `session.progressed`: a session recorded more (at most every few seconds). */
1180export type SessionProgressedData = { repoId: string; number?: number; sessionId: string; rootId?: string };
1181
1182/** An issue in brief. */
1183export type IssueBrief = { number: number; title: string; state: State };
1184
1185/** A pull request in brief. */
1186export type PullBrief = { number: number; title: string; status: PullStatus };
1187
1188/** An issue another is blocked by, and where it stands. */
1189export type Blocker = {
1190 number: number;
1191 title: string;
1192 state: State;
1193 /** Active claims on it. */
1194 claims: Claim[];
1195 pulls: PullBrief[];
1196};
1197
1198/** A pull request's place in the merge queue; `position` 1 is the front. */
1199export type QueuePlace = { state: QueueState; position: number };
1200
1201/** One of an issue's pull requests, with where it stands. */
1202export type WorkPull = {
1203 number: number;
1204 title: string;
1205 status: PullStatus;
1206 branch: string | null;
1207 headCommit: string | null;
1208 author: { id: string; username: string };
1209 /** `failed` when the merge queue took it out, until its head moves. */
1210 checkStatus: CheckStatus | null;
1211 requiredChecks: RequiredCheck[];
1212 mergeable: Mergeable;
1213 /** Other pull requests in progress changing the same files. */
1214 overlaps: Overlap[];
1215 /** Its place in the merge queue, while it is in it. */
1216 queue: QueuePlace | null;
1217};
1218
1219// --- Decision records (crates/contracts/src/decisions.rs) ------------------
1220
1221export type DecisionKind =
1222 | "proposal"
1223 | "claim"
1224 | "integration_failure"
1225 | "integration_passed"
1226 | "fix"
1227 | "approval"
1228 | "merge"
1229 | "note";
1230
1231/** Claimed: someone said so. Tested: a check or the merge queue proved it. Approved: a person approved it. */
1232export type DecisionConfidence = "claimed" | "tested" | "approved";
1233
1234/** A structured reason kept on an issue or pull request, with how sure it is. */
1235export type Decision = {
1236 /** `dec_…`. */
1237 id: string;
1238 issue: number | null;
1239 pull: number | null;
1240 sessionId: string | null;
1241 /** Username of who recorded it; null when g1t did. */
1242 actor: string | null;
1243 kind: DecisionKind;
1244 confidence: DecisionConfidence;
1245 summary: string;
1246 /**
1247 * What it rests on, kept as recorded, in snake_case: `commit`, `check`
1248 * (`name`, `conclusion`, `url`), `combination`, `other_pulls`,
1249 * `suspects`, `bisected`, `merge_queue`.
1250 */
1251 evidence: Record<string, unknown>;
1252 /** RFC 3339. */
1253 createdAt: string;
1254};
1255
1256/** Everything about an issue an agent needs to pick it up: `get_work`. */
1257export type WorkContext = {
1258 issue: Issue;
1259 /** The items under its "Definition of done" heading. */
1260 definitionOfDone: string[];
1261 /** The checks every pull request into the default branch must pass. */
1262 requiredChecks: string[];
1263 /** Active claims on it. */
1264 claims: Claim[];
1265 /** Sessions on it or its pull requests, most recently heard from first. */
1266 sessions: WorkSession[];
1267 pulls: WorkPull[];
1268 blockedBy: Blocker[];
1269 /** The issues it blocks. */
1270 blocks: IssueBrief[];
1271 /** On it and its pull requests, newest first. */
1272 decisions: Decision[];
1273 /** The latest `integration_failure` decision among them. */
1274 lastIntegrationFailure: Decision | null;
1275 /** Its latest coordination events, newest first. */
1276 recent: G1tEvent[];
1277};
1278
1279/** A harness session and what it recorded. */
1280export type WorkSessionDetail = {
1281 /** Pending repository coordination events; reading does not acknowledge them. */
1282 updates?: { id: string; kind: string; data: unknown; at: string }[];
1283 session: WorkSession;
1284 entries: SessionEntry[];
1285 /** Its subagents and theirs, in the order they started. */
1286 children?: WorkSession[];
1287 /** What it and its subagents touched. */
1288 touched?: SessionTouch[];
1289 /** The issues and pull requests it and its subagents worked on. */
1290 links?: SessionLink[];
1291 skills?: SessionSkill[];
1292 /** For a subagent: the session at the top of its tree. */
1293 root?: WorkSession | null;
1294};
1295
1296/** Reading coordination: work service methods (services/work/src/coordination, decisions.rs). */
1297export interface CoordinationApi {
1298 getWork(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<WorkContext>>;
1299 /** Newest first; active ones unless `state` says otherwise. */
1300 listClaims(repo: RepoPath, viewer: Viewer, filter?: { state?: ClaimState; issue?: number }): Promise<Result<Claim[]>>;
1301 /** Most recently heard from first. */
1302 listWorkSessions(
1303 repo: RepoPath,
1304 viewer: Viewer,
1305 filter?: { state?: SessionState; issue?: number; pull?: number; path?: string; subagents?: boolean },
1306 ): Promise<Result<WorkSession[]>>;
1307 getWorkSession(repo: RepoPath, id: string, viewer: Viewer, afterSeq?: number): Promise<Result<WorkSessionDetail>>;
1308 /** An issue's (with its pull requests') or a pull request's, oldest first. */
1309 listDecisions(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<Decision[]>>;
1310}
1311
1312/**
1313 * Delivery counts cover the requested UTC days; waiting counts are current.
1314 * `checking` and `failed` count each open pull request once, by its head's
1315 * checks (statuses, check runs and g1t Actions jobs); one with none is in neither.
1316 */
1317export type DeliveryMetrics = { repoId: string; opened: number; merged: number; active: number; unreviewed: number; checking: number; failed: number; queued: number; leadSeconds: number; reviewSeconds: number; reviewed: number };
1318/** `checksUnavailable`: g1t Actions could not be read, so `checking` and `failed` are not known. */
1319export type DeliveryInsights = { projects: DeliveryMetrics[]; days: { day: string; merged: number }[]; checksUnavailable: boolean };