make webhook

This commit is contained in:
2026-07-13 18:50:38 +02:00
parent 2f128550fc
commit 40c822d3bf
67 changed files with 6286 additions and 2053 deletions
+206
View File
@@ -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 };
}