This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
ci-agent-runner/app/src/core/contracts.ts
T

189 lines
4.4 KiB
TypeScript

import { createHash } from "node:crypto";
import type {
FeedbackReference,
PlanDiscussionData,
PlanReviewData,
PullReviewData,
} from "./features/contracts.js";
export type {
FeedbackReference,
ImplementationFixReply,
PlanConversationReply,
PlanDiscussionData,
PlanReviewData,
PullReviewData,
PullReviewFinding,
PullReviewReply,
} from "./features/contracts.js";
export {
assertImplementationFixReply,
assertImplementationSummary,
assertPlanConversationReply,
assertPlanDraft,
assertPullReviewReply,
assertReviewDecision,
} from "./features/validation.js";
export const protocolVersion = 1;
export const planLabel = "agent:plan";
export const implementLabel = "agent:implement";
export const generatedLabel = "agent:generated";
export const blockedLabel = "agent:blocked";
export const planReadyLabel = "agent:plan-ready";
export const reviewFixLabel = "agent:fix-review";
export function log(
level: string,
message: string,
fields: Record<string, unknown>,
): string {
return JSON.stringify({
level,
message,
...fields,
time: new Date().toISOString(),
});
}
export type Mode = "plan" | "implement";
export interface PlanData {
issueDigest: string;
baseSha: string;
planDigest: string;
markdown: string;
summary: string;
iterations: number;
}
export interface ImplementationData {
issueDigest: string;
planDigest: string;
branch: string;
baseBranch: string;
baseSha: string;
startingRemoteSha: string | null;
gitSafetyDigest: string;
diffDigest: string;
changedFiles: string[];
pendingFiles: string[];
summary: string;
iterations: number;
pullRequestNumber?: number;
response?: string;
feedback?: FeedbackReference[];
}
export interface Result {
version: number;
mode: Mode;
status: "success" | "no-changes" | "failed";
message: string;
plan?: PlanData;
implementation?: ImplementationData;
discussion?: PlanDiscussionData;
review?: PlanReviewData | PullReviewData;
}
export interface Marker {
v: number;
kind:
| "status"
| "plan"
| "implementation"
| "pull-request"
| "conversation"
| "review";
issue: number;
mode?: Mode;
request?: string;
status?: string;
issueDigest?: string;
baseSha?: string;
planDigest?: string;
branch?: string;
pullRequest?: number;
}
export interface IssueSnapshot {
digest: string;
title: string;
body: string;
comments: Array<{
id: number;
author: string;
createdAt: string;
body: string;
}>;
}
export interface PlanDraft {
planMarkdown: string;
summary: string;
}
export interface ReviewDecision {
verdict: "accept" | "revise";
findings: string[];
rationale: string;
}
export interface ImplementationSummary {
summary: string;
files: string[];
}
export function requireEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value)
throw new Error(`Required environment variable ${name} is not set`);
return value;
}
export function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
export function marker(value: Marker): string {
return `<!-- gitea-agent:${JSON.stringify(value)} -->`;
}
export function parseMarker(
body: string,
kind?: Marker["kind"],
): Marker | undefined {
const match = body.match(/<!-- gitea-agent:(\{[^\n]*\}) -->/);
if (!match?.[1]) return undefined;
try {
const value = JSON.parse(match[1]) as Marker;
if (
value.v !== protocolVersion ||
!value.kind ||
!Number.isInteger(value.issue)
)
return undefined;
if (kind && value.kind !== kind) return undefined;
return value;
} catch {
return undefined;
}
}
export const statusMarker = (issue: number, mode: Mode): Marker => ({
v: protocolVersion,
kind: "status",
issue,
mode,
});
export function formatError(error: unknown): string {
if (error instanceof Error) {
if (error.cause === undefined) return error.message;
const cause = formatError(error.cause);
return cause && cause !== error.message
? `${error.message}: ${cause}`
: error.message;
}
return String(error);
}