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