g1t

syntaqx/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/packages/contracts/src/work.ts

335 lines11,276 bytes
import type { User, Viewer } from "./identity";
import type { RepoPath } from "./repos";
import type { Result } from "./result";

/**
 * The filter on lists of issues and pull requests. An open pull request is
 * a draft or one ready for review; a closed one was merged or closed
 * without merging.
 */
export type State = "open" | "closed";

/** Why an issue was closed. */
export type IssueReason = "completed" | "not_planned";

/**
 * Something that should change in a repository: a bug, a feature, a
 * question. Opened by a person, an agent or an integration. Pull requests
 * are made against it; the one that is merged resolves it.
 *
 * Issues and pull requests share one sequence of numbers per repository.
 */
export type Issue = {
  id: string;
  repoId: string;
  /** Shown as `#12`. */
  number: number;
  title: string;
  /** Markdown. Also what an agent is given to work from. */
  body: string;
  labels: string[];
  /** Commands that must pass for a pull request to be accepted. */
  checks: string[];
  state: State;
  /** Set when closed. */
  reason: IssueReason | null;
  /** The number of the pull request whose merge closed this issue. */
  resolvedBy: number | null;
  author: User;
  /** RFC 3339. */
  createdAt: string;
  /** RFC 3339. */
  updatedAt: string;
  /** RFC 3339. */
  closedAt: string | null;
  /** Pull requests made against this issue, in any state. */
  pullCount: number;
  commentCount: number;
};

/** `draft` is still being worked on; `open` is ready for review. */
export type PullStatus = "draft" | "open" | "merged" | "closed";

/** Where the agent runs: on g1t's sandboxes, or in someone's own session. */
export type Runtime = "hosted" | "external";

/**
 * A proposed change. It is made either in a fork created for it, which is
 * how agents work, or on a branch pushed to the repository itself.
 */
export type Pull = {
  id: string;
  repoId: string;
  /** Shown as `#12`. */
  number: number;
  /** The number of the issue this is for, if any. */
  issue: number | null;
  title: string;
  /** Markdown: what changed and why. Set when marked ready. */
  body: string | null;
  /** A label for the agent doing the work, e.g. `claude-code`. */
  agent: string;
  runtime: Runtime;
  status: PullStatus;
  /** The fork holding the change, unless it is on a branch. */
  fork: RepoPath | null;
  /** The fork's repository id. */
  forkRepoId: string | null;
  /** The branch of the repository holding the change, unless it is in a fork. */
  branch: string | null;
  headCommit: string | null;
  /**
   * For a merged pull request, what the branch pointed to before the merge.
   * Comparing against it shows what the pull request changed.
   */
  mergeBase: string | null;
  /** Username of whoever merged it. */
  mergedBy: string | null;
  /** RFC 3339. */
  mergedAt: string | null;
  /**
   * Set on a pull request closed because another one for the same issue was
   * merged: that one's number.
   */
  supersededBy: number | null;
  /**
   * Where the latest run of the issue's acceptance checks stands, if there
   * has been one against the current head.
   */
  checkStatus: CheckStatus | null;
  author: User;
  /** RFC 3339. */
  createdAt: string;
  /** RFC 3339. */
  updatedAt: string;
};

/** `queued` waits for a sandbox; `errored` means the checks could not be run. */
export type CheckStatus = "queued" | "running" | "passed" | "failed" | "errored";

/** How one acceptance check went. */
export type CheckResult = {
  command: string;
  passed: boolean;
  /** Null when the command was stopped for taking too long. */
  exitCode: number | null;
  /** What the command printed; the end of it, when there was a lot. */
  output: string;
  durationMs: number;
};

/**
 * One run of an issue's acceptance checks against a pull request's head, in
 * a sandbox that holds nothing but that commit.
 */
export type CheckRun = {
  id: string;
  /** The commit that was checked. */
  headCommit: string;
  status: CheckStatus;
  results: CheckResult[];
  /** Why the checks could not be run, when `status` is `errored`. */
  error: string | null;
  /** RFC 3339. */
  createdAt: string;
  /** RFC 3339. */
  finishedAt: string | null;
};

/** A reviewer's decision on a pull request. */
export type Verdict = "approve" | "request_changes";

/**
 * A comment on an issue or a pull request. On a pull request it can sit on
 * one line of the change, and it can carry a reviewer's verdict.
 */
export type Comment = {
  id: string;
  author: User;
  /** Markdown. */
  body: string;
  /** The file commented on, for a comment on a line. */
  path: string | null;
  /** The line of that file, as numbered after the change. */
  line: number | null;
  verdict: Verdict | null;
  /** RFC 3339. */
  createdAt: string;
};

export type NewComment = {
  /** May be empty when approving. */
  body: string;
  path?: string;
  line?: number;
  verdict?: Verdict;
};

/** What a sandbox needs to carry out a check run. */
export type CheckJob = {
  runId: string;
  /** Lets the sandbox, and nothing else, report this run's results. */
  token: string;
  commands: string[];
  /** The repository holding the commit: the fork, or the repository itself. */
  source: RepoPath;
  commit: string;
  /** Who opened the pull request, and so can read its source. */
  author: User;
  /** Username of whoever wrote the checks: the issue's author. */
  requestedBy: string;
  repo: RepoPath;
  number: number;
};

export type CheckReport = { results?: CheckResult[]; error?: string; skip?: boolean };

export type SessionEntryKind = "prompt" | "message" | "tool_call" | "tool_result" | "note";

/** One step of an agent's session: the "why" behind a pull request's commits. */
export type SessionEntry = {
  seq: number;
  kind: SessionEntryKind;
  text: string;
  /** For tool calls and results. */
  tool: string | null;
  /** The fork's head commit when this entry was recorded, if known. */
  commit: string | null;
  /** RFC 3339. */
  at: string;
};

export type NewSessionEntry = Pick<SessionEntry, "kind" | "text"> &
  Partial<Pick<SessionEntry, "tool" | "commit">>;

export type IssueDetail = {
  issue: Issue;
  /** Every pull request made against it, oldest first. */
  pulls: Pull[];
  comments: Comment[];
};

export type PullDetail = {
  pull: Pull;
  /** The issue it is for, if any. */
  issue: Issue | null;
  comments: Comment[];
  /** The latest run of the issue's acceptance checks. */
  checks: CheckRun | null;
};

export type OpenIssueInput = {
  title: string;
  body: string;
  labels?: string[];
  checks?: string[];
};

export type UpdateIssueInput = { title?: string; body?: string; labels?: string[] };

export type OpenPullInput = {
  /** The number of the issue this is for. */
  issue?: number;
  /** Defaults to the issue's title; required without an issue. */
  title?: string;
  /** What changed and why. Usually set later, when a draft is marked ready. */
  body?: string;
  /**
   * A branch of the repository that already holds the change. The pull
   * request is then ready for review at once and has no fork.
   */
  branch?: string;
  agent: string;
  runtime: Runtime;
};

/** Issues, pull requests, comments and sessions. */
export interface WorkApi {
  openIssue(actor: User, repo: RepoPath, input: OpenIssueInput): Promise<Result<Issue>>;
  /** Newest first. */
  listIssues(
    repo: RepoPath,
    viewer: Viewer,
    filter?: { state?: State; label?: string },
  ): Promise<Result<Issue[]>>;
  getIssue(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<IssueDetail>>;
  /** The author or a member of the workspace may. */
  updateIssue(actor: User, repo: RepoPath, number: number, input: UpdateIssueInput): Promise<Result<Issue>>;
  closeIssue(actor: User, repo: RepoPath, number: number, reason?: IssueReason): Promise<Result<Issue>>;
  reopenIssue(actor: User, repo: RepoPath, number: number): Promise<Result<Issue>>;
  /** The default labels, then every other label in use on the repository. */
  listLabels(repo: RepoPath, viewer: Viewer): Promise<Result<string[]>>;
  /** How many issues and pull requests are open. */
  counts(repo: RepoPath, viewer: Viewer): Promise<Result<{ issues: number; pulls: number }>>;

  /**
   * On an issue or a pull request. On a pull request it may name a line of
   * the change and carry a verdict; nobody can give a verdict on their own.
   */
  addComment(actor: User, repo: RepoPath, number: number, comment: NewComment): Promise<Result<Comment>>;

  /**
   * Begins a run of the acceptance checks for a pull request that is ready
   * for review. For the runner service, which starts the sandbox.
   */
  startChecks(pullId: string): Promise<Result<CheckJob>>;
  /**
   * What a sandbox says about its run. With no results and no error it has
   * started; `skip` forgets a run that will not be carried out.
   */
  reportChecks(runId: string, token: string, report: CheckReport): Promise<Result<CheckRun>>;

  /**
   * Opens a pull request: a draft with a fork to push to, or, given a
   * branch, one ready for review.
   */
  openPull(actor: User, repo: RepoPath, input: OpenPullInput): Promise<Result<Pull>>;
  /** Newest first. */
  listPulls(repo: RepoPath, viewer: Viewer, state?: State): Promise<Result<Pull[]>>;
  getPull(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<PullDetail>>;
  /** Marks a draft ready for review and sets its description. */
  readyPull(actor: User, repo: RepoPath, number: number, summary: string): Promise<Result<Pull>>;
  closePull(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
  /**
   * Lands the pull request on the repository's default branch. Unless
   * `keepIssueOpen`, that resolves the issue it was for: the issue closes
   * naming this pull request, and the others still in progress for it close
   * as superseded. Only members of the repository's workspace may merge.
   */
  mergePull(
    actor: User,
    repo: RepoPath,
    number: number,
    options?: {
      keepIssueOpen?: boolean;
      /** Merge although the acceptance checks have not passed. */
      ignoreChecks?: boolean;
    },
  ): Promise<Result<Pull>>;
  /** Drafts and open pull requests the viewer started, most recently active first. */
  listActivePulls(viewer: Viewer): Promise<{ pull: Pull; issue: Issue | null }[]>;

  appendSession(actor: User, repo: RepoPath, number: number, entries: NewSessionEntry[]): Promise<Result<{ count: number }>>;
  readSession(repo: RepoPath, number: number, viewer: Viewer, afterSeq?: number): Promise<Result<SessionEntry[]>>;
}

/**
 * What to pass `ReposApi.compare` to see what a pull request changes.
 *
 * A fork is compared as a whole. A branch is compared by name while the
 * pull request is open, and by the commit it was merged or closed at
 * afterwards, so later pushes to the branch do not change the record.
 */
export function pullComparison(pull: Pull): {
  repoId: string;
  base: string | null;
  head: string | null;
} {
  if (pull.forkRepoId) return { repoId: pull.forkRepoId, base: pull.mergeBase, head: null };
  const settled = pull.status === "merged" || pull.status === "closed";
  return {
    repoId: pull.repoId,
    base: pull.mergeBase,
    head: (settled && pull.headCommit) || pull.branch,
  };
}