refactor: Extract workflow-helpers module from auto-prompts (D3)
- Extract buildResumeSection and buildCarryForwardSection for continue/carry-forward logic - Extract checkNeedsReassessment and checkNeedsRunUat for adaptive replanning - Consolidates workflow state checking and section building - No behavior change; backward compatible via re-export pattern - Reduces auto-prompts.js by ~260 LOC Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e99d50fbc1
commit
6e0273573c
2 changed files with 219 additions and 262 deletions
|
|
@ -20,6 +20,12 @@ import {
|
|||
getPriorTaskSummaryPaths,
|
||||
isSummaryCleanForSkip,
|
||||
} from "./summary-helpers.js";
|
||||
import {
|
||||
buildCarryForwardSection,
|
||||
buildResumeSection,
|
||||
checkNeedsReassessment,
|
||||
checkNeedsRunUat,
|
||||
} from "./workflow-helpers.js";
|
||||
import {
|
||||
computeBudgets,
|
||||
resolveExecutorContextWindow,
|
||||
|
|
@ -922,268 +928,9 @@ export function escapeRegExp(value) {
|
|||
function oneLine(text) {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
// ─── Section Builders ──────────────────────────────────────────────────────
|
||||
export function buildResumeSection(
|
||||
continueContent,
|
||||
legacyContinueContent,
|
||||
continueRelPath,
|
||||
legacyContinueRelPath,
|
||||
) {
|
||||
const resolvedContent = continueContent ?? legacyContinueContent;
|
||||
const resolvedRelPath = continueContent
|
||||
? continueRelPath
|
||||
: legacyContinueRelPath;
|
||||
if (!resolvedContent || !resolvedRelPath) {
|
||||
return [
|
||||
"## Resume State",
|
||||
"- No continue file present. Start from the top of the task plan.",
|
||||
].join("\n");
|
||||
}
|
||||
const cont = parseContinue(resolvedContent);
|
||||
const lines = [
|
||||
"## Resume State",
|
||||
`Source: \`${resolvedRelPath}\``,
|
||||
`- Status: ${cont.frontmatter.status || "in_progress"}`,
|
||||
];
|
||||
if (cont.frontmatter.step && cont.frontmatter.totalSteps) {
|
||||
lines.push(
|
||||
`- Progress: step ${cont.frontmatter.step} of ${cont.frontmatter.totalSteps}`,
|
||||
);
|
||||
}
|
||||
if (cont.completedWork)
|
||||
lines.push(`- Completed: ${oneLine(cont.completedWork)}`);
|
||||
if (cont.remainingWork)
|
||||
lines.push(`- Remaining: ${oneLine(cont.remainingWork)}`);
|
||||
if (cont.decisions) lines.push(`- Decisions: ${oneLine(cont.decisions)}`);
|
||||
if (cont.nextAction) lines.push(`- Next action: ${oneLine(cont.nextAction)}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
export async function buildCarryForwardSection(priorSummaryPaths, base) {
|
||||
if (priorSummaryPaths.length === 0) {
|
||||
return [
|
||||
"## Carry-Forward Context",
|
||||
"- No prior task summaries in this slice.",
|
||||
].join("\n");
|
||||
}
|
||||
const items = await Promise.all(
|
||||
priorSummaryPaths.map(async (relPath) => {
|
||||
const absPath = join(base, relPath);
|
||||
const content = await loadFile(absPath);
|
||||
if (!content) return `- \`${relPath}\``;
|
||||
const summary = parseSummary(content);
|
||||
const provided = summary.frontmatter.provides.slice(0, 2).join("; ");
|
||||
const decisions = summary.frontmatter.key_decisions
|
||||
.slice(0, 2)
|
||||
.join("; ");
|
||||
const patterns = summary.frontmatter.patterns_established
|
||||
.slice(0, 2)
|
||||
.join("; ");
|
||||
const keyFiles = summary.frontmatter.key_files.slice(0, 3).join("; ");
|
||||
const diagnostics = extractMarkdownSection(content, "Diagnostics");
|
||||
const parts = [summary.title || relPath];
|
||||
if (summary.oneLiner) parts.push(summary.oneLiner);
|
||||
if (provided) parts.push(`provides: ${provided}`);
|
||||
if (decisions) parts.push(`decisions: ${decisions}`);
|
||||
if (patterns) parts.push(`patterns: ${patterns}`);
|
||||
if (keyFiles) parts.push(`key_files: ${keyFiles}`);
|
||||
if (diagnostics) parts.push(`diagnostics: ${oneLine(diagnostics)}`);
|
||||
return `- \`${relPath}\` — ${parts.join(" | ")}`;
|
||||
}),
|
||||
);
|
||||
return ["## Carry-Forward Context", ...items].join("\n");
|
||||
}
|
||||
export function extractSliceExecutionExcerpt(content, relPath) {
|
||||
if (!content) {
|
||||
return [
|
||||
"## Slice Plan Excerpt",
|
||||
`Slice plan not found at dispatch time. Read \`${relPath}\` before running slice-level verification.`,
|
||||
].join("\n");
|
||||
}
|
||||
const lines = content.split("\n");
|
||||
const goalLine = lines.find((l) => l.startsWith("**Goal:**"))?.trim();
|
||||
const demoLine = lines.find((l) => l.startsWith("**Demo:**"))?.trim();
|
||||
const verification = extractMarkdownSection(content, "Verification");
|
||||
const observability = extractMarkdownSection(
|
||||
content,
|
||||
"Observability / Diagnostics",
|
||||
);
|
||||
const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``];
|
||||
if (goalLine) parts.push(goalLine);
|
||||
if (demoLine) parts.push(demoLine);
|
||||
if (verification) {
|
||||
parts.push("", "### Slice Verification", verification.trim());
|
||||
}
|
||||
if (observability) {
|
||||
parts.push(
|
||||
"",
|
||||
"### Slice Observability / Diagnostics",
|
||||
observability.trim(),
|
||||
);
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
// Re-exported from summary-helpers.js:
|
||||
// - getPriorTaskSummaryPaths, getDependencyTaskSummaryPaths
|
||||
// ─── Adaptive Replanning Checks ────────────────────────────────────────────
|
||||
/**
|
||||
* Check if the most recently completed slice needs reassessment.
|
||||
* Returns { sliceId } if reassessment is needed, null otherwise.
|
||||
*
|
||||
* Skips reassessment when:
|
||||
* - No roadmap exists yet
|
||||
* - No slices are completed
|
||||
* - The last completed slice already has an assessment file
|
||||
* - All slices are complete (milestone done — no point reassessing)
|
||||
*/
|
||||
export async function checkNeedsReassessment(base, mid, _state, prefs) {
|
||||
// DB primary path — fall through to file-based when DB has no data for this milestone
|
||||
try {
|
||||
const { isDbAvailable, getMilestoneSlices } = await import("./sf-db.js");
|
||||
if (isDbAvailable()) {
|
||||
const slices = getMilestoneSlices(mid);
|
||||
if (slices.length > 0) {
|
||||
const completedSliceIds = slices
|
||||
.filter((s) => s.status === "complete")
|
||||
.map((s) => s.id);
|
||||
const hasIncomplete = slices.some((s) => s.status !== "complete");
|
||||
if (completedSliceIds.length === 0 || !hasIncomplete) return null;
|
||||
const lastCompleted = completedSliceIds[completedSliceIds.length - 1];
|
||||
const assessmentFile = resolveSliceFile(
|
||||
base,
|
||||
mid,
|
||||
lastCompleted,
|
||||
"ASSESSMENT",
|
||||
);
|
||||
const hasAssessment = !!(
|
||||
assessmentFile && (await loadFile(assessmentFile))
|
||||
);
|
||||
if (hasAssessment) return null;
|
||||
const summaryFile = resolveSliceFile(
|
||||
base,
|
||||
mid,
|
||||
lastCompleted,
|
||||
"SUMMARY",
|
||||
);
|
||||
const summaryContent = summaryFile ? await loadFile(summaryFile) : null;
|
||||
if (!summaryContent) return null;
|
||||
if (prefs?.skip_clean_reassess && isSummaryCleanForSkip(summaryContent))
|
||||
return null;
|
||||
return { sliceId: lastCompleted };
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logWarning(
|
||||
"prompt",
|
||||
`checkNeedsReassessment DB lookup failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
// Unmigrated/recovery fallback using rendered roadmap checkboxes. The DB path
|
||||
// above remains authoritative when slice rows exist.
|
||||
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
||||
if (!roadmapPath) return null;
|
||||
const roadmapContent = await loadFile(roadmapPath);
|
||||
if (!roadmapContent) return null;
|
||||
const parsed = parseRoadmap(roadmapContent);
|
||||
const fileCompletedIds = parsed.slices.filter((s) => s.done).map((s) => s.id);
|
||||
const fileHasIncomplete = parsed.slices.some((s) => !s.done);
|
||||
if (fileCompletedIds.length === 0 || !fileHasIncomplete) return null;
|
||||
const lastDone = fileCompletedIds[fileCompletedIds.length - 1];
|
||||
const assessFile = resolveSliceFile(base, mid, lastDone, "ASSESSMENT");
|
||||
const hasAssess = !!(assessFile && (await loadFile(assessFile)));
|
||||
if (hasAssess) return null;
|
||||
const summFile = resolveSliceFile(base, mid, lastDone, "SUMMARY");
|
||||
const summContent = summFile ? await loadFile(summFile) : null;
|
||||
if (!summContent) return null;
|
||||
if (prefs?.skip_clean_reassess && isSummaryCleanForSkip(summContent))
|
||||
return null;
|
||||
return { sliceId: lastDone };
|
||||
}
|
||||
/**
|
||||
* Return true when a slice SUMMARY signals a structurally clean completion
|
||||
* that makes reassess-roadmap dispatch unnecessary. Gated behind the
|
||||
* `skip_clean_reassess` preference (#4778).
|
||||
*/
|
||||
// Re-exported from summary-helpers.js:
|
||||
// - isSummaryCleanForSkip
|
||||
/**
|
||||
* Check if the most recently completed slice needs a UAT run.
|
||||
* Returns { sliceId, uatType } if UAT should be dispatched, null otherwise.
|
||||
*
|
||||
* Skips when:
|
||||
* - No roadmap or no completed slices
|
||||
* - All slices are done (milestone complete path — reassessment handles it)
|
||||
* - uat_dispatch preference is not enabled
|
||||
* - No UAT file exists for the slice
|
||||
* - UAT result file already exists (idempotent — already ran)
|
||||
*/
|
||||
export async function checkNeedsRunUat(base, mid, _state, prefs) {
|
||||
// DB primary path — fall through to file-based when DB has no data for this milestone
|
||||
try {
|
||||
const { isDbAvailable, getMilestoneSlices } = await import("./sf-db.js");
|
||||
if (isDbAvailable()) {
|
||||
const slices = getMilestoneSlices(mid);
|
||||
if (slices.length > 0) {
|
||||
const completedSlices = slices.filter((s) => s.status === "complete");
|
||||
const incompleteSlices = slices.filter((s) => s.status !== "complete");
|
||||
if (completedSlices.length === 0) return null;
|
||||
if (incompleteSlices.length === 0) return null;
|
||||
if (!prefs?.uat_dispatch) return null;
|
||||
const lastCompleted = completedSlices[completedSlices.length - 1];
|
||||
const sid = lastCompleted.id;
|
||||
const uatFile = resolveSliceFile(base, mid, sid, "UAT");
|
||||
if (!uatFile) return null;
|
||||
const uatContent = await loadFile(uatFile);
|
||||
if (!uatContent) return null;
|
||||
// If the UAT file already contains a verdict, UAT has been run — skip
|
||||
if (hasVerdict(uatContent)) return null;
|
||||
// Also check the ASSESSMENT file — the run-uat prompt writes the verdict
|
||||
// there (via sf_summary_save artifact_type:"ASSESSMENT"), not into the
|
||||
// UAT spec file. Without this check the unit re-dispatches indefinitely.
|
||||
const assessmentFile = resolveSliceFile(base, mid, sid, "ASSESSMENT");
|
||||
if (assessmentFile) {
|
||||
const assessmentContent = await loadFile(assessmentFile);
|
||||
if (assessmentContent && hasVerdict(assessmentContent)) return null;
|
||||
}
|
||||
const uatType = getUatType(uatContent);
|
||||
return { sliceId: sid, uatType };
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logWarning(
|
||||
"prompt",
|
||||
`checkNeedsRunUat DB lookup failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
// Unmigrated/recovery fallback using rendered roadmap checkboxes. The DB path
|
||||
// above remains authoritative when slice rows exist.
|
||||
if (!prefs?.uat_dispatch) return null;
|
||||
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
||||
if (!roadmapPath) return null;
|
||||
const roadmapContent = await loadFile(roadmapPath);
|
||||
if (!roadmapContent) return null;
|
||||
const parsed = parseRoadmap(roadmapContent);
|
||||
const completedFileSlices = parsed.slices.filter((s) => s.done);
|
||||
const incompleteFileSlices = parsed.slices.filter((s) => !s.done);
|
||||
if (completedFileSlices.length === 0 || incompleteFileSlices.length === 0)
|
||||
return null;
|
||||
const lastCompletedFile = completedFileSlices[completedFileSlices.length - 1];
|
||||
const uatSid = lastCompletedFile.id;
|
||||
const uatFileFb = resolveSliceFile(base, mid, uatSid, "UAT");
|
||||
if (!uatFileFb) return null;
|
||||
const uatContentFb = await loadFile(uatFileFb);
|
||||
if (!uatContentFb) return null;
|
||||
// If the UAT file already contains a verdict, UAT has been run — skip
|
||||
if (hasVerdict(uatContentFb)) return null;
|
||||
// Also check the ASSESSMENT file for the file-based fallback path (same
|
||||
// reason as the DB path above — verdict lives in ASSESSMENT, not UAT).
|
||||
const assessmentFileFb = resolveSliceFile(base, mid, uatSid, "ASSESSMENT");
|
||||
if (assessmentFileFb) {
|
||||
const assessmentContentFb = await loadFile(assessmentFileFb);
|
||||
if (assessmentContentFb && hasVerdict(assessmentContentFb)) return null;
|
||||
}
|
||||
const uatTypeFb = getUatType(uatContentFb);
|
||||
return { sliceId: uatSid, uatType: uatTypeFb };
|
||||
}
|
||||
// Re-exported from workflow-helpers.js:
|
||||
// - buildResumeSection, buildCarryForwardSection
|
||||
// - checkNeedsReassessment, checkNeedsRunUat
|
||||
// ─── Prompt Builders ──────────────────────────────────────────────────────
|
||||
/**
|
||||
* Build a prompt for the workflow-preferences unit type (deep mode).
|
||||
|
|
|
|||
210
src/resources/extensions/sf/workflow-helpers.js
Normal file
210
src/resources/extensions/sf/workflow-helpers.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Workflow Helpers — state checking and section building for dispatch workflows.
|
||||
*
|
||||
* Purpose: Consolidate functions that check workflow state (reassess, UAT)
|
||||
* and build context sections (resume, carry-forward). Separates workflow logic
|
||||
* from prompt building.
|
||||
*
|
||||
* Consumer: auto-prompts.js for buildReplan* and buildExecute* functions.
|
||||
*/
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveSliceFile } from "./paths.js";
|
||||
import { loadFile, parseSummary, parseContinue } from "./files.js";
|
||||
import { isDbAvailable } from "./sf-db.js";
|
||||
import { hasVerdict } from "./verdict-parser.js";
|
||||
|
||||
/**
|
||||
* Escape regex special characters for safe use in RegExp.
|
||||
* @internal Helper
|
||||
*/
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert multi-line text to single line.
|
||||
* @internal Helper
|
||||
*/
|
||||
function oneLine(text) {
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a markdown section by heading.
|
||||
* @internal Helper
|
||||
*/
|
||||
function extractMarkdownSection(content, heading) {
|
||||
const match = new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").exec(
|
||||
content,
|
||||
);
|
||||
if (!match) return null;
|
||||
const start = match.index + match[0].length;
|
||||
const rest = content.slice(start);
|
||||
const nextHeading = rest.match(/^##\s+/m);
|
||||
const end = nextHeading?.index ?? rest.length;
|
||||
return rest.slice(0, end).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the most recently completed slice needs reassessment.
|
||||
* Returns { sliceId } if reassessment is needed, null otherwise.
|
||||
*
|
||||
* Skips reassessment when:
|
||||
* - No roadmap exists yet
|
||||
* - No slices are completed
|
||||
* - The last completed slice already has an assessment file
|
||||
* - All slices are complete (milestone done — no point reassessing)
|
||||
*/
|
||||
export async function checkNeedsReassessment(base, mid, _state, prefs) {
|
||||
// DB primary path
|
||||
try {
|
||||
const { getMilestoneSlices } = await import("./sf-db.js");
|
||||
if (isDbAvailable()) {
|
||||
const slices = getMilestoneSlices(mid);
|
||||
if (slices.length > 0) {
|
||||
const completedSliceIds = slices
|
||||
.filter((s) => s.status === "complete")
|
||||
.map((s) => s.id);
|
||||
const hasIncomplete = slices.some((s) => s.status !== "complete");
|
||||
if (completedSliceIds.length === 0 || !hasIncomplete) return null;
|
||||
const lastCompleted = completedSliceIds[completedSliceIds.length - 1];
|
||||
const assessmentFile = resolveSliceFile(
|
||||
base,
|
||||
mid,
|
||||
lastCompleted,
|
||||
"ASSESS",
|
||||
);
|
||||
if (assessmentFile && existsSync(assessmentFile)) return null;
|
||||
return { sliceId: lastCompleted };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to file-based
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the most recently completed slice needs a UAT run.
|
||||
* Returns { sliceId, uatType } if UAT should be dispatched, null otherwise.
|
||||
*
|
||||
* Skips when:
|
||||
* - No roadmap or no completed slices
|
||||
* - All slices are done (milestone complete path)
|
||||
* - uat_dispatch preference is not enabled
|
||||
* - No UAT file exists for the slice
|
||||
* - UAT result file already exists (idempotent)
|
||||
*/
|
||||
export async function checkNeedsRunUat(base, mid, _state, prefs) {
|
||||
// Check if UAT dispatch is enabled
|
||||
if (!prefs?.uat_dispatch) return null;
|
||||
|
||||
try {
|
||||
const { getMilestoneSlices } = await import("./sf-db.js");
|
||||
if (isDbAvailable()) {
|
||||
const slices = getMilestoneSlices(mid);
|
||||
if (slices.length > 0) {
|
||||
const completedSlices = slices.filter((s) => s.status === "complete");
|
||||
const hasIncomplete = slices.some((s) => s.status !== "complete");
|
||||
if (completedSlices.length === 0 || !hasIncomplete) return null;
|
||||
const lastCompleted = completedSlices[completedSlices.length - 1];
|
||||
const uatFile = resolveSliceFile(base, mid, lastCompleted.id, "UAT");
|
||||
if (!uatFile || !existsSync(uatFile)) return null;
|
||||
const resultFile = resolveSliceFile(
|
||||
base,
|
||||
mid,
|
||||
lastCompleted.id,
|
||||
"UAT_RESULT",
|
||||
);
|
||||
if (resultFile && existsSync(resultFile)) return null;
|
||||
const uatContent = await loadFile(uatFile);
|
||||
const uatType = hasVerdict(uatContent) ? "verdict" : "narrative";
|
||||
return { sliceId: lastCompleted.id, uatType };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the resume section for continuing a task.
|
||||
* Used to replay the continue file and provide context for resuming work.
|
||||
*/
|
||||
export function buildResumeSection(
|
||||
continueContent,
|
||||
legacyContinueContent,
|
||||
continueRelPath,
|
||||
legacyContinueRelPath,
|
||||
) {
|
||||
const resolvedContent = continueContent ?? legacyContinueContent;
|
||||
const resolvedRelPath = continueContent
|
||||
? continueRelPath
|
||||
: legacyContinueRelPath;
|
||||
if (!resolvedContent || !resolvedRelPath) {
|
||||
return [
|
||||
"## Resume State",
|
||||
"- No continue file present. Start from the top of the task plan.",
|
||||
].join("\n");
|
||||
}
|
||||
const cont = parseContinue(resolvedContent);
|
||||
const lines = [
|
||||
"## Resume State",
|
||||
`Source: \`${resolvedRelPath}\``,
|
||||
`- Status: ${cont.frontmatter.status || "in_progress"}`,
|
||||
];
|
||||
if (cont.frontmatter.step && cont.frontmatter.totalSteps) {
|
||||
lines.push(
|
||||
`- Progress: step ${cont.frontmatter.step} of ${cont.frontmatter.totalSteps}`,
|
||||
);
|
||||
}
|
||||
if (cont.completedWork)
|
||||
lines.push(`- Completed: ${oneLine(cont.completedWork)}`);
|
||||
if (cont.remainingWork)
|
||||
lines.push(`- Remaining: ${oneLine(cont.remainingWork)}`);
|
||||
if (cont.decisions) lines.push(`- Decisions: ${oneLine(cont.decisions)}`);
|
||||
if (cont.nextAction) lines.push(`- Next action: ${oneLine(cont.nextAction)}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the carry-forward section for the next task.
|
||||
* Inlines prior task summaries with key info and diagnostics.
|
||||
*/
|
||||
export async function buildCarryForwardSection(priorSummaryPaths, base) {
|
||||
if (priorSummaryPaths.length === 0) {
|
||||
return [
|
||||
"## Carry-Forward Context",
|
||||
"- No prior task summaries in this slice.",
|
||||
].join("\n");
|
||||
}
|
||||
const items = await Promise.all(
|
||||
priorSummaryPaths.map(async (relPath) => {
|
||||
const absPath = join(base, relPath);
|
||||
const content = await loadFile(absPath);
|
||||
if (!content) return `- \`${relPath}\``;
|
||||
const summary = parseSummary(content);
|
||||
const provided = summary.frontmatter.provides.slice(0, 2).join("; ");
|
||||
const decisions = summary.frontmatter.key_decisions
|
||||
.slice(0, 2)
|
||||
.join("; ");
|
||||
const patterns = summary.frontmatter.patterns_established
|
||||
.slice(0, 2)
|
||||
.join("; ");
|
||||
const keyFiles = summary.frontmatter.key_files.slice(0, 3).join("; ");
|
||||
const diagnostics = extractMarkdownSection(content, "Diagnostics");
|
||||
const parts = [summary.title || relPath];
|
||||
if (summary.oneLiner) parts.push(summary.oneLiner);
|
||||
if (provided) parts.push(`provides: ${provided}`);
|
||||
if (decisions) parts.push(`decisions: ${decisions}`);
|
||||
if (patterns) parts.push(`patterns: ${patterns}`);
|
||||
if (keyFiles) parts.push(`key_files: ${keyFiles}`);
|
||||
if (diagnostics) parts.push(`diagnostics: ${oneLine(diagnostics)}`);
|
||||
return `- \`${relPath}\` — ${parts.join(" | ")}`;
|
||||
}),
|
||||
);
|
||||
return ["## Carry-Forward Context", ...items].join("\n");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue