// review-targets.ts — REVIEW TARGETS (AUTOPILOT-57): where the human actually // looks to review approved work, built from what already exists — the branch // and its commits (what to diff), the file:line anchors the reviewer cited // as evidence, the doc paths the work created/changed, and an MR/PR link // where a remote exists — rather than the orchestrator hand-writing branches, // SHAs and doc paths into prose every handover. // // REUSES flag_for_review's review_targets SHAPE (`string[]`) and semantics — // the manual and automatic handovers describe work identically. This module // only supplies the AUTOMATIC handover's targets (runner.ts on a review // PASS); flag_for_review's own future is AUTOPILOT-27's call, not pre-empted // here. // // HONESTY: a target that does not resolve (a deleted branch, a gc'd commit) // is worse than none. `buildReviewTargets` verifies existence before // printing a diff pointer and returns [] when nothing resolves — callers // render the plain "nothing found" case via `noReviewTargetsFoundLine` // rather than substituting a fabricated placeholder. // // REFRESH, NOT ACCUMULATE: this is rebuilt from scratch on every call (the // caller persists the RETURNED array as the item's `reviewTargets` field — a // full overwrite each verdict PASS). A recorded SHA is superseded by a // re-dispatch: only the LATEST journaled branch is used, never the item's // whole re-dispatch history. // // RANKED + CAPPED: a fifty-anchor handover is as unusable as none — total // output is capped at MAX_REVIEW_TARGETS, filled in PRIORITY order (branch + // commits, then reviewer-cited anchors, then doc paths, then the MR/PR // link) so the highest-value pointers survive the cap. import { reviewPointersFor, deliverablePathsFor, commitResolvesIn } from "./worktree-preservation.ts"; import { mrCreateUrl } from "./shipping.ts"; import { reviewNotesCitedAnchors, type ReviewNotes } from "../review-notes.ts"; /** Total cap on a handover's review_targets — "a handful of ranked anchors, * not a wall" (AUTOPILOT-57). */ export const MAX_REVIEW_TARGETS = 8; /** * The review-target list for one item's CURRENT attempt. Rebuilt from * scratch every call — never appended across attempts. Empty when nothing * resolves (no repo, no journaled branch, no reviewer citations, no docs, no * remote): callers must say so plainly (`noReviewTargetsFoundLine`), never * substitute a fabricated pointer. */ export function buildReviewTargets( stateDir: string, key: string, item: { cwd?: string | null }, reviewNotes?: ReviewNotes | null, ): string[] { const targets: string[] = []; const repo = item.cwd ?? null; // 1) branch + commits — what to diff. Only the LATEST journaled branch: // an earlier attempt's branch is superseded by a re-dispatch, not a // second target for THIS attempt. let latest: { branch: string; tipSha: string } | undefined; if (repo) { try { const pointers = reviewPointersFor(stateDir, key); latest = pointers.at(-1); } catch { latest = undefined; } } if (repo && latest) { if (commitResolvesIn(repo, latest.tipSha)) { targets.push(`branch ${latest.branch} @ ${latest.tipSha.slice(0, 8)} — diff vs main: git diff main...${latest.tipSha.slice(0, 8)}`); } else { // HONESTY: the journaled commit is gone (deleted branch, gc'd ref) — // say so instead of printing a diff command against a dead sha. targets.push(`branch ${latest.branch} @ ${latest.tipSha.slice(0, 8)} — NO LONGER RESOLVABLE (deleted or garbage-collected); cannot diff`); } } // 2) file:line anchors the reviewer cited as evidence. targets.push(...reviewNotesCitedAnchors(reviewNotes)); // 3) doc paths created/changed by the work (branch-scoped; docs sort first // inside deliverablePathsFor) — only from a branch whose tip still // resolves (a gone commit has nothing to diff for file contents either). if (repo && latest && commitResolvesIn(repo, latest.tipSha)) { try { const files = deliverablePathsFor(repo, latest.tipSha); if (files?.length) { targets.push(...files.map((f) => `${f} — on branch (view: git show ${latest!.tipSha.slice(0, 8)}:${f})`)); } } catch { // best-effort — doc-path enrichment must never break the handover } } // 4) MR/PR link, where a remote exists — the shipping lane's own URL // builder (base defaults to "main", the same base the diff command // above uses; a best-effort navigation pointer, not the ship decision). if (repo && latest && commitResolvesIn(repo, latest.tipSha)) { try { const url = mrCreateUrl(repo, latest.branch, "main"); if (url) targets.push(url); } catch { // best-effort } } return targets.slice(0, MAX_REVIEW_TARGETS); } /** The plain, honest line for an EMPTY target list — never a fabricated * pointer. Shared by the automatic handover message and the decision panel * so both surfaces say the same thing when nothing resolved. */ export function noReviewTargetsFoundLine(item: { cwd?: string | null }, key: string): string { return `No review targets could be resolved automatically for ${key}${item.cwd ? ` — check ${item.cwd} directly` : ""}.`; }