Archived
make webhook
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
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 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[];
|
||||
summary: string;
|
||||
iterations: number;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
version: number;
|
||||
mode: Mode;
|
||||
status: "success" | "no-changes" | "failed";
|
||||
message: string;
|
||||
plan?: PlanData;
|
||||
implementation?: ImplementationData;
|
||||
}
|
||||
|
||||
export interface Marker {
|
||||
v: number;
|
||||
kind: "status" | "plan" | "implementation" | "pull-request";
|
||||
issue: number;
|
||||
mode?: Mode;
|
||||
request?: string;
|
||||
status?: string;
|
||||
issueDigest?: string;
|
||||
baseSha?: string;
|
||||
planDigest?: string;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
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 `<!-- olixero-ci-agent:${JSON.stringify(value)} -->`;
|
||||
}
|
||||
|
||||
export function parseMarker(
|
||||
body: string,
|
||||
kind?: Marker["kind"],
|
||||
): Marker | undefined {
|
||||
const match = body.match(/<!-- olixero-ci-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 function statusMarker(issue: number, mode: Mode): Marker {
|
||||
return { v: protocolVersion, kind: "status", issue, mode };
|
||||
}
|
||||
|
||||
export function formatError(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export function assertPlanDraft(value: unknown): PlanDraft {
|
||||
const draft = value as Partial<PlanDraft>;
|
||||
if (
|
||||
!draft ||
|
||||
typeof draft.planMarkdown !== "string" ||
|
||||
draft.planMarkdown.trim().length < 40
|
||||
) {
|
||||
throw new Error("Planner returned an invalid or empty plan");
|
||||
}
|
||||
if (draft.planMarkdown.length > 200_000)
|
||||
throw new Error("Planner returned an oversized plan");
|
||||
if (typeof draft.summary !== "string" || !draft.summary.trim()) {
|
||||
throw new Error("Planner returned no summary");
|
||||
}
|
||||
if (draft.summary.length > 20_000)
|
||||
throw new Error("Planner returned an oversized summary");
|
||||
return {
|
||||
planMarkdown: draft.planMarkdown.trim(),
|
||||
summary: draft.summary.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function assertReviewDecision(value: unknown): ReviewDecision {
|
||||
const review = value as Partial<ReviewDecision>;
|
||||
if (review?.verdict !== "accept" && review?.verdict !== "revise") {
|
||||
throw new Error("Reviewer returned an invalid verdict");
|
||||
}
|
||||
if (
|
||||
!Array.isArray(review.findings) ||
|
||||
!review.findings.every((item) => typeof item === "string")
|
||||
) {
|
||||
throw new Error("Reviewer returned invalid findings");
|
||||
}
|
||||
if (
|
||||
review.findings.length > 100 ||
|
||||
review.findings.some((item) => item.length > 10_000)
|
||||
) {
|
||||
throw new Error("Reviewer returned oversized findings");
|
||||
}
|
||||
if (typeof review.rationale !== "string")
|
||||
throw new Error("Reviewer returned no rationale");
|
||||
if (review.rationale.length > 20_000)
|
||||
throw new Error("Reviewer returned an oversized rationale");
|
||||
return {
|
||||
verdict: review.verdict,
|
||||
findings: review.findings.map((item) => item.trim()).filter(Boolean),
|
||||
rationale: review.rationale.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function assertImplementationSummary(
|
||||
value: unknown,
|
||||
): ImplementationSummary {
|
||||
const summary = value as Partial<ImplementationSummary>;
|
||||
if (
|
||||
!summary ||
|
||||
typeof summary.summary !== "string" ||
|
||||
!summary.summary.trim()
|
||||
) {
|
||||
throw new Error("Implementation agent returned no summary");
|
||||
}
|
||||
if (summary.summary.length > 20_000)
|
||||
throw new Error("Implementation agent returned an oversized summary");
|
||||
if (
|
||||
!Array.isArray(summary.files) ||
|
||||
!summary.files.every((item) => typeof item === "string")
|
||||
) {
|
||||
throw new Error("Implementation agent returned an invalid file list");
|
||||
}
|
||||
if (
|
||||
summary.files.length > 100 ||
|
||||
summary.files.some((item) => item.length > 1_000)
|
||||
) {
|
||||
throw new Error("Implementation agent returned an oversized file list");
|
||||
}
|
||||
return { summary: summary.summary.trim(), files: summary.files };
|
||||
}
|
||||
Reference in New Issue
Block a user