diff --git a/src/resources/extensions/sf/auto-prompts.js b/src/resources/extensions/sf/auto-prompts.js index 2cc848fae..fdef1e450 100644 --- a/src/resources/extensions/sf/auto-prompts.js +++ b/src/resources/extensions/sf/auto-prompts.js @@ -81,7 +81,7 @@ import { getDependencyTaskSummaryPaths, getPriorTaskSummaryPaths, } from "./summary-helpers.js"; -import { composeInlinedContext } from "./unit-context-composer.js"; +import { composeInlinedContext, composeUnitContext } from "./unit-context-composer.js"; import { getUatType } from "./verdict-parser.js"; import { buildCarryForwardSection, @@ -1197,71 +1197,80 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) { const contextRel = relMilestoneFile(base, mid, "CONTEXT"); const researchPath = resolveMilestoneFile(base, mid, "RESEARCH"); const researchRel = relMilestoneFile(base, mid, "RESEARCH"); - const inlined = []; - // Inject phase handoff anchor from research phase (if available) - const researchAnchor = readPhaseAnchor(base, mid, "research-milestone"); - if (researchAnchor) inlined.push(formatAnchorForPrompt(researchAnchor)); - inlined.push(await inlineFile(contextPath, contextRel, "Milestone Context")); - const researchInline = await inlineFileOptional( - researchPath, - researchRel, - "Milestone Research", - ); - if (researchInline) inlined.push(researchInline); - const { inlinePriorMilestoneSummary } = await import("./files.js"); - const priorSummaryInline = await inlinePriorMilestoneSummary(mid, base); - if (priorSummaryInline) inlined.push(priorSummaryInline); - if (inlineLevel !== "minimal") { - const projectInline = await inlineProjectFromDb(base); - if (projectInline) inlined.push(projectInline); - const requirementsInline = await inlineRequirementsFromDb( - base, - mid, - undefined, - inlineLevel, - ); - if (requirementsInline) inlined.push(requirementsInline); - const decisionsInline = await inlineDecisionsFromDb( - base, - mid, - undefined, - inlineLevel, - ); - if (decisionsInline) inlined.push(decisionsInline); - } const queuePath = resolveSfRootFile(base, "QUEUE"); - if (existsSync(queuePath)) { - const queueInline = await inlineFileSmart( - queuePath, - relSfRootFile("QUEUE"), - "Project Queue", - `${mid} ${midTitle}`, - ); - inlined.push(queueInline); - } - // Scoped + budgeted — see issue #4719 - const knowledgeInlinePM = await inlineKnowledgeBudgeted( + const keywords = extractKeywords(midTitle); + const { inlinePriorMilestoneSummary } = await import("./files.js"); + const { prepend, inline } = await composeUnitContext("plan-milestone", { base, - extractKeywords(midTitle), - ); - if (knowledgeInlinePM) inlined.push(knowledgeInlinePM); - const graphBlockPM = await inlineGraphSubgraph(base, `${mid} ${midTitle}`, { - budget: 3000, + resolveArtifact: async (key) => { + switch (key) { + case "milestone-context": + return inlineFile(contextPath, contextRel, "Milestone Context"); + case "milestone-research": + return inlineFileOptional( + researchPath, + researchRel, + "Milestone Research", + ); + case "prior-milestone-summary": + return inlinePriorMilestoneSummary(mid, base); + case "project": + if (inlineLevel === "minimal") return null; + return inlineProjectFromDb(base); + case "requirements": + if (inlineLevel === "minimal") return null; + return inlineRequirementsFromDb(base, mid, undefined, inlineLevel); + case "decisions": + if (inlineLevel === "minimal") return null; + return inlineDecisionsFromDb(base, mid, undefined, inlineLevel); + case "queue": + if (!existsSync(queuePath)) return null; + return inlineFileSmart( + queuePath, + relSfRootFile("QUEUE"), + "Project Queue", + `${mid} ${midTitle}`, + ); + case "templates": { + const tplParts = [inlineTemplate("roadmap", "Roadmap")]; + if (inlineLevel === "full" || inlineLevel === "standard") { + tplParts.push(inlineTemplate("decisions", "Decisions")); + tplParts.push(inlineTemplate("plan", "Slice Plan")); + tplParts.push(inlineTemplate("task-plan", "Task Plan")); + } + if (inlineLevel === "full") { + tplParts.push( + inlineTemplate("secrets-manifest", "Secrets Manifest"), + ); + } + return tplParts.join("\n\n---\n\n"); + } + default: + return null; + } + }, + computed: { + "phase-anchor": { + build: async (_, b) => { + const anchor = readPhaseAnchor(b, mid, "research-milestone"); + return anchor ? formatAnchorForPrompt(anchor) : null; + }, + inputs: {}, + }, + knowledge: { + build: async ({ kw }, b) => inlineKnowledgeBudgeted(b, kw), + inputs: { kw: keywords }, + }, + graph: { + build: async ({ query }, b) => + inlineGraphSubgraph(b, query, { budget: 3000 }), + inputs: { query: `${mid} ${midTitle}` }, + }, + }, }); - if (graphBlockPM) inlined.push(graphBlockPM); - inlined.push(inlineTemplate("roadmap", "Roadmap")); - if (inlineLevel === "full") { - inlined.push(inlineTemplate("decisions", "Decisions")); - inlined.push(inlineTemplate("plan", "Slice Plan")); - inlined.push(inlineTemplate("task-plan", "Task Plan")); - inlined.push(inlineTemplate("secrets-manifest", "Secrets Manifest")); - } else if (inlineLevel === "standard") { - inlined.push(inlineTemplate("decisions", "Decisions")); - inlined.push(inlineTemplate("plan", "Slice Plan")); - inlined.push(inlineTemplate("task-plan", "Task Plan")); - } + const parts = [prepend, inline].filter(Boolean); const inlinedContext = capPreamble( - `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`, + `## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`, ); // Milestone framing check — surfaces anti-goal violations and vision-alignment // concerns in the planning context. Non-blocking: the agent reads and decides. @@ -1321,62 +1330,68 @@ export async function buildResearchSlicePrompt( const milestoneResearchRel = relMilestoneFile(base, mid, "RESEARCH"); const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT"); const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT"); - const inlined = []; - // Use roadmap excerpt instead of full roadmap for context reduction - const roadmapExcerptRS = await inlineRoadmapExcerpt(base, mid, sid); - if (roadmapExcerptRS) { - inlined.push(roadmapExcerptRS); - } else { - // Fall back to full roadmap if excerpt fails - inlined.push( - await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"), - ); - } - const contextInline = await inlineFileOptional( - contextPath, - contextRel, - "Milestone Context", - ); - if (contextInline) inlined.push(contextInline); - const sliceCtxInline = await inlineFileOptional( - sliceContextPath, - sliceContextRel, - "Slice Context (from discussion)", - ); - if (sliceCtxInline) inlined.push(sliceCtxInline); - const researchInline = await inlineFileOptional( - milestoneResearchPath, - milestoneResearchRel, - "Milestone Research", - ); - if (researchInline) inlined.push(researchInline); - // Derive scope from slice title for decision filtering (R005) - const derivedScope = deriveSliceScope(sTitle); - const decisionsInline = await inlineDecisionsFromDb(base, mid, derivedScope); - if (decisionsInline) inlined.push(decisionsInline); - const requirementsInline = await inlineRequirementsFromDb(base, mid, sid); - if (requirementsInline) inlined.push(requirementsInline); - // Use scoped knowledge based on slice title keywords - const keywords = extractKeywords(sTitle); - const knowledgeInlineRS = await inlineKnowledgeScoped(base, keywords); - if (knowledgeInlineRS) inlined.push(knowledgeInlineRS); - // Knowledge graph: subgraph for this slice (graceful — skipped if no graph.json) - const graphBlockRS = await inlineGraphSubgraph(base, `${sid} ${sTitle}`, { - budget: 3000, - }); - if (graphBlockRS) inlined.push(graphBlockRS); - inlined.push(inlineTemplate("research", "Research")); + // depContent stays as a separate template variable (not part of inlinedContext). const depContent = await inlineDependencySummaries( mid, sid, base, resolveSummaryBudgetChars(), ); - const activeOverrides = await loadActiveOverrides(base); - const overridesInline = formatOverridesSection(activeOverrides); - if (overridesInline) inlined.unshift(overridesInline); + const keywords = extractKeywords(sTitle); + const { prepend, inline } = await composeUnitContext("research-slice", { + base, + resolveArtifact: async (key) => { + switch (key) { + case "roadmap": { + // Excerpt with full-roadmap fallback for context reduction. + const excerpt = await inlineRoadmapExcerpt(base, mid, sid); + return excerpt ?? inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"); + } + case "milestone-context": + return inlineFileOptional(contextPath, contextRel, "Milestone Context"); + case "slice-context": + return inlineFileOptional( + sliceContextPath, + sliceContextRel, + "Slice Context (from discussion)", + ); + case "milestone-research": + return inlineFileOptional( + milestoneResearchPath, + milestoneResearchRel, + "Milestone Research", + ); + case "decisions": + // Derive scope from slice title for decision filtering (R005). + return inlineDecisionsFromDb(base, mid, deriveSliceScope(sTitle)); + case "requirements": + return inlineRequirementsFromDb(base, mid, sid); + case "templates": + return inlineTemplate("research", "Research"); + default: + return null; + } + }, + computed: { + overrides: { + build: async (_, b) => + formatOverridesSection(await loadActiveOverrides(b)), + inputs: {}, + }, + knowledge: { + build: async ({ kw }, b) => inlineKnowledgeScoped(b, kw), + inputs: { kw: keywords }, + }, + graph: { + build: async ({ query }, b) => + inlineGraphSubgraph(b, query, { budget: 3000 }), + inputs: { query: `${sid} ${sTitle}` }, + }, + }, + }); + const parts = [prepend, inline].filter(Boolean); const inlinedContext = capPreamble( - `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`, + `## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`, ); const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH"); return loadPrompt("research-slice", { @@ -1408,9 +1423,9 @@ export async function buildResearchSlicePrompt( * requirements, knowledge, graph subgraph, templates, dependency summaries, * overrides). Extracted to prevent drift between the two sites. * - * `prependBlocks` are pushed onto the start of the inlined array BEFORE any - * shared content, so callers can add unit-specific headers (e.g., the refine - * sketch-scope constraint). + * `prependBlocks` are inserted between the overrides prepend and the first + * inline artifact, so callers can add unit-specific headers (e.g., the refine + * sketch-scope constraint or plan-slice pre-exec failure context). */ async function renderSlicePrompt(options) { const { @@ -1431,74 +1446,79 @@ async function renderSlicePrompt(options) { const researchRel = relSliceFile(base, mid, sid, "RESEARCH"); const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT"); const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT"); - const inlined = [...prependBlocks]; - // Phase handoff anchor from research phase (if available) + // Phase handoff anchor from research phase (if available). + // Combined with caller prependBlocks to form prefixContent inserted between + // the overrides prepend and the inline artifacts. const researchSliceAnchor = readPhaseAnchor(base, mid, "research-slice"); - if (researchSliceAnchor) - inlined.push(formatAnchorForPrompt(researchSliceAnchor)); - // Roadmap excerpt with full-roadmap fallback - const roadmapExcerpt = await inlineRoadmapExcerpt(base, mid, sid); - if (roadmapExcerpt) { - inlined.push(roadmapExcerpt); - } else { - inlined.push( - await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"), - ); - } - const sliceCtxInline = await inlineFileOptional( - sliceContextPath, - sliceContextRel, - "Slice Context (from discussion)", - ); - if (sliceCtxInline) inlined.push(sliceCtxInline); - const researchInline = await inlineFileOptional( - researchPath, - researchRel, - "Slice Research", - ); - if (researchInline) inlined.push(researchInline); - if (level !== "minimal") { - const derivedScope = deriveSliceScope(sTitle); - const decisionsInline = await inlineDecisionsFromDb( - base, - mid, - derivedScope, - level, - ); - if (decisionsInline) inlined.push(decisionsInline); - const requirementsInline = await inlineRequirementsFromDb( - base, - mid, - sid, - level, - ); - if (requirementsInline) inlined.push(requirementsInline); - } - const knowledgeInline = await inlineKnowledgeScoped( - base, - extractKeywords(sTitle), - ); - if (knowledgeInline) inlined.push(knowledgeInline); - const graphBlock = await inlineGraphSubgraph(base, `${sid} ${sTitle}`, { - budget: 3000, - }); - if (graphBlock) inlined.push(graphBlock); - inlined.push(inlineTemplate("plan", "Slice Plan")); - if (level === "full") { - inlined.push(inlineTemplate("task-plan", "Task Plan")); - } + const prefixBlocks = [...prependBlocks]; + if (researchSliceAnchor) prefixBlocks.push(formatAnchorForPrompt(researchSliceAnchor)); + const prefixContent = + prefixBlocks.length > 0 ? prefixBlocks.join("\n\n---\n\n") : null; const depContent = await inlineDependencySummaries( mid, sid, base, resolveSummaryBudgetChars(), ); - const overridesInline = formatOverridesSection( - await loadActiveOverrides(base), - ); - if (overridesInline) inlined.unshift(overridesInline); + const { prepend, inline } = await composeUnitContext(promptTemplate, { + base, + resolveArtifact: async (key) => { + switch (key) { + case "roadmap": { + // Excerpt with full-roadmap fallback for context reduction. + const excerpt = await inlineRoadmapExcerpt(base, mid, sid); + return excerpt ?? inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"); + } + case "slice-context": + return inlineFileOptional( + sliceContextPath, + sliceContextRel, + "Slice Context (from discussion)", + ); + case "slice-research": + return inlineFileOptional(researchPath, researchRel, "Slice Research"); + case "decisions": + if (level === "minimal") return null; + return inlineDecisionsFromDb( + base, + mid, + deriveSliceScope(sTitle), + level, + ); + case "requirements": + if (level === "minimal") return null; + return inlineRequirementsFromDb(base, mid, sid, level); + case "templates": { + const tplParts = [inlineTemplate("plan", "Slice Plan")]; + if (level === "full") tplParts.push(inlineTemplate("task-plan", "Task Plan")); + return tplParts.join("\n\n---\n\n"); + } + default: + return null; + } + }, + computed: { + overrides: { + build: async (_, b) => + formatOverridesSection(await loadActiveOverrides(b)), + inputs: {}, + }, + knowledge: { + build: async ({ keywords }, b) => + inlineKnowledgeScoped(b, keywords), + inputs: { keywords: extractKeywords(sTitle) }, + }, + graph: { + build: async ({ query }, b) => + inlineGraphSubgraph(b, query, { budget: 3000 }), + inputs: { query: `${sid} ${sTitle}` }, + }, + }, + }); + // Combine: overrides prepend → caller prefix (prependBlocks + phase anchor) → inline artifacts + computed + const allParts = [prepend, prefixContent, inline].filter(Boolean); const inlinedContext = capPreamble( - `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`, + `## Inlined Context (preloaded — do not re-read these files)\n\n${allParts.join("\n\n---\n\n")}`, ); const executorContextConstraints = formatExecutorConstraints( sessionContextWindow, @@ -2119,9 +2139,8 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) { const inlineLevel = level ?? resolveInlineLevel(); const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP"); const roadmapRel = relMilestoneFile(base, mid, "ROADMAP"); - const inlined = []; - inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap")); - // Inline all slice summaries (deduplicated by slice ID) + // Pre-compute slice summaries (parallel DB + file reads). + // This is a complex aggregation that feeds resolveArtifact("slice-summaries"). let sliceIds = []; try { const { isDbAvailable, getMilestoneSlices } = await import("./sf-db.js"); @@ -2136,82 +2155,84 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) { `buildCompleteMilestonePrompt DB lookup failed: ${getErrorMessage(err)}`, ); } - // File-based fallback: parse roadmap for slice IDs when DB has no data + // File-based fallback: parse roadmap for slice IDs when DB has no data. if (sliceIds.length === 0 && roadmapPath) { const roadmapContent = await loadFile(roadmapPath); if (roadmapContent) { sliceIds = parseRoadmap(roadmapContent).slices.map((s) => s.id); } } - // Deduplicate slice IDs while preserving order. const uniqueSliceIds = [...new Set(sliceIds)]; - // Load all slice summary excerpts in parallel — independent reads. + // Parallel slice summary excerpts — independent reads. const sliceSummaryResults = await Promise.all( uniqueSliceIds.map(async (sid) => { const summaryPath = resolveSliceFile(base, mid, sid, "SUMMARY"); const summaryRel = relSliceFile(base, mid, sid, "SUMMARY"); - // Compact excerpt instead of full inline (#4780). Closer Reads the - // full file on-demand when synthesizing LEARNINGS narrative. const excerpt = await buildSliceSummaryExcerpt( summaryPath, summaryRel, sid, ); - return { sid, summaryRel, excerpt }; + return { summaryRel, excerpt }; }), ); - const summaryRelPaths = []; - for (const { summaryRel, excerpt } of sliceSummaryResults) { - summaryRelPaths.push(summaryRel); - inlined.push(excerpt); + const summaryRelPaths = sliceSummaryResults.map((r) => r.summaryRel); + const excerptBlocks = sliceSummaryResults.map((r) => r.excerpt); + let sliceSummariesBlock = null; + if (excerptBlocks.length > 0) { + const parts = [...excerptBlocks]; + if (summaryRelPaths.length > 0) { + const pathList = summaryRelPaths.map((p) => `- \`${p}\``).join("\n"); + parts.push( + `### On-demand Slice Summaries\n\nExcerpted above. Read the full file for any slice when the excerpt's section heads don't carry enough narrative for the milestone summary you're drafting:\n\n${pathList}`, + ); + } + sliceSummariesBlock = parts.join("\n\n---\n\n"); } - if (summaryRelPaths.length > 0) { - const pathList = summaryRelPaths.map((p) => `- \`${p}\``).join("\n"); - inlined.push( - `### On-demand Slice Summaries\n\nExcerpted above. Read the full file for any slice when the excerpt's section heads don't carry enough narrative for the milestone summary you're drafting:\n\n${pathList}`, - ); - } - // Inline root SF files (skip for minimal — completion can read these if needed) - if (inlineLevel !== "minimal") { - const requirementsInline = await inlineRequirementsFromDb( - base, - mid, - undefined, - inlineLevel, - ); - if (requirementsInline) inlined.push(requirementsInline); - const decisionsInline = await inlineDecisionsFromDb( - base, - mid, - undefined, - inlineLevel, - ); - if (decisionsInline) inlined.push(decisionsInline); - const projectInline = await inlineProjectFromDb(base); - if (projectInline) inlined.push(projectInline); - } - // Scoped + budgeted — see issue #4719 - const knowledgeInlineCM = await inlineKnowledgeBudgeted( - base, - extractKeywords(midTitle), - ); - if (knowledgeInlineCM) inlined.push(knowledgeInlineCM); - const graphBlockCM = await inlineGraphSubgraph(base, `${mid} ${midTitle}`, { - budget: 3000, - }); - if (graphBlockCM) inlined.push(graphBlockCM); - // Inline milestone context file (milestone-level, not SF root) const contextPath = resolveMilestoneFile(base, mid, "CONTEXT"); const contextRel = relMilestoneFile(base, mid, "CONTEXT"); - const contextInline = await inlineFileOptional( - contextPath, - contextRel, - "Milestone Context", - ); - if (contextInline) inlined.push(contextInline); - inlined.push(inlineTemplate("milestone-summary", "Milestone Summary")); + const keywords = extractKeywords(midTitle); + const { prepend, inline } = await composeUnitContext("complete-milestone", { + base, + resolveArtifact: async (key) => { + switch (key) { + case "roadmap": + return inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"); + case "slice-summaries": + return sliceSummariesBlock; + case "requirements": + if (inlineLevel === "minimal") return null; + return inlineRequirementsFromDb(base, mid, undefined, inlineLevel); + case "decisions": + if (inlineLevel === "minimal") return null; + return inlineDecisionsFromDb(base, mid, undefined, inlineLevel); + case "project": + if (inlineLevel === "minimal") return null; + return inlineProjectFromDb(base); + case "milestone-context": + return inlineFileOptional(contextPath, contextRel, "Milestone Context"); + case "templates": + return inlineTemplate("milestone-summary", "Milestone Summary"); + default: + return null; + } + }, + computed: { + knowledge: { + build: async ({ keywords: kw }, b) => + inlineKnowledgeBudgeted(b, kw), + inputs: { keywords }, + }, + graph: { + build: async ({ query }, b) => + inlineGraphSubgraph(b, query, { budget: 3000 }), + inputs: { query: `${mid} ${midTitle}` }, + }, + }, + }); + const parts = [prepend, inline].filter(Boolean); const inlinedContext = capPreamble( - `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`, + `## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`, ); const milestoneSummaryPath = join( base, @@ -2248,9 +2269,8 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) { const inlineLevel = level ?? resolveInlineLevel(); const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP"); const roadmapRel = relMilestoneFile(base, mid, "ROADMAP"); - const inlined = []; - inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap")); - // Inline verification classes from planning (if available in DB) + // Pre-compute verification classes from DB. + let verificationClassesBlock = null; try { const { isDbAvailable, getMilestone } = await import("./sf-db.js"); if (isDbAvailable()) { @@ -2270,9 +2290,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) { if (milestone.verification_uat) classes.push(`- **UAT:** ${milestone.verification_uat}`); if (classes.length > 0) { - inlined.push( - `### Verification Classes (from planning)\n\nThese verification tiers were defined during milestone planning. Each non-empty class must be checked for evidence during validation.\n\n${classes.join("\n")}`, - ); + verificationClassesBlock = `### Verification Classes (from planning)\n\nThese verification tiers were defined during milestone planning. Each non-empty class must be checked for evidence during validation.\n\n${classes.join("\n")}`; } } } @@ -2282,7 +2300,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) { `buildValidateMilestonePrompt verification classes lookup failed: ${getErrorMessage(err)}`, ); } - // Inline all slice summaries and assessment results + // Pre-compute slice summaries, assessments, and outstanding items. let valSliceIds = []; try { const { isDbAvailable, getMilestoneSlices } = await import("./sf-db.js"); @@ -2297,17 +2315,16 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) { `buildValidateMilestonePrompt slice IDs lookup failed: ${getErrorMessage(err)}`, ); } - // File-based fallback: parse roadmap for slice IDs when DB has no data + // File-based fallback: parse roadmap for slice IDs when DB has no data. if (valSliceIds.length === 0 && roadmapPath) { const roadmapContent = await loadFile(roadmapPath); if (roadmapContent) { valSliceIds = parseRoadmap(roadmapContent).slices.map((s) => s.id); } } - // Single parallel pass per slice: load summary + assessment, derive inline - // blocks AND outstanding-items extraction in one read (previously two loops - // that each called loadFile on every SUMMARY). const uniqueValSliceIds = [...new Set(valSliceIds)]; + // Single parallel pass per slice: load summary + assessment, derive inline + // blocks AND outstanding-items extraction in one read. const valSliceResults = await Promise.all( uniqueValSliceIds.map(async (sid) => { const summaryPath = resolveSliceFile(base, mid, sid, "SUMMARY"); @@ -2321,7 +2338,6 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) { const summaryInline = summaryContent ? `### ${sid} Summary\nSource: \`${summaryRel}\`\n\n${summaryContent.trim()}` : `### ${sid} Summary\nSource: \`${summaryRel}\`\n\n_(not found — file does not exist yet)_`; - // Derive outstanding items from the same content we just loaded. const outstandingLines = []; if (summaryContent) { try { @@ -2341,86 +2357,87 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) { return { summaryInline, assessmentInline, outstandingLines }; }), ); - // Push inline blocks in order; collect outstanding items across all slices. + // Assemble slice-summaries block (summaries + assessments interleaved). + const sliceSummariesParts = []; const outstandingItems = []; - for (const { - summaryInline, - assessmentInline, - outstandingLines, - } of valSliceResults) { - inlined.push(summaryInline); - if (assessmentInline) inlined.push(assessmentInline); + for (const { summaryInline, assessmentInline, outstandingLines } of valSliceResults) { + sliceSummariesParts.push(summaryInline); + if (assessmentInline) sliceSummariesParts.push(assessmentInline); outstandingItems.push(...outstandingLines); } - if (outstandingItems.length > 0) { - inlined.push( - `### Outstanding Items (aggregated from slice summaries)\n\nThese follow-ups and known limitations were documented during slice completion but have not been resolved.\n\n${outstandingItems.join("\n")}`, - ); - } - // Inline existing VALIDATION file if this is a re-validation round + const sliceSummariesBlock = + sliceSummariesParts.length > 0 + ? sliceSummariesParts.join("\n\n---\n\n") + : null; + const outstandingItemsBlock = + outstandingItems.length > 0 + ? `### Outstanding Items (aggregated from slice summaries)\n\nThese follow-ups and known limitations were documented during slice completion but have not been resolved.\n\n${outstandingItems.join("\n")}` + : null; + // Pre-compute previous validation for re-validation rounds. const validationPath = resolveMilestoneFile(base, mid, "VALIDATION"); const validationRel = relMilestoneFile(base, mid, "VALIDATION"); - const validationContent = validationPath - ? await loadFile(validationPath) - : null; + const validationContent = validationPath ? await loadFile(validationPath) : null; let remediationRound = 0; + let previousValidationBlock = null; if (validationContent) { const roundMatch = validationContent.match(/remediation_round:\s*(\d+)/); remediationRound = roundMatch ? parseInt(roundMatch[1], 10) + 1 : 1; - inlined.push( - `### Previous Validation (re-validation round ${remediationRound})\nSource: \`${validationRel}\`\n\n${validationContent.trim()}`, - ); + previousValidationBlock = `### Previous Validation (re-validation round ${remediationRound})\nSource: \`${validationRel}\`\n\n${validationContent.trim()}`; } - // Inline root SF files - if (inlineLevel !== "minimal") { - const requirementsInline = await inlineRequirementsFromDb( - base, - mid, - undefined, - inlineLevel, - ); - if (requirementsInline) inlined.push(requirementsInline); - const decisionsInline = await inlineDecisionsFromDb( - base, - mid, - undefined, - inlineLevel, - ); - if (decisionsInline) inlined.push(decisionsInline); - const projectInline = await inlineProjectFromDb(base); - if (projectInline) inlined.push(projectInline); - } - // Scoped + budgeted — see issue #4719 - const knowledgeInline = await inlineKnowledgeBudgeted( - base, - extractKeywords(midTitle), - ); - if (knowledgeInline) inlined.push(knowledgeInline); - const graphBlockVM = await inlineGraphSubgraph(base, `${mid} ${midTitle}`, { - budget: 3000, - }); - if (graphBlockVM) inlined.push(graphBlockVM); - // Inline milestone context file const contextPath = resolveMilestoneFile(base, mid, "CONTEXT"); const contextRel = relMilestoneFile(base, mid, "CONTEXT"); - const contextInline = await inlineFileOptional( - contextPath, - contextRel, - "Milestone Context", - ); - if (contextInline) inlined.push(contextInline); + const keywords = extractKeywords(midTitle); + const { prepend, inline } = await composeUnitContext("validate-milestone", { + base, + resolveArtifact: async (key) => { + switch (key) { + case "roadmap": + return inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"); + case "verification-classes": + return verificationClassesBlock; + case "slice-summaries": + return sliceSummariesBlock; + case "outstanding-items": + return outstandingItemsBlock; + case "previous-validation": + return previousValidationBlock; + case "requirements": + if (inlineLevel === "minimal") return null; + return inlineRequirementsFromDb(base, mid, undefined, inlineLevel); + case "decisions": + if (inlineLevel === "minimal") return null; + return inlineDecisionsFromDb(base, mid, undefined, inlineLevel); + case "project": + if (inlineLevel === "minimal") return null; + return inlineProjectFromDb(base); + case "milestone-context": + return inlineFileOptional(contextPath, contextRel, "Milestone Context"); + default: + return null; + } + }, + computed: { + knowledge: { + build: async ({ keywords: kw }, b) => + inlineKnowledgeBudgeted(b, kw), + inputs: { keywords }, + }, + graph: { + build: async ({ query }, b) => + inlineGraphSubgraph(b, query, { budget: 3000 }), + inputs: { query: `${mid} ${midTitle}` }, + }, + }, + }); + const parts = [prepend, inline].filter(Boolean); const inlinedContext = capPreamble( - `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`, + `## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`, ); const validationOutputPath = join( base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`, ); const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`; - // Every milestone validation turn owns MV01–MV04 unconditionally: the - // registry is the source of truth for which gates the validator must - // address, and the block below is what the template renders so the - // assistant can never accidentally skip one. const mvGates = getGatesForTurn("validate-milestone"); const gatesToEvaluate = renderGatesToCloseBlock(mvGates, { pending: new Set(mvGates.map((g) => g.id)), @@ -2451,19 +2468,10 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) { const slicePlanRel = relSliceFile(base, mid, sid, "PLAN"); const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT"); const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT"); - const inlined = []; - inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap")); - const sliceCtxInline = await inlineFileOptional( - sliceContextPath, - sliceContextRel, - "Slice Context (from discussion)", - ); - if (sliceCtxInline) inlined.push(sliceCtxInline); - inlined.push( - await inlineFile(slicePlanPath, slicePlanRel, "Current Slice Plan"), - ); - // Find the blocker task summary — the completed task with blocker_discovered: true + // Pre-compute blocker task summaries (scan task files for blocker_discovered). + // Must run before composeUnitContext since the result feeds resolveArtifact. let blockerTaskId = ""; + const blockerBlocks = []; const tDir = resolveTasksDir(base, mid, sid); if (tDir) { const summaryFiles = resolveTaskFiles(tDir, "SUMMARY").sort(); @@ -2477,20 +2485,47 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) { if (summary.frontmatter.blocker_discovered) { blockerTaskId = summary.frontmatter.id || file.replace(/-SUMMARY\.md$/i, ""); - inlined.push( + blockerBlocks.push( `### Blocker Task Summary: ${blockerTaskId}\nSource: \`${relPath}\`\n\n${content.trim()}`, ); } } } - // Inline decisions - const decisionsInline = await inlineDecisionsFromDb(base, mid); - if (decisionsInline) inlined.push(decisionsInline); - const replanActiveOverrides = await loadActiveOverrides(base); - const replanOverridesInline = formatOverridesSection(replanActiveOverrides); - if (replanOverridesInline) inlined.unshift(replanOverridesInline); + const blockerSummariesBlock = + blockerBlocks.length > 0 ? blockerBlocks.join("\n\n---\n\n") : null; + const { prepend, inline } = await composeUnitContext("replan-slice", { + base, + resolveArtifact: async (key) => { + switch (key) { + case "roadmap": + return inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"); + case "slice-context": + return inlineFileOptional( + sliceContextPath, + sliceContextRel, + "Slice Context (from discussion)", + ); + case "slice-plan": + return inlineFile(slicePlanPath, slicePlanRel, "Current Slice Plan"); + case "blocker-summaries": + return blockerSummariesBlock; + case "decisions": + return inlineDecisionsFromDb(base, mid); + default: + return null; + } + }, + computed: { + overrides: { + build: async (_, b) => + formatOverridesSection(await loadActiveOverrides(b)), + inputs: {}, + }, + }, + }); + const parts = [prepend, inline].filter(Boolean); const inlinedContext = capPreamble( - `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`, + `## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`, ); const replanPath = join( base, diff --git a/src/resources/extensions/sf/tests/auto-prompts-phase3.test.mjs b/src/resources/extensions/sf/tests/auto-prompts-phase3.test.mjs new file mode 100644 index 000000000..4082383d4 --- /dev/null +++ b/src/resources/extensions/sf/tests/auto-prompts-phase3.test.mjs @@ -0,0 +1,286 @@ +/** + * auto-prompts-phase3.test.mjs — Phase 3 v2 builder migration contracts. + * + * Purpose: prove that the composeUnitContext v2 migration of plan-milestone, + * validate-milestone, complete-milestone, replan-slice, and research-slice + * produces prompts containing the expected artifact sections and does not + * regress the ordering or content of prior implementations. + */ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; +import { + buildPlanMilestonePrompt, + buildReplanSlicePrompt, + buildResearchSlicePrompt, + buildValidateMilestonePrompt, +} from "../auto-prompts.js"; +import { + closeDatabase, + insertMilestone, + insertSlice, + insertTask, + openDatabase, +} from "../sf-db.js"; + +let tempDirs = []; + +function makeProject(opts = {}) { + const dir = mkdtempSync(join(tmpdir(), "sf-phase3-prompts-")); + tempDirs.push(dir); + const mid = opts.mid ?? "M910"; + const sid = opts.sid ?? "S01"; + mkdirSync(join(dir, ".sf", "milestones", mid, "slices", sid, "tasks"), { + recursive: true, + }); + writeFileSync( + join(dir, ".sf", "milestones", mid, `${mid}-ROADMAP.md`), + `# ${mid}: Test Milestone\n\n## ${sid}: Test Slice\n`, + ); + writeFileSync( + join(dir, ".sf", "milestones", mid, `${mid}-CONTEXT.md`), + `# Context\n\nMilestone context for testing.\n`, + ); + writeFileSync( + join(dir, ".sf", "milestones", mid, "slices", sid, `${sid}-CONTEXT.md`), + `# Slice Context\n\nSlice context for testing.\n`, + ); + writeFileSync( + join(dir, ".sf", "milestones", mid, "slices", sid, `${sid}-PLAN.md`), + `# ${sid}: Test Slice\n\n## Tasks\n\n- T01: Do the thing\n`, + ); + return { dir, mid, sid }; +} + +afterEach(() => { + closeDatabase(); + for (const dir of tempDirs) { + rmSync(dir, { recursive: true, force: true }); + } + tempDirs = []; +}); + +describe("buildPlanMilestonePrompt v2", () => { + test("plan_milestone_prompt_inlines_context_and_templates_at_standard_level", async () => { + const { dir, mid } = makeProject({ mid: "M910" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Test Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + + const prompt = await buildPlanMilestonePrompt( + mid, + "Test Milestone", + dir, + "standard", + ); + + expect(prompt).toContain("Milestone Context"); + expect(prompt).toContain("## Inlined Context"); + // Standard level includes roadmap template + expect(prompt).toContain("Roadmap"); + // Standard level includes plan template + expect(prompt).toContain("Slice Plan"); + }); + + test("plan_milestone_prompt_omits_db_artifacts_at_minimal_level", async () => { + const { dir, mid } = makeProject({ mid: "M911" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Minimal Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + + const prompt = await buildPlanMilestonePrompt( + mid, + "Minimal Milestone", + dir, + "minimal", + ); + + expect(prompt).toContain("## Inlined Context"); + // Minimal level skips project/requirements/decisions DB reads + // Still inlines context file + expect(prompt).toContain("Milestone Context"); + }); + + test("plan_milestone_prompt_inlines_queue_file_when_present", async () => { + const { dir, mid } = makeProject({ mid: "M912" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Queue Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + mkdirSync(join(dir, ".sf"), { recursive: true }); + writeFileSync(join(dir, ".sf", "QUEUE.md"), "M912 — next in queue\n"); + + const prompt = await buildPlanMilestonePrompt( + mid, + "Queue Milestone", + dir, + "minimal", + ); + + expect(prompt).toContain("Project Queue"); + }); +}); + +describe("buildReplanSlicePrompt v2", () => { + test("replan_slice_prompt_includes_roadmap_and_slice_plan", async () => { + const { dir, mid, sid } = makeProject({ mid: "M920", sid: "S01" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Replan Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + insertSlice({ + milestoneId: mid, + id: sid, + title: "Test Slice", + status: "active", + risk: "low", + depends: [], + demo: "Done.", + sequence: 1, + }); + + const prompt = await buildReplanSlicePrompt( + mid, + "Replan Milestone", + sid, + "Test Slice", + dir, + ); + + expect(prompt).toContain("## Inlined Context"); + expect(prompt).toContain("Milestone Roadmap"); + expect(prompt).toContain("Current Slice Plan"); + }); + + test("replan_slice_prompt_includes_blocker_summaries_when_present", async () => { + const { dir, mid, sid } = makeProject({ mid: "M921", sid: "S01" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Blocker Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + insertSlice({ + milestoneId: mid, + id: sid, + title: "Blocked Slice", + status: "active", + risk: "high", + depends: [], + demo: "Unblocked.", + sequence: 1, + }); + // Write a task summary with blocker_discovered: true + writeFileSync( + join( + dir, + ".sf", + "milestones", + mid, + "slices", + sid, + "tasks", + "T01-SUMMARY.md", + ), + "---\nblocker_discovered: true\nid: T01\n---\n# T01 Summary\n\nBlocked by external API.\n", + ); + + const prompt = await buildReplanSlicePrompt( + mid, + "Blocker Milestone", + sid, + "Blocked Slice", + dir, + ); + + expect(prompt).toContain("Blocker Task Summary"); + }); +}); + +describe("buildValidateMilestonePrompt v2", () => { + test("validate_milestone_prompt_includes_roadmap_and_gate_block", async () => { + const { dir, mid, sid } = makeProject({ mid: "M930", sid: "S01" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Validate Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + insertSlice({ + milestoneId: mid, + id: sid, + title: "Completed Slice", + status: "complete", + risk: "low", + depends: [], + demo: "Done.", + sequence: 1, + }); + writeFileSync( + join(dir, ".sf", "milestones", mid, `${mid}-SUMMARY.md`), + "# M930 Summary\n\nMilestone done.\n", + ); + + const prompt = await buildValidateMilestonePrompt( + mid, + "Validate Milestone", + dir, + "minimal", + ); + + expect(prompt).toContain("## Inlined Context"); + // Gate block must always be present per the milestone validation contract + expect(prompt).toContain("MV0"); + }); +}); + +describe("buildResearchSlicePrompt v2", () => { + test("research_slice_prompt_includes_roadmap_and_slice_context", async () => { + const { dir, mid, sid } = makeProject({ mid: "M940", sid: "S01" }); + openDatabase(join(dir, ".sf", "sf.db")); + insertMilestone({ + id: mid, + title: "Research Milestone", + status: "active", + planning: { vision: "Test.", successCriteria: [] }, + }); + insertSlice({ + milestoneId: mid, + id: sid, + title: "Research Slice", + status: "active", + risk: "low", + depends: [], + demo: "Research done.", + sequence: 1, + }); + + const prompt = await buildResearchSlicePrompt( + mid, + "Research Milestone", + sid, + "Research Slice", + dir, + ); + + expect(prompt).toContain("## Inlined Context"); + expect(prompt).toContain("Milestone Roadmap"); + }); +}); diff --git a/src/resources/extensions/sf/unit-context-manifest.js b/src/resources/extensions/sf/unit-context-manifest.js index e0a496888..7c3c358bf 100644 --- a/src/resources/extensions/sf/unit-context-manifest.js +++ b/src/resources/extensions/sf/unit-context-manifest.js @@ -46,6 +46,7 @@ export const ARTIFACT_KEYS = [ "slice-research", "slice-plan", "slice-summary", + "slice-summaries", // aggregated slice excerpts (complete/validate milestone) "slice-uat", "slice-assessment", // Task-scoped @@ -53,11 +54,19 @@ export const ARTIFACT_KEYS = [ "task-summary", "prior-task-summaries", "dependency-summaries", + "blocker-summaries", // tasks with blocker_discovered:true (replan-slice) // Project-scoped "requirements", "decisions", "project", "templates", + "queue", // .sf/QUEUE file (plan-milestone) + // Validation-scoped + "verification-classes", // milestone.verification_* from DB (validate-milestone) + "outstanding-items", // follow-ups aggregated from slice summaries (validate-milestone) + "previous-validation", // prior VALIDATION file for re-validation rounds + // History-scoped + "prior-milestone-summary", // previous milestone's SUMMARY (plan-milestone) ]; // ─── Manifests ──────────────────────────────────────────────────────────── // Phase 1 policy: every manifest encodes today's behavior. Skills = "all" @@ -154,16 +163,26 @@ export const UNIT_MANIFESTS = { codebaseMap: true, preferences: "active-only", tools: TOOLS_PLANNING, + // phase-anchor prepends the research-phase handoff block computed at call time. + prepend: ["phase-anchor"], artifacts: { + // Matches today's buildPlanMilestonePrompt inlining order. + // Templates are level-dependent (resolved imperatively in resolveArtifact). + // queue is conditional on .sf/QUEUE existence (null return = skipped). + // knowledge/graph are true computed artifacts (async, budgeted). inline: [ + "milestone-context", + "milestone-research", + "prior-milestone-summary", "project", "requirements", "decisions", - "milestone-research", + "queue", "templates", ], excerpt: [], onDemand: [], + computed: ["knowledge", "graph"], }, maxSystemPromptChars: COMMON_BUDGET_LARGE, }, @@ -215,16 +234,25 @@ export const UNIT_MANIFESTS = { preferences: "active-only", tools: TOOLS_PLANNING, artifacts: { + // Matches today's buildValidateMilestonePrompt inlining order. + // verification-classes, slice-summaries, outstanding-items, and + // previous-validation are pre-computed blocks returned from + // resolveArtifact — null when absent/empty. + // knowledge/graph are true computed artifacts (async, budgeted). inline: [ "roadmap", - "slice-summary", - "slice-uat", + "verification-classes", + "slice-summaries", + "outstanding-items", + "previous-validation", "requirements", "decisions", - "templates", + "project", + "milestone-context", ], excerpt: [], onDemand: [], + computed: ["knowledge", "graph"], }, maxSystemPromptChars: COMMON_BUDGET_LARGE, }, @@ -236,19 +264,22 @@ export const UNIT_MANIFESTS = { preferences: "active-only", tools: TOOLS_PLANNING, artifacts: { - // #4780 landed slice-summary as excerpt for this unit; phase 2 of - // the architecture will read this manifest as the source of truth - // and retire the special-case wiring in auto-prompts.ts. + // Matches today's buildCompleteMilestonePrompt inlining order. + // slice-summaries is a pre-computed block (parallel DB + file reads) + // returned from resolveArtifact. knowledge/graph are true computed + // artifacts (async, budgeted). inline: [ "roadmap", - "milestone-context", + "slice-summaries", "requirements", "decisions", "project", + "milestone-context", "templates", ], - excerpt: ["slice-summary"], - onDemand: ["slice-summary"], + excerpt: [], + onDemand: [], + computed: ["knowledge", "graph"], }, maxSystemPromptChars: COMMON_BUDGET_MEDIUM, }, @@ -260,15 +291,24 @@ export const UNIT_MANIFESTS = { codebaseMap: true, preferences: "active-only", tools: TOOLS_PLANNING, + // overrides prepends the active-overrides block computed at call time. + prepend: ["overrides"], artifacts: { + // Matches today's buildResearchSlicePrompt inlining order. + // dependency-summaries is a SEPARATE template variable (not inlined + // into inlinedContext). knowledge/graph are true computed artifacts. inline: [ "roadmap", + "milestone-context", + "slice-context", "milestone-research", - "dependency-summaries", + "decisions", + "requirements", "templates", ], excerpt: [], onDemand: [], + computed: ["knowledge", "graph"], }, maxSystemPromptChars: COMMON_BUDGET_MEDIUM, }, @@ -279,17 +319,27 @@ export const UNIT_MANIFESTS = { codebaseMap: true, preferences: "active-only", tools: TOOLS_PLANNING, + // overrides prepends the active-overrides block computed at call time. + prepend: ["overrides"], artifacts: { + // Matches today's renderSlicePrompt inlining order. + // dependency-summaries is a SEPARATE template variable (not inlined). + // prependBlocks (soft-scope hint / pre-exec failure) and the + // research-slice phase anchor are injected between prepend and inline + // by the builder (imperatively). knowledge/graph are true computed. + // decisions and requirements are skipped when level === "minimal". + // templates is level-dependent (plan always; task-plan if full). inline: [ "roadmap", + "slice-context", "slice-research", - "dependency-summaries", - "requirements", "decisions", + "requirements", "templates", ], excerpt: [], onDemand: [], + computed: ["knowledge", "graph"], }, maxSystemPromptChars: COMMON_BUDGET_LARGE, }, @@ -300,15 +350,23 @@ export const UNIT_MANIFESTS = { codebaseMap: true, preferences: "active-only", tools: TOOLS_PLANNING, + // overrides prepends the active-overrides block computed at call time. + prepend: ["overrides"], artifacts: { + // Same inlining order as plan-slice (shares renderSlicePrompt). + // The sketch-scope hard constraint is injected between prepend and + // inline by the builder (imperatively, via prependBlocks). inline: [ - "slice-plan", + "roadmap", + "slice-context", "slice-research", - "dependency-summaries", + "decisions", + "requirements", "templates", ], excerpt: [], onDemand: [], + computed: ["knowledge", "graph"], }, maxSystemPromptChars: COMMON_BUDGET_MEDIUM, }, @@ -319,16 +377,22 @@ export const UNIT_MANIFESTS = { codebaseMap: true, preferences: "active-only", tools: TOOLS_PLANNING, + // overrides prepends the active-overrides block computed at call time. + prepend: ["overrides"], artifacts: { + // Matches today's buildReplanSlicePrompt inlining order. + // blocker-summaries is a pre-computed block (task file scan) returned + // from resolveArtifact — null when no blocker task is found. inline: [ + "roadmap", + "slice-context", "slice-plan", - "slice-research", - "dependency-summaries", - "prior-task-summaries", - "templates", + "blocker-summaries", + "decisions", ], excerpt: [], onDemand: [], + computed: [], }, maxSystemPromptChars: COMMON_BUDGET_MEDIUM, },