import { waveGroups, detectFileTargetOverlaps } from "../core/engine.js";
import type { Blackboard } from "../core/blackboard.js";
import type { MorphState, PlanOutput, SparkOutput, ReviewOutput } from "../schemas/contracts.js";
export function escapeHtml(text: string): string {
return text.replace(/&/g, "&").replace(//g, ">").replace(/\"/g, """).replace(/'/g, "'");
}
export function renderHtmlList(items: string[], fallback: string): string {
const values = items.length ? items : [fallback];
return `
${values.map((item) => `${escapeHtml(item)} `).join("")} `;
}
function renderInlineMarkdown(value: string): string {
return escapeHtml(value)
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1 ")
.replace(/\*([^*]+)\*/g, "$1 ");
}
export function renderMarkdownLite(markdown: string, fallback = "No detail recorded."): string {
const normalized = markdown.trim();
if (!normalized || /^(nothing|none|n\/a)$/i.test(normalized)) {
return `${escapeHtml(fallback)}
`;
}
const blocks: string[] = [];
let paragraph: string[] = [];
let listItems: string[] = [];
let listKind: "ul" | "ol" | null = null;
const flushParagraph = () => {
if (paragraph.length > 0) {
blocks.push(`${renderInlineMarkdown(paragraph.join(" "))}
`);
paragraph = [];
}
};
const flushList = () => {
if (listItems.length > 0 && listKind) {
blocks.push(`<${listKind}>${listItems.map((item) => `${renderInlineMarkdown(item)} `).join("")}${listKind}>`);
listItems = [];
listKind = null;
}
};
for (const rawLine of normalized.replace(/\r\n/g, "\n").split("\n")) {
const line = rawLine.trim();
if (!line) {
flushParagraph();
flushList();
continue;
}
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
if (heading) {
flushParagraph();
flushList();
const level = Math.min(6, Math.max(3, heading[1].length + 1));
blocks.push(`${renderInlineMarkdown(heading[2])} `);
continue;
}
const unordered = /^[-*]\s+(.+)$/.exec(line);
if (unordered) {
flushParagraph();
if (listKind && listKind !== "ul") flushList();
listKind = "ul";
listItems.push(unordered[1]);
continue;
}
const ordered = /^\d+\.\s+(.+)$/.exec(line);
if (ordered) {
flushParagraph();
if (listKind && listKind !== "ol") flushList();
listKind = "ol";
listItems.push(ordered[1]);
continue;
}
flushList();
paragraph.push(line);
}
flushParagraph();
flushList();
return `${blocks.join("")}
`;
}
function summarizeMarkdown(markdown: string, fallback = "No detail recorded."): string {
for (const rawLine of markdown.replace(/\r\n/g, "\n").split("\n")) {
const line = rawLine.trim();
if (!line || /^(nothing|none|n\/a)$/i.test(line) || /^#{1,6}\s+/.test(line)) continue;
return line
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1");
}
return fallback;
}
export const REVIEW_SCORE_FLOOR = 8;
export type ReviewGateDisposition = "ship_candidate" | "auto_repair" | "escalate";
export interface ReviewReadinessAssessment {
disposition: ReviewGateDisposition;
reasons: string[];
actionableTaskIds: string[];
}
function hasMeaningfulReviewText(value: string | undefined): boolean {
if (!value) return false;
const normalized = value.trim().toLowerCase();
return Boolean(normalized) && !["nothing", "none", "n/a", "no user feedback provided"].includes(normalized);
}
export function isWorkReadyForReview(state: MorphState): boolean {
if (!state.planOutput) return false;
const doneTaskIds = new Set(state.workResults.filter((result) => result.status === "done").map((result) => result.taskId));
return state.planOutput.tasks.every((task) => doneTaskIds.has(task.id));
}
export function assessReviewReadiness(review: ReviewOutput): ReviewReadinessAssessment {
const reasons: string[] = [];
const blockingChanges = review.requiredChanges.filter((change) => change.severity === "critical" || change.severity === "major");
const actionableTaskIds = [...new Set(review.requiredChanges.map((change) => change.taskId).filter((id): id is string => Boolean(id)))];
if (review.status !== "APPROVED") reasons.push(`verdict is ${review.status}`);
if (review.efficiencyScore < REVIEW_SCORE_FLOOR) reasons.push(`score ${review.efficiencyScore}/10 is below ${REVIEW_SCORE_FLOOR}/10`);
if (blockingChanges.length > 0) reasons.push(`${blockingChanges.length} critical/major required change${blockingChanges.length === 1 ? "" : "s"} remain`);
if (review.securityIssues.length > 0) reasons.push(`${review.securityIssues.length} unresolved security issue${review.securityIssues.length === 1 ? "" : "s"} remain`);
if (!hasMeaningfulReviewText(review.technicalAudit)) reasons.push("technical audit is missing");
if (!hasMeaningfulReviewText(review.userPerspectiveFeedback)) reasons.push("user-perspective feedback is missing");
if (!hasMeaningfulReviewText(review.testCoverageAssessment)) reasons.push("coverage assessment is missing");
if (reasons.length === 0) return { disposition: "ship_candidate", reasons, actionableTaskIds };
if (actionableTaskIds.length > 0) return { disposition: "auto_repair", reasons, actionableTaskIds };
return { disposition: "escalate", reasons, actionableTaskIds };
}
export type BrowserGateTone = "neutral" | "success" | "warning" | "danger";
export interface BrowserGateMetric {
label: string;
value: string;
tone?: BrowserGateTone;
}
export interface BrowserGateFlag {
label: string;
detail: string;
tone?: BrowserGateTone;
}
export interface BrowserGateCard {
label: string;
title: string;
body: string;
}
export interface BrowserGateSection {
title: string;
summary: string;
html: string;
open?: boolean;
}
export interface BrowserGatePage {
phase: string;
eyebrow: string;
title: string;
subtitle: string;
recommendation: string;
decisionSummary: string;
metrics: BrowserGateMetric[];
flags: BrowserGateFlag[];
cards: BrowserGateCard[];
approveIf: string[];
pauseIf: string[];
sections: BrowserGateSection[];
primaryAction: string;
secondaryAction: string;
}
export function renderBrowserGatePage(page: BrowserGatePage): string {
const toneClass = (tone?: BrowserGateTone) => tone ?? "neutral";
const toneIcon = (tone?: BrowserGateTone) => {
switch (tone) {
case "success":
return ` `;
case "warning":
return ` `;
case "danger":
return ` `;
default:
return ` `;
}
};
const flags = page.flags.length
? page.flags.map((flag) => `
${escapeHtml(flag.label)}
${escapeHtml(flag.detail)}
`).join("")
: `No exceptions surfaced Morph did not surface a blocking concern for this gate. `;
const cards = page.cards.map((card) => `
${escapeHtml(card.label)}
${escapeHtml(card.title)}
${escapeHtml(card.body)}
`).join("");
const sections = page.sections.map((section) => `
${escapeHtml(section.title)}
${escapeHtml(section.summary)}
${section.html}
`).join("");
return `
${escapeHtml(page.title)}
${escapeHtml(page.eyebrow)}
${escapeHtml(page.title)}
${escapeHtml(page.subtitle)}
${page.metrics.map((metric) => `
${escapeHtml(metric.label)}
${toneIcon(metric.tone)}${escapeHtml(metric.value)}
`).join("")}
Recommended move
${escapeHtml(page.recommendation)}
${escapeHtml(page.decisionSummary)}
Approve if
${renderHtmlList(page.approveIf, "No explicit approve criteria captured.")}
Pause if
${renderHtmlList(page.pauseIf, "No explicit pause criteria captured.")}
${sections}
This gate is generated from live Morph state.
${escapeHtml(page.primaryAction)}
${escapeHtml(page.secondaryAction)}
`;
}
export function buildWorkSpecHtml(plan: PlanOutput, markdown: string, spark?: SparkOutput): string {
const waves = waveGroups(plan.tasks);
const fileOverlaps = detectFileTargetOverlaps(plan.tasks);
const highOverlaps = fileOverlaps.filter((overlap) => overlap.severity === "high");
const taskRows = plan.tasks.map((task) => `${escapeHtml(task.id)}${escapeHtml(task.description)} ${escapeHtml(task.category)} ${escapeHtml(task.estimatedComplexity)} ${escapeHtml(task.dependsOn.join(", ") || "none")} ${escapeHtml(task.acceptanceCriteria)} `).join("\n");
const waveCards = waves.map((wave, index) => `
Wave ${index + 1}
${wave.map((task) => `${escapeHtml(task.id)} ${escapeHtml(task.description)} `).join("")}
`).join("");
const overlapHtml = fileOverlaps.length
? `${fileOverlaps.map((overlap) => `${escapeHtml(overlap.severity.toUpperCase())} ${escapeHtml(overlap.file)} | ${escapeHtml(overlap.taskIds.join(", "))} (waves ${escapeHtml(overlap.waveNumbers.join(", "))}). ${escapeHtml(overlap.suggestion)} `).join("")} `
: "No concrete file-target overlaps detected.
";
const productShape = spark
? `${spark.productShape.deliverableType} | ${spark.productShape.runtime} | ${spark.productShape.distribution}`
: "Product shape not captured";
const topFlags: BrowserGateFlag[] = [
...highOverlaps.slice(0, 2).map((overlap) => ({
label: `HIGH overlap: ${overlap.taskIds.join(" + ")}`,
detail: `${overlap.file} | ${overlap.suggestion}`,
tone: "danger" as const,
})),
...fileOverlaps.filter((overlap) => overlap.severity !== "high").slice(0, Math.max(0, 3 - highOverlaps.length)).map((overlap) => ({
label: `Shared target: ${overlap.taskIds.join(" + ")}`,
detail: `${overlap.file} | ${overlap.suggestion}`,
tone: "warning" as const,
})),
...plan.riskMitigations.slice(0, Math.max(0, 3 - fileOverlaps.length)).map((risk) => ({
label: "Risk watch",
detail: risk,
tone: "warning" as const,
})),
].slice(0, 3);
return renderBrowserGatePage({
phase: "pre-work",
eyebrow: "morph approval gate | plan -> work",
title: "Approve implementation scope",
subtitle: "This is the final human checkpoint before Morph hands the plan to Engineer and Peer Reviewer agents.",
recommendation:
highOverlaps.length > 0
? "Review flagged overlaps before approving"
: fileOverlaps.length > 0
? "Approve after checking shared-file edits"
: "Approve if the product shape and task scope are right",
decisionSummary:
"Approving locks this edited spec as human guidance, then starts WORK automatically. Pausing keeps the plan intact without implementation.",
metrics: [
{ label: "Tasks", value: String(plan.tasks.length) },
{ label: "Waves", value: String(waves.length) },
{ label: "Effort", value: plan.estimatedEffort },
{ label: "Overlaps", value: String(fileOverlaps.length), tone: highOverlaps.length > 0 ? "danger" : fileOverlaps.length > 0 ? "warning" : "success" },
],
flags: topFlags,
cards: [
{
label: "Why",
title: "Product intent",
body: spark?.visionStatement ?? "No product vision captured.",
},
{
label: "Deliverable",
title: productShape,
body: spark?.productShape.explicitUserIntent ?? "Explicit user intent not captured.",
},
{
label: "Scope",
title: `${plan.tasks.length} tasks across ${waves.length} waves`,
body: `${plan.componentTree.length} components | ${plan.dataModels.length} data-model notes`,
},
{
label: "Risk",
title: highOverlaps.length > 0 ? `${highOverlaps.length} high-risk overlap${highOverlaps.length === 1 ? "" : "s"}` : fileOverlaps.length > 0 ? `${fileOverlaps.length} shared-file target${fileOverlaps.length === 1 ? "" : "s"}` : "No overlap pressure",
body: plan.riskMitigations[0] ?? "No explicit risk mitigation captured.",
},
],
approveIf: [
"The product intent and deliverable shape match what you want built.",
"The task count and execution waves feel like the right scope for this effort.",
"Flagged overlaps and risks are acceptable or intentionally serialized.",
],
pauseIf: [
"The product shape is wrong or the task graph is solving the wrong problem.",
"Acceptance criteria look vague, inflated, or incomplete.",
"A flagged overlap or risk needs human clarification before implementation starts.",
],
sections: [
{
title: "Scope and success criteria",
summary: "Intent, features, constraints, and what success means",
open: true,
html: spark
? `
Core features ${renderHtmlList(spark.coreFeatures, "No features captured.")}
Success criteria ${renderHtmlList(spark.successCriteria, "No explicit success criteria captured.")}
Constraints ${renderHtmlList(spark.constraints, "No hard constraints captured.")}
Target user ${escapeHtml(spark.targetUserPersona)}
`
: "No Spark context captured.
",
},
{
title: "Execution waves",
summary: `${waves.length} waves define implementation order and parallelism`,
open: highOverlaps.length > 0,
html: `${waveCards}
`,
},
{
title: "File-target overlaps",
summary: fileOverlaps.length ? `${fileOverlaps.length} shared targets to inspect` : "No shared targets detected",
open: fileOverlaps.length > 0,
html: overlapHtml,
},
{
title: "Task DAG",
summary: "All implementation tasks with dependencies and acceptance criteria",
html: `ID Description Category Complexity Dependencies Acceptance ${taskRows}
`,
},
{
title: "Architecture and QA",
summary: "Technical shape, testing plan, and risk mitigations",
html: `
Architecture ${escapeHtml(plan.architectureDiagram)}
QA strategy ${escapeHtml(plan.qaStrategy)}
Risk mitigations ${renderHtmlList(plan.riskMitigations, "No explicit mitigations captured.")}
`,
},
{
title: "Approved spec snapshot",
summary: "Exact markdown guidance handed to implementation agents",
html: `${escapeHtml(markdown)} `,
},
],
primaryAction: "Approve & start WORK",
secondaryAction: "Pause pipeline",
});
}
export function buildSparkApprovalHtml(spark: SparkOutput): string {
const topFlags: BrowserGateFlag[] = spark.risks.slice(0, 3).map((risk) => ({
label: "Risk to inspect",
detail: risk,
tone: "warning",
}));
return renderBrowserGatePage({
phase: "spark",
eyebrow: "morph approval gate · spark → plan",
title: "Approve the product direction",
subtitle: "Before Morph spends effort designing architecture and tasks, confirm that it understood the right product.",
recommendation: "Approve if this is the right thing to build",
decisionSummary:
"Approving advances to PLAN, where Morph turns this product brief into architecture, QA strategy, and an implementation DAG.",
metrics: [
{ label: "Features", value: String(spark.coreFeatures.length) },
{ label: "Risks", value: String(spark.risks.length), tone: spark.risks.length > 0 ? "warning" : "success" },
{ label: "Constraints", value: String(spark.constraints.length) },
{ label: "Criteria", value: String(spark.successCriteria.length) },
],
flags: topFlags,
cards: [
{
label: "Why",
title: "Vision",
body: spark.visionStatement,
},
{
label: "User",
title: "Target persona",
body: spark.targetUserPersona,
},
{
label: "Shape",
title: `${spark.productShape.deliverableType} · ${spark.productShape.runtime}`,
body: spark.productShape.explicitUserIntent,
},
{
label: "Stack",
title: "Recommended direction",
body: spark.technicalStackRecommendation,
},
],
approveIf: [
"Morph captured the real user intent, not just surface wording.",
"The target user and product shape are correct enough to plan against.",
"The listed risks are acceptable inputs to planning rather than signs of a wrong direction.",
],
pauseIf: [
"The product is for the wrong audience or solves the wrong problem.",
"A hard constraint or must-have outcome is missing.",
"You want to reshape the brief before any implementation architecture is created.",
],
sections: [
{
title: "Product brief",
summary: "Intent, target user, runtime, and delivery shape",
open: true,
html: `
Explicit intent ${escapeHtml(spark.productShape.explicitUserIntent)}
Product shape
Deliverable: ${escapeHtml(spark.productShape.deliverableType)}
Runtime: ${escapeHtml(spark.productShape.runtime)}
Distribution: ${escapeHtml(spark.productShape.distribution)}
`,
},
{
title: "Features and success criteria",
summary: "What Morph thinks the product must do and how success is judged",
html: `
Core features ${renderHtmlList(spark.coreFeatures, "No features captured.")}
Success criteria ${renderHtmlList(spark.successCriteria, "No success criteria captured.")}
`,
},
{
title: "Constraints and risks",
summary: "Inputs that should shape planning decisions",
open: spark.risks.length > 0,
html: `
Constraints ${renderHtmlList(spark.constraints, "No hard constraints captured.")}
Risks ${renderHtmlList(spark.risks, "No risks captured.")}
`,
},
],
primaryAction: "Approve & start PLAN",
secondaryAction: "Pause pipeline",
});
}
export function buildReviewApprovalHtml(state: ReturnType): string {
const review = state.reviewOutput!;
const telemetry = state.reviewTelemetry;
const severities: Record = { critical: 0, major: 0, minor: 0, "nice-to-have": 0 };
for (const change of review.requiredChanges) severities[change.severity]++;
const completedTasks = state.workResults.filter((result) => result.status === "done");
const changedFiles = [...new Set(state.workResults.flatMap((result) => result.filesChanged))];
const specialistEvidence = telemetry
? `
QA
Signals: ${escapeHtml(String(telemetry.qa.signalsFound ?? 0))}
${escapeHtml(telemetry.qa.notableGap ?? "No notable QA gap recorded.")}
Performance
Signals: ${escapeHtml(String(telemetry.perf.signalsFound ?? 0))}
${escapeHtml(telemetry.perf.notableConcern ?? "No notable performance concern recorded.")}
User
Signals: ${escapeHtml(String(telemetry.user.signalsFound ?? 0))}
${escapeHtml(telemetry.user.notableConcern ?? "No notable user concern recorded.")}
Synthesis
Verdict: ${escapeHtml(telemetry.synthesis.verdict ?? "not recorded")}
Coverage: ${escapeHtml(telemetry.synthesis.coverageAssessment ?? "No coverage assessment recorded.")}
`
: "No specialist telemetry was recorded for this review run.
";
const topFlags: BrowserGateFlag[] = [
...review.securityIssues.slice(0, 2).map((issue) => ({
label: "Security issue",
detail: issue,
tone: "danger" as const,
})),
...review.requiredChanges.slice(0, Math.max(0, 3 - review.securityIssues.length)).map((change) => ({
label: `${change.severity.toUpperCase()} follow-up${change.taskId ? ` · ${change.taskId}` : ""}`,
detail: change.description,
tone: change.severity === "critical" || change.severity === "major" ? "danger" as const : "warning" as const,
})),
].slice(0, 3);
return renderBrowserGatePage({
phase: "review",
eyebrow: "morph approval gate · review → ship",
title: "Approve release readiness",
subtitle: "Review has finished. Confirm that the delivered work is ready to enter SHIP and become a release candidate.",
recommendation:
review.requiredChanges.some((change) => change.severity === "critical" || change.severity === "major") || review.securityIssues.length > 0
? "Pause unless you intentionally accept the flagged findings"
: "Approve if the review verdict matches your release bar",
decisionSummary:
"Approving starts SHIP, where DevOps and Release Consultant agents prepare the release package, checklist, changelog, and final handoff.",
metrics: [
{ label: "Verdict", value: review.status, tone: review.status === "APPROVED" ? "success" : "warning" },
{ label: "Score", value: `${review.efficiencyScore}/10` },
{ label: "Required", value: String(review.requiredChanges.length), tone: review.requiredChanges.length > 0 ? "warning" : "success" },
{ label: "Security", value: String(review.securityIssues.length), tone: review.securityIssues.length > 0 ? "danger" : "success" },
],
flags: topFlags,
cards: [
{
label: "Validation",
title: review.testCoverageAssessment ? "Coverage assessed" : "Coverage note absent",
body: review.testCoverageAssessment ?? "No test coverage assessment was captured.",
},
{
label: "Delivered",
title: `${completedTasks.length} completed tasks`,
body: `${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"} recorded in WORK.`,
},
{
label: "User view",
title: "End-user perspective",
body: summarizeMarkdown(review.userPerspectiveFeedback, "No end-user feedback recorded."),
},
{
label: "Routing",
title: telemetry
? `QA ${telemetry.routing.qa ? "used" : "skipped"} · Perf ${telemetry.routing.perf ? "used" : "skipped"} · User ${telemetry.routing.user ? "used" : "skipped"}`
: "Routing unavailable",
body: telemetry?.nextStep ?? "Review telemetry was not recorded.",
},
],
approveIf: [
"The verdict and score satisfy your release bar.",
"There are no unresolved major/critical changes you expect Morph to fix first.",
"Security findings and coverage notes are acceptable for the release you are about to prepare.",
],
pauseIf: [
"A required change should return to WORK before any release prep begins.",
"Security findings need triage, mitigation, or explicit acceptance.",
"The user-perspective feedback reveals a product issue you do not want to ship.",
],
sections: [
{
title: "Review summary",
summary: `${severities.critical} critical · ${severities.major} major · ${severities.minor} minor`,
open: true,
html: `
Technical audit ${renderMarkdownLite(review.technicalAudit, "No technical audit recorded.")}
User perspective ${renderMarkdownLite(review.userPerspectiveFeedback, "No end-user feedback recorded.")}
`,
},
{
title: "Required changes and security",
summary: `${review.requiredChanges.length} required changes · ${review.securityIssues.length} security issues`,
open: review.requiredChanges.length > 0 || review.securityIssues.length > 0,
html: `
Required changes ${renderHtmlList(review.requiredChanges.map((change) => `${change.severity}${change.taskId ? ` · ${change.taskId}` : ""}: ${change.description}`), "No required changes recorded.")}
Security issues ${renderHtmlList(review.securityIssues, "No security issues recorded.")}
`,
},
{
title: "Specialist evidence",
summary: telemetry
? `QA ${telemetry.routing.qa ? "used" : "skipped"} · Perf ${telemetry.routing.perf ? "used" : "skipped"} · User ${telemetry.routing.user ? "used" : "skipped"}`
: "No specialist telemetry recorded",
html: specialistEvidence,
},
{
title: "Delivered work",
summary: `${completedTasks.length} completed tasks · ${changedFiles.length} changed files`,
html: `
Completed tasks ${renderHtmlList(completedTasks.map((result) => `${result.taskId} — ${result.summary}`), "No completed work recorded.")}
Changed files ${renderHtmlList(changedFiles, "No changed files recorded.")}
`,
},
{
title: "What approving starts",
summary: "Release preparation, not deployment",
html: `SHIP will ask DevOps and Release Consultant agents to prepare a release package, changelog, deployment checklist, rollback plan, and final handoff artifacts. Approval here does not hide review findings; it means you are comfortable moving into release preparation.
`,
},
],
primaryAction: "Approve & start SHIP",
secondaryAction: "Pause pipeline",
});
}
export function buildReviewExceptionHtml(
state: ReturnType,
readiness: ReviewReadinessAssessment
): string {
const review = state.reviewOutput!;
const telemetry = state.reviewTelemetry;
const completedTasks = state.workResults.filter((result) => result.status === "done");
const changedFiles = [...new Set(state.workResults.flatMap((result) => result.filesChanged))];
const reasonFlags: BrowserGateFlag[] = readiness.reasons.map((reason) => ({
label: "Ship-readiness floor missed",
detail: reason,
tone: reason.includes("security") || reason.includes("critical/major") ? "danger" : "warning",
}));
return renderBrowserGatePage({
phase: "review-exception",
eyebrow: "morph exception gate | review -> ship",
title: "Resolve release exception",
subtitle:
"Morph could not satisfy the normal ship-readiness floor or map the issue to a safe targeted repair. Continuing now is an explicit human override.",
recommendation: "Pause unless you consciously accept every exception",
decisionSummary:
"Accepting this exception bypasses the normal release-quality floor and starts SHIP anyway. Pausing keeps the review intact for human intervention or a later rerun.",
metrics: [
{ label: "Verdict", value: review.status, tone: review.status === "APPROVED" ? "warning" : "danger" },
{ label: "Score", value: `${review.efficiencyScore}/10`, tone: review.efficiencyScore >= REVIEW_SCORE_FLOOR ? "neutral" : "warning" },
{ label: "Exceptions", value: String(readiness.reasons.length), tone: readiness.reasons.length > 0 ? "danger" : "neutral" },
{ label: "Security", value: String(review.securityIssues.length), tone: review.securityIssues.length > 0 ? "danger" : "success" },
],
flags: reasonFlags,
cards: [
{
label: "Why now",
title: "Normal gate withheld",
body: "This page appears only when Morph will not show the ordinary release approval gate.",
},
{
label: "Automation",
title: "No safe targeted repair",
body: "The review is below bar, but Morph could not identify a precise task branch to repair automatically.",
},
{
label: "Delivered",
title: `${completedTasks.length} completed tasks`,
body: `${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"} recorded in WORK.`,
},
{
label: "Next move",
title: telemetry?.nextStep ?? "Human judgment required",
body: "Only accept when outside context makes the remaining exception worth shipping.",
},
],
approveIf: [
"You understand each missed quality condition and are choosing to own it.",
"You have external context Morph does not have that justifies shipping anyway.",
"The cost of delaying release is higher than the known residual risk.",
],
pauseIf: [
"Any exception is surprising, unclear, or not explicitly accepted.",
"You want a human or a future Morph run to repair the issue before release prep.",
"The gate is missing evidence you would need to defend the override later.",
],
sections: [
{
title: "Why this is an exception",
summary: `${readiness.reasons.length} ship-readiness condition${readiness.reasons.length === 1 ? "" : "s"} missed`,
open: true,
html: renderHtmlList(readiness.reasons, "No exception reasons recorded."),
},
{
title: "Review evidence",
summary: "The evidence behind the blocked happy path",
open: true,
html: `
Technical audit ${renderMarkdownLite(review.technicalAudit, "No technical audit recorded.")}
User perspective ${renderMarkdownLite(review.userPerspectiveFeedback, "No end-user feedback recorded.")}
`,
},
{
title: "Coverage and security",
summary: `${review.securityIssues.length} security issues | coverage ${review.testCoverageAssessment ? "recorded" : "missing"}`,
open: review.securityIssues.length > 0 || !review.testCoverageAssessment,
html: `
Coverage ${renderMarkdownLite(review.testCoverageAssessment ?? "", "No coverage assessment recorded.")}
Security issues ${renderHtmlList(review.securityIssues, "No security issues recorded.")}
`,
},
{
title: "What accepting does",
summary: "Explicit override of the ordinary release-quality floor",
html: `Acceptance does not improve the review result. It records that a human knowingly chose to continue despite the unresolved exception, then moves Morph into SHIP for release preparation.
`,
},
],
primaryAction: "Accept exception & start SHIP",
secondaryAction: "Pause for intervention",
});
}