import { isPlanningRequest } from "./continuity";
import type {
CardFinding,
PlanPhaseFramingMode,
PlanPhaseFramingState,
PlanProjectionMode,
PlanProjectionState,
RepositoryIdentity,
RuntimeCard,
} from "./types";
export interface FormatContextCardOptions {
planProjectionMode?: PlanProjectionMode;
planPhaseFramingMode?: PlanPhaseFramingMode;
}
function verifiedImplementation(card: RuntimeCard): boolean {
const changes = [
...(card.resumed?.execution.changes ?? []),
...card.execution.changes,
];
return (
changes.some((record) => record.kind === "change") &&
changes.some((record) => record.kind === "validation")
);
}
function needsFullPlan(request: string | undefined): boolean {
if (!request) return true;
return /\b(?:apply|build|continue|correct|debug|execute|fix|follow|implement|proceed|resume|review|test|validate)\b/i.test(
request,
);
}
export function planProjectionState(
card: RuntimeCard,
options: FormatContextCardOptions = {},
): PlanProjectionState {
if (!card.plan) return "none";
return options.planProjectionMode === "phase-aware" &&
verifiedImplementation(card) &&
!needsFullPlan(card.latestRequest)
? "retired"
: "full";
}
export function planPhaseFramingState(
card: RuntimeCard,
options: FormatContextCardOptions = {},
): PlanPhaseFramingState {
if (!card.plan) return "none";
if (options.planPhaseFramingMode !== "scope-note") return "disabled";
return card.latestRequest && !isPlanningRequest(card.latestRequest)
? "post-planning"
: "planning";
}
function formatChange(record: { action: string; count: number }): string {
return record.count > 1 ? `${record.action} ×${record.count}` : record.action;
}
function formatFailure(record: { action: string; detail?: string }): string {
return record.detail ? `${record.action} — ${record.detail}` : record.action;
}
function formatFinding(finding: CardFinding): string {
return `${finding.topic}: ${finding.detail}`;
}
function formatProject(card: RuntimeCard): string | undefined {
const description = card.capabilities.description?.trim();
if (description) return description;
if (card.capabilities.packageName && card.capabilities.projectType)
return `${card.capabilities.packageName} (${card.capabilities.projectType})`;
if (card.capabilities.packageName) return card.capabilities.packageName;
if (card.capabilities.projectType) return card.capabilities.projectType;
return undefined;
}
function formatRepo(repo: RepositoryIdentity): string {
const head = repo.head ? repo.head.slice(0, 8) : undefined;
return head ? `${repo.root} @ ${head}` : repo.root;
}
function projectCapabilitiesSection(card: RuntimeCard): string[] {
const rows: string[] = [];
const type = card.capabilities.projectType
? `Type: ${card.capabilities.projectType}`
: "";
const pkg = card.capabilities.packageName
? `Package: ${card.capabilities.packageName}${
card.capabilities.packageManager
? ` (${card.capabilities.packageManager})`
: ""
}`
: "";
const docs = card.capabilities.documentation.length
? `Documentation: ${card.capabilities.documentation.join(", ")}`
: "";
const validate = card.capabilities.validation.length
? `Validation: ${card.capabilities.validation.join(", ")}`
: "";
for (const row of [type, pkg, docs, validate]) if (row) rows.push(row);
return rows;
}
export function formatContextCard(
card: RuntimeCard,
options: FormatContextCardOptions = {},
): string {
const lines = [""];
if (card.goal) lines.push(`goal: ${card.goal}`);
if (card.taskId) lines.push(`TASK ID: ${card.taskId}`);
if (card.latestRequest && card.latestRequest !== card.goal)
lines.push(`LATEST REQUEST: ${card.latestRequest}`);
const project = formatProject(card);
if (project) lines.push(`project: ${project}`);
if (card.repo) lines.push(`repo: ${formatRepo(card.repo)}`);
if (card.plan) {
if (planProjectionState(card, options) === "retired")
lines.push(
`PLAN STATE (revision ${card.plan.revision}): verified change and validation recorded; full plan body retired from this phase.`,
);
else {
lines.push(`PINNED PLAN (revision ${card.plan.revision}):`);
const framing = planPhaseFramingState(card, options);
if (framing === "post-planning") {
if (card.plan.scopeNotes) {
lines.push(` PROCESS NOTES RETIRED AT IMPLEMENTATION START`);
} else {
// Fallback to original scope-note framing if no specific notes were captured
lines.push(
" PHASE NOTE: This plan was written during planning. The current request is post-planning; constraints scoped only to planning (for example, not modifying files while planning) no longer apply. The plan body is preserved verbatim below.",
);
}
} else if (framing === "planning" && card.plan.scopeNotes) {
lines.push(` PROCESS NOTES:`);
for (const line of card.plan.scopeNotes.split(/\r?\n/))
lines.push(` ${line}`);
}
for (const line of card.plan.content.split(/\r?\n/))
lines.push(` ${line}`);
}
}
const capabilityRows = projectCapabilitiesSection(card);
if (capabilityRows.length) {
lines.push(`PROJECT CAPABILITIES:`);
for (const row of capabilityRows) lines.push(`- ${row}`);
}
lines.push("");
return lines.join("\n");
}
// Everything here changes on nearly every tool round: execution changes,
// pending items, findings, file-read leases, failures, resumed facts. The
// header above (goal, plan, capabilities) stays essentially fixed for the
// whole task, so callers should place this as a separate trailing message
// after the projected conversation rather than folding it into the leading
// card - that keeps the large stable history a valid, ever-growing
// cacheable provider prefix, and only this small block needs reprocessing
// when it changes.
export function formatCardStatus(card: RuntimeCard): string {
const lines: string[] = [];
if (card.execution.changes.length) {
lines.push(
`what happened: ${card.execution.changes.map(formatChange).join("; ")}`,
);
}
if (card.pending && card.pending.length) {
lines.push(`what's pending: ${card.pending.join("; ")}`);
}
if (card.findings && card.findings.length) {
lines.push(`findings: ${card.findings.map(formatFinding).join("; ")}`);
}
// Active entries duplicate content already visible in the projected
// transcript itself; only non-active entries (e.g. consumed) tell the
// model something it can no longer see directly.
const nonActiveFilesRead = card.filesRead?.filter(
(entry) => entry.state !== "active",
);
if (nonActiveFilesRead && nonActiveFilesRead.length) {
lines.push(
`files read: ${nonActiveFilesRead
.map((entry) => `${entry.path} (${entry.state})`)
.join(", ")}`,
);
}
if (card.execution.failures.length) {
lines.push(
`failures: ${card.execution.failures.map(formatFailure).join("; ")}`,
);
}
if (card.resumed) {
if (card.resumed.repositoryChanged)
lines.push(
"REPOSITORY STATE CHANGED SINCE THE PRIOR SESSION; prior validations are historical.",
);
const priorFailures = card.resumed.execution.failures
.map(formatFailure)
.filter(Boolean);
if (priorFailures.length) {
lines.push(`PRIOR SESSION UNRESOLVED FAILURES:`);
for (const failure of priorFailures) lines.push(`- ${failure}`);
}
const priorChanges = card.resumed.execution.changes
.map(formatChange)
.filter(Boolean);
if (priorChanges.length) {
lines.push(`PRIOR SESSION VERIFIED FACTS:`);
for (const change of priorChanges) lines.push(`- ${change}`);
}
}
if (lines.length === 0) return "";
return ["", ...lines, ""].join(
"\n",
);
}