/** * `agent-native recap` — the helper surface used by the PR Visual Recap GitHub * Action. Run `agent-native recap help` for the full subcommand list. * * The action no longer generates the recap deterministically. Instead a coding * agent (Claude Code or Codex) RUNS THE REPO'S visual-recap skill against the * diff and publishes the plan via the plan MCP tools. These subcommands are the * thin, deterministic glue around that: * * gate The security boundary: decide whether the recap runs at all * (skipping drafts, forks without secret access, bots, missing * secrets, an invalid agent/model, and untrusted PRs that touch * recap-control files) and which normalized backend agent to use. * collect-diff Collect the bounded base...head diff (excluding lockfiles, * build output, snapshots), cap it at ~600KB, and classify the * huge/tiny flags. * scan Refuse to hand a secret-leaking diff to the agent. * block-reference * Fetch the live get-plan-blocks reference for the target app. * build-prompt Assemble the agent prompt = latest visual-recap skill bundle * + a task wrapper (or repo-pinned skill with --skill-source). * publish Publish the agent-authored recap-source.json over HTTP. * shot Screenshot the published plan and upload it to the plan app's * signed public image route (for an inline PR-comment image). * usage Parse and emit agent token-usage/cost from stdout. * comment Find the previous plan id / upsert the sticky PR comment. * check Evaluate the recap result and set a GitHub commit status. * setup Install the PR Visual Recap GitHub Action workflow. * doctor Diagnose missing secrets / misconfigured workflow. * * Promoting these to the published CLI means an installed repo's workflow calls * `agent-native recap …` instead of copying helper scripts into the repo. * * Node built-ins only (plus an optional dynamic `playwright` import for `shot`). */ /** GitHub secrets the installed PR Visual Recap workflow needs. */ export declare const PR_VISUAL_RECAP_SETUP: string[]; /** * Result of attempting to write the PR Visual Recap workflow. * * - `written` — the file was written (new or forced overwrite). * - `skipped` — the file already exists and is identical; no-op. * - `refused` — the file already exists and differs; nothing was written. * Caller should re-run with `--force` (or pass `force: true`) to overwrite. */ export type WriteWorkflowResult = { status: "written"; path: string; existed: boolean; } | { status: "skipped"; path: string; } | { status: "refused"; path: string; message: string; }; /** Write .github/workflows/pr-visual-recap.yml into a repo. */ export declare function writePrVisualRecapWorkflow(baseDir: string, options?: { force?: boolean; }): WriteWorkflowResult; /** * The thin caller workflow that consumers paste into their repo when using the * reusable variant. It references the canonical reusable workflow in the * BuilderIO/agent-native repo rather than carrying a full copy. * * Callers must trigger on the same `pull_request` event types so that * `github.event.pull_request.*` expressions in the reusable workflow resolve * correctly (workflow_call inherits the caller's event context). The `labeled` * event lets required-label configurations run as soon as a maintainer opts in. * * @param options.cliVersion Semver or tag to pin (default "main" / latest). * @param options.ref Git ref to pin the reusable workflow to (default "@main"). */ export declare function buildReusableCallerWorkflow(options?: { ref?: string; agent?: RecapAgentValue; model?: string; runsOn?: string; gateRunsOn?: string; requiredLabels?: string; }): string; /** Write the thin caller workflow that references the reusable workflow. */ export declare function writePrVisualRecapReusableCallerWorkflow(baseDir: string, options?: { force?: boolean; ref?: string; agent?: RecapAgentValue; model?: string; runsOn?: string; gateRunsOn?: string; requiredLabels?: string; }): WriteWorkflowResult; type RecapAgentValue = "claude" | "codex" | "openai-compatible"; export type RecapAgent = "claude" | "codex" | "openai-compatible"; export declare function normalizeRecapAgent(value: string | undefined): RecapAgent; export declare function recapRequiredSecrets(agent: RecapAgent): string[]; export interface RecapRunner { name: string; status: string; labels: string[]; } export declare function matchingRecapRunners(runners: RecapRunner[], requiredLabels: string[]): RecapRunner[]; export interface RecapSetupPlan { agent: RecapAgent; appUrl: string; repo?: string; workflowPath: string; workflowExists: boolean; requiredSecrets: string[]; requiredVariables: readonly RecapVariableRequirement[]; variableProblems: RecapVariableProblem[]; variableValues: Record; secretValues: Record; } export interface RecapVariableRequirement { name: "VISUAL_RECAP_BASE_URL" | "VISUAL_RECAP_MODEL" | "VISUAL_RECAP_RUNS_ON" | "VISUAL_RECAP_GATE_RUNS_ON"; example: string; } export interface RecapVariableProblem { requirement: RecapVariableRequirement; reason: string; } export interface RecapRunsOnConfig { json: string; labels: string[]; selfHosted: boolean; } /** Parse the JSON consumed by GitHub Actions `fromJSON(...)` for `runs-on`. */ export declare function parseRecapRunsOn(value: string): RecapRunsOnConfig; /** Validate the plain label used directly by the gate job's `runs-on`. */ export declare function parseRecapGateRunsOn(value: string): string; export declare function validateOpenAiCompatibleRecapVariables(input: { baseUrl?: string; model?: string; }): RecapVariableProblem[]; export declare function buildRecapSetupPlan(input: { baseDir: string; appUrl?: string; agent?: string; repo?: string; runsOn?: string; gateRunsOn?: string; requiredLabels?: string; env?: NodeJS.ProcessEnv; }): RecapSetupPlan; export type RecapSecretScanMode = "off" | "high-confidence" | "strict"; export declare function normalizeRecapSecretScanMode(value: string | undefined): RecapSecretScanMode; export declare function lineLooksSecret(line: string, mode?: RecapSecretScanMode): boolean; /** * Parse a `.github/recap-scan-allowlist` file into a list of matchers. * Each non-blank, non-comment line is either: * - a `/regex/` literal (JS regex syntax) — matched against the full line * - a plain literal string — checked with String.includes() * * Returns an empty array when the file is absent or empty. */ export declare function parseRecapScanAllowlist(allowlistPath: string): Array; /** * Return true when `line` matches ANY entry in the allowlist (i.e., the * finding should be ignored). */ export declare function lineMatchesAllowlist(line: string, allowlist: Array): boolean; export declare function diffContainsSecret(diffText: string, allowlist?: Array, mode?: RecapSecretScanMode): boolean; export declare function sanitizeAgentFailureSummary(value: string, maxChars?: number): string; export declare function summarizeAgentResult(agent: string, resultText: string): string; export declare function summarizeAgentRun(input: { agent: string; resultText?: string; stderrText?: string; exitCode?: string; }): string; export declare function summarizeLocalAgentFailure(input?: { cwd?: string; agent?: string; }): string; /** ~600KB byte cap for the diff handed to the recap agent. */ export declare const RECAP_DIFF_BYTE_CAP = 614400; /** The footer appended when a diff is truncated at the byte cap. */ export declare const RECAP_DIFF_TRUNCATED_FOOTER = "\n\n[diff truncated at 600KB for the recap agent]\n"; /** * Classify a bounded diff into the `huge` / `tiny` flags the workflow consumes. * * - huge: BYTES over the ~600KB cap. The agent is told to summarize AND the * diff file is physically truncated so it can't overflow the prompt budget. * - tiny: <= 1 changed file AND <= 8 changed lines. Uses ORIGINAL line count * (captured before any truncation) so a large diff is never misclassified as * tiny after the byte cap drops most of its lines. * * Pure (no I/O) so the classification can be unit-tested without invoking git. */ export declare function classifyDiff(input: { bytes: number; changed: number; originalLines: number; }): { huge: boolean; tiny: boolean; }; /** * Reorder a unified diff's per-file segments so likely-noise paths (paths whose * first component starts with `.`, e.g. `.changeset/`, `.github/`) sort LAST, * and all other paths keep their original git order. This ensures that when * `truncateDiffAtLineBoundary` drops the tail to stay under the byte cap, source * files survive and dotfile dirs are sacrificed instead. * * Pure (string in → string out) for unit testing. The initial preamble (lines * before the first `diff --git` header) is preserved unchanged. */ export declare function sortDiffSourceFirst(text: string): string; /** * Truncate a diff to the ~600KB byte cap at a COMPLETE LINE boundary, then * append the truncated footer. Dropping the last (possibly-partial) line is the * equivalent of the original `head -c 614400 | sed '$d'`: it guarantees the cap * never cuts a multi-byte UTF-8 char or a diff line mid-way and corrupts the * agent's input. Pure (string in, string out) so it can be unit-tested. */ export declare function truncateDiffAtLineBoundary(text: string): string; /** * Count lines that begin with `+` or `-` (added/removed diff lines), excluding * the `+++ b/file` / `--- a/file` unified-diff header lines. Without this * exclusion a single-file change loses ~2 "real" lines from the 8-line tiny * threshold, incorrectly classifying a small-but-meaningful change as tiny. */ export declare function countDiffLines(diffText: string): number; /** * Locate the repo's visual-recap SKILL.md, preferring the host-agent install * locations so a user's `agent-native skills add` copy wins, then falling back * to the framework's own source locations. */ export declare function readRepoSkillMd(cwd?: string): { text: string; source: string; }; type RecapSkillSourceMode = "auto" | "latest" | "repo"; export declare function readVisualRecapSkillBundle(cwd?: string, mode?: RecapSkillSourceMode): { text: string; source: string; }; export declare function buildRecapPrompt(input: { skillMd: string; pr: string; repo?: string; head?: string; appUrl: string; diffPath: string; statPath?: string; blockReferencePath?: string; prevPlanId?: string; huge?: boolean; localFiles?: boolean; localDir?: string; /** Fully-qualified PR URL to store on the plan as the back-link. When * `repo` is supplied this is auto-derived; pass explicitly to override. */ sourceUrl?: string; /** * When true, the diff originates from a fork PR — an external contributor's * branch. Add an explicit prompt-hardening note so the agent treats diff * content as untrusted user data, never as instructions. This does NOT change * what the agent is allowed to do; it is a reminder that the diff text is * attacker-controlled input to an LLM that holds a publish token. */ forkPr?: boolean; /** * Byte size of the (possibly truncated) diff file — used to emit a * consumption instruction so the agent knows how large the file is and reads * it in full before authoring. When omitted, no size instruction is emitted. */ diffBytes?: number; /** * Line count of the (possibly truncated) diff — same purpose as diffBytes. */ diffLines?: number; }): string; type RecapScreenshotTheme = "light" | "dark"; type GitHubComment = { id: number; body?: string | null; html_url?: string; user?: { type?: string | null; } | null; }; export declare function resolveGitHubPullRequestAuthor(input: { token: string; repo: string; pr: string; fetchFn?: typeof fetch; }): Promise<{ email?: string; name?: string; login?: string; }>; export declare function isPullRequestHeadCurrent(input: { token: string; owner: string; repo: string; issue: string; headSha: string; fetchFn?: typeof fetch; }): Promise; export declare function findExistingComment(input: { token: string; owner: string; repo: string; issue: string; /** @internal test seam — defaults to global fetch */ fetchFn?: typeof fetch; }): Promise; export declare function upsertComment(input: { token: string; owner: string; repo: string; issue: string; body: string; /** When true, refresh an existing comment but never create a new one. */ updateOnly?: boolean; /** @internal test seam — defaults to global fetch */ fetchFn?: typeof fetch; }): Promise<{ action: "created" | "updated" | "skipped"; id: number; html_url?: string; }>; export declare function withRecapImageCacheKey(imageUrl: string, cacheKey: string | undefined | null): string; /** Build the sticky comment body from the workflow's environment. */ export declare function buildCommentBody(env?: NodeJS.ProcessEnv): string; export declare class RecapPublishHttpError extends Error { readonly status: number; constructor(status: number, message: string); } export declare function isRepairableRecapPublishError(error: unknown): error is RecapPublishHttpError; type RecapSourceFilePayload = { title?: string; brief?: string; mdx: Record; }; export declare function readRecapSourcePayload(filePath?: string): RecapSourceFilePayload; export declare function validateRecapRepairSource(input: { originalPath: string; sourcePath: string; reason: string; }): { targetFile: string; }; export declare function buildRecapRepairPrompt(input: { reason: string; sourcePath?: string; }): string; export declare function fetchRecapBlockReference(input: { appUrl: string; out?: string; fetchFn?: typeof fetch; }): Promise<{ ok: true; out: string; count?: number; }>; export declare function publishRecapSource(input: { appUrl: string; token: string; githubToken?: string; sourcePath?: string; out?: string; prevPlanId?: string; repo?: string; pr?: string; sourceUrl?: string; sourceType?: string; sourceRepo?: string; sourcePrNumber?: string; sourcePrState?: string; sourcePrMergedAt?: string; sourceAuthorEmail?: string; sourceAuthorName?: string; sourceAuthorLogin?: string; fetchFn?: typeof fetch; cwd?: string; }): Promise<{ ok: true; url: string; out: string; }>; /** * Confirm GitHub can fetch the uploaded image anonymously before we embed it. * * Default budget: 8 attempts with capped exponential backoff (1s, 2s, 3s, … * capped at 4s) → ~20s total. This is enough to survive a cold-start CDN * propagation delay that would otherwise cause `uploadRecapImage` to return a * URL that the GitHub PR comment can't display. * * The `attempts` and `delayMs` overrides remain for unit tests and for callers * that need a tighter or looser budget. */ export declare function waitForPublicRecapImage(input: { imageUrl: string; attempts?: number; delayMs?: number; fetchFn?: typeof fetch; }): Promise; /** Upload a PNG to the plan app's signed public image route; returns its URL. */ export declare function uploadRecapImage(input: { appUrl: string; token: string; pngPath: string; cacheKey?: string; /** @internal test seam — defaults to global fetch */ fetchFn?: typeof fetch; /** @internal test seam — defaults to waitForPublicRecapImage */ waitFn?: typeof waitForPublicRecapImage; }): Promise; type PlaywrightModule = { chromium: import("playwright").BrowserType; }; export declare function launchRecapChromium(chromium: import("playwright").BrowserType): Promise; export declare function withRecapScreenshotParams(url: string, options?: { theme?: RecapScreenshotTheme; }): string; export declare function runShot(args: Record, /** @internal test seam — defaults to dynamic playwright import */ importPlaywright?: () => Promise): Promise; /** * Minimal shape of the `pull_request` object from a GitHub `pull_request` event * payload that the gate inspects. Everything is optional so a malformed/partial * payload degrades to "skip" rather than throwing. */ export interface RecapGatePullRequest { number?: number; draft?: boolean; author_association?: string | null; head?: { repo?: { full_name?: string | null; } | null; } | null; user?: { login?: string | null; type?: string | null; } | null; labels?: Array | null; } export interface RecapGateInput { /** The `pull_request` payload object, or null when absent. */ pr: RecapGatePullRequest | null; /** GITHUB_REPOSITORY ("owner/name"). */ repository: string | undefined; /** Whether the base repository is private. */ repositoryPrivate?: boolean; /** PLAN_RECAP_TOKEN present. */ hasPlan: boolean; /** ANTHROPIC_API_KEY present. */ hasAnthropic: boolean; /** OPENAI_API_KEY present. */ hasOpenai: boolean; /** VISUAL_RECAP_API_KEY present for OpenAI-compatible backends. */ hasOpenaiCompatible?: boolean; /** Raw VISUAL_RECAP_AGENT value (may be undefined / mis-cased). */ agentRaw: string | undefined; /** Raw VISUAL_RECAP_MODEL value (may be undefined). */ model: string | undefined; /** Raw VISUAL_RECAP_BASE_URL value for OpenAI-compatible backends. */ baseUrl?: string; /** Raw VISUAL_RECAP_SKILL_SOURCE value (auto/latest/repo; may be undefined). */ skillSource: string | undefined; /** Comma-separated PR labels required before the recap runs. */ requiredLabels?: string; /** Filenames changed by the PR (for the self-modifying guard). */ changedFiles: string[]; } export declare function isRecapSensitivePath(p: string, options?: { skillSource?: string; }): boolean; /** * The pure gate decision: given the PR payload, secret-presence flags, the * configured backend/model, and the PR's changed files, decide whether the * visual recap should run, which (normalized) agent to use, and — when skipped — * the human-readable reasons. This is the security boundary; it replicates the * inline github-script gate bit-for-bit. No I/O so it can be unit-tested. */ export declare function evaluateRecapGate(input: RecapGateInput): { run: boolean; agent: string; reasons: string[]; }; /** * Canonicalize the agent-written plan URL into a trusted recap URL, or "". * * recap-url.txt is produced by the (LLM) agent, so the raw URL is untrusted. * This rebuilds a canonical `${origin}${base}/recaps/` link from the TRUSTED * app URL plus a strictly-validated plan id, enforcing the app origin and * honoring a path-prefixed mount (e.g. https://host/agent-native). Returns "" * for a wrong origin or an unrecognized path. Pure so it can be unit-tested — * SAME impl as the workflow's previous inline `canonicalRecapUrl`. */ export declare function canonicalRecapUrl(rawUrl: string, appUrl: string): string; export declare function inferLocalRecapUrlFailureReason(input?: { cwd?: string; appUrl?: string; }): string; export declare function buildRecapFailureDiagnostic(input: { failureSummary?: string; urlReason?: string; }): string; /** The signals that decide the completed "Visual Recap" check's conclusion. */ export interface RecapCheckOutcomeInput { /** steps.url.outputs.ok — the agent published a plan whose origin validated. */ planOk: boolean; /** steps.url.outputs.plan_url — the (untrusted) agent-written plan URL. */ planUrl: string; /** PLAN_RECAP_APP_URL — the trusted plan app origin/base. */ appUrl: string; /** steps.diff.outputs.huge — the diff exceeded the byte cap (summarized). */ huge: boolean; /** steps.diff.outputs.tiny — the diff was too small to recap. */ tiny: boolean; /** steps.scan.outputs.suppressed — a secret pattern suppressed the recap. */ suppressed: boolean; /** steps.scan.outputs.json — the raw scan JSON (carries the suppress reason). */ suppressedJson: string; /** Sanitized final agent output when no valid plan URL was produced. */ failureSummary?: string; /** Explanation from the URL-reading step when recap-url.txt was absent/bad. */ urlReason?: string; /** The Actions run URL, used as the default details_url. */ workflowUrl: string; } /** The completed-check fields PATCHed to the GitHub check run. */ export interface RecapCheckOutcome { conclusion: "neutral" | "success" | "skipped"; title: string; summary: string; text: string; detailsUrl: string; } /** * Map the workflow's terminal recap state to the completed check's * conclusion/title/summary/text/details_url. Pure so it can be unit-tested — * reproduces the workflow's previous inline branch logic EXACTLY: * * - default → neutral "Visual recap not generated" * - planOk + valid recapUrl → success "Visual recap ready" (huge → "summarized" * summary), Open-recap link as text, details_url = recapUrl * - planOk + invalid url → neutral "Visual recap published" (see the comment) * - else tiny → skipped "Visual recap skipped" * - else suppressed → skipped "Visual recap suppressed" (reason from scan JSON) */ export declare function recapCheckOutcome(input: RecapCheckOutcomeInput): RecapCheckOutcome; interface ParsedUsage { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; model?: string; reportedCostUsd?: number; } /** * Claude Code `-p --output-format json` prints one final result object with a * `usage` block and `total_cost_usd`. Anthropic's `input_tokens` EXCLUDES cache * tokens, so the cache counts are added back: `inputTokens` reports the whole * prompt, with the cache counts a slice of it. That is the one convention * `calculateCost` and the engine `usage` event share. */ export declare function parseClaudeUsage(stdout: string): ParsedUsage | null; /** * Codex `exec --json` reports `input_tokens` INCLUSIVE of `cached_input_tokens` * (OpenAI counts cached as a subset of prompt tokens), which is already the * convention `calculateCost` and the engine `usage` event share — so input * passes through untouched and `calculateCost` subtracts to price each token * once. This used to strip the cached tokens out here instead, back when * pricing added the cache counts on top; doing both now bills them twice. * * `reasoning_output_tokens` is still folded into output — it is billed at the * output rate and would otherwise be dropped. */ export declare function parseCodexUsage(jsonl: string): ParsedUsage | null; /** Parse the usage sidecar emitted by an Agent-Native Code run. */ export declare function parseOpenAiCompatibleUsage(json: string): ParsedUsage | null; export declare function runRecap(argv: string[]): Promise; export {}; //# sourceMappingURL=recap.d.ts.map