From ab57548f2bbb611f11cb978c29122c216f075ef3 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Wed, 29 Apr 2026 20:37:56 +0200 Subject: [PATCH] fix: keep skipped tasks out of slice verification --- src/resources/extensions/sf/auto-prompts.ts | 29 ++++++- .../extensions/sf/prompts/complete-slice.md | 2 +- .../complete-slice-skipped-tasks.test.ts | 87 +++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/resources/extensions/sf/tests/complete-slice-skipped-tasks.test.ts diff --git a/src/resources/extensions/sf/auto-prompts.ts b/src/resources/extensions/sf/auto-prompts.ts index 5941aa888..55d7cd826 100644 --- a/src/resources/extensions/sf/auto-prompts.ts +++ b/src/resources/extensions/sf/auto-prompts.ts @@ -63,7 +63,11 @@ import { resolveSkillDiscoveryMode, } from "./preferences.js"; import { inlineTemplate, loadPrompt } from "./prompt-loader.js"; -import { getPendingGatesForTurn } from "./sf-db.js"; +import { + getPendingGatesForTurn, + getSliceTasks, + isDbAvailable, +} from "./sf-db.js"; import { warnIfManifestHasMissingSkills } from "./skill-manifest.js"; import { formatDecisionsCompact, @@ -2422,6 +2426,28 @@ export async function buildCompleteSlicePrompt( ): Promise { const inlineLevel = level ?? resolveInlineLevel(); + const skippedTaskBlock = (() => { + try { + if (!isDbAvailable()) return null; + const skippedTasks = getSliceTasks(mid, sid).filter( + (t) => t.status === "skipped", + ); + if (skippedTasks.length === 0) return null; + const rows = skippedTasks.map( + (t) => + `- ${t.id}: ${t.title || "(untitled)"} — skipped by SF state; do not execute its task-level verification during slice closeout.`, + ); + return [ + "### Skipped Tasks", + "These tasks are closed as skipped. Treat their original verification commands as non-applicable for this closeout and record the gap in the slice summary/UAT instead of running them.", + "", + ...rows, + ].join("\n"); + } catch { + return null; + } + })(); + // #4782 phase 3: complete-slice migrated through composer. Manifest // declares [roadmap, slice-context, slice-plan, requirements, // prior-task-summaries, templates]. Overrides prepend and knowledge @@ -2468,6 +2494,7 @@ export async function buildCompleteSlicePrompt( }), ); const blocks = entries.filter((b): b is string => b !== null); + if (skippedTaskBlock) blocks.push(skippedTaskBlock); return blocks.length > 0 ? blocks.join("\n\n---\n\n") : null; } case "templates": { diff --git a/src/resources/extensions/sf/prompts/complete-slice.md b/src/resources/extensions/sf/prompts/complete-slice.md index c6d1ec840..7746619cd 100644 --- a/src/resources/extensions/sf/prompts/complete-slice.md +++ b/src/resources/extensions/sf/prompts/complete-slice.md @@ -23,7 +23,7 @@ All relevant context has been preloaded below — the slice plan, all task summa Then: 1. Use the **Slice Summary** and **UAT** output templates from the inlined context above 2. {{skillActivation}} -3. Run all slice-level verification checks defined in the slice plan. All must pass before marking the slice done. If any fail, fix them first. Task artifacts use a **flat file layout** directly inside `tasks/` (for example `T01-SUMMARY.md`, `T02-SUMMARY.md`) rather than per-task subdirectories. If you need to count or re-read task summaries during verification, use `find .sf/milestones/{{milestoneId}}/slices/{{sliceId}}/tasks -name "*-SUMMARY.md"` or `ls .sf/milestones/{{milestoneId}}/slices/{{sliceId}}/tasks/*-SUMMARY.md`. Never use `tasks/*/SUMMARY.md` — that glob expects subdirectories that do not exist. +3. Run all applicable slice-level verification checks defined in the slice plan. All applicable checks must pass before marking the slice done. If any fail, fix them first. If the inlined context includes **Skipped Tasks**, do not execute verification that belongs only to those skipped tasks; record the evidence gap in the slice summary and UAT instead. Task artifacts use a **flat file layout** directly inside `tasks/` (for example `T01-SUMMARY.md`, `T02-SUMMARY.md`) rather than per-task subdirectories. If you need to count or re-read task summaries during verification, use `find .sf/milestones/{{milestoneId}}/slices/{{sliceId}}/tasks -name "*-SUMMARY.md"` or `ls .sf/milestones/{{milestoneId}}/slices/{{sliceId}}/tasks/*-SUMMARY.md`. Never use `tasks/*/SUMMARY.md` — that glob expects subdirectories that do not exist. 4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections. 5. Address every gate listed in the **Gates to Close** section above — each gate maps to a specific slice-summary section the handler inspects (for example, Q8 maps to **Operational Readiness**: health signal, failure signal, recovery procedure, and monitoring gaps). Leaving a section empty records the gate as `omitted`. 6. If this slice produced evidence that a requirement changed status (Active → Validated, Active → Deferred, etc.), call `sf_requirement_update` with the requirement ID, updated `status`, and `validation` evidence. Do NOT write `.sf/REQUIREMENTS.md` directly — the engine renders it from the database. diff --git a/src/resources/extensions/sf/tests/complete-slice-skipped-tasks.test.ts b/src/resources/extensions/sf/tests/complete-slice-skipped-tasks.test.ts new file mode 100644 index 000000000..6cc0871f3 --- /dev/null +++ b/src/resources/extensions/sf/tests/complete-slice-skipped-tasks.test.ts @@ -0,0 +1,87 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { buildCompleteSlicePrompt } from "../auto-prompts.ts"; +import { + closeDatabase, + insertMilestone, + insertSlice, + insertTask, + openDatabase, +} from "../sf-db.ts"; + +test("complete-slice prompt treats skipped tasks as non-executable gaps", async (t) => { + const tmp = mkdtempSync(join(tmpdir(), "sf-complete-skip-")); + t.after(() => { + closeDatabase(); + rmSync(tmp, { recursive: true, force: true }); + }); + + mkdirSync(join(tmp, ".sf"), { recursive: true }); + openDatabase(join(tmp, ".sf", "sf.db")); + + const sliceDir = join(tmp, ".sf", "milestones", "M001", "slices", "S05"); + mkdirSync(join(sliceDir, "tasks"), { recursive: true }); + writeFileSync( + join(tmp, ".sf", "milestones", "M001", "M001-ROADMAP.md"), + "# M001\n", + ); + writeFileSync( + join(sliceDir, "S05-PLAN.md"), + [ + "# S05", + "", + "## Verification", + "Run docker compose up -d and poll staging health.", + "", + ].join("\n"), + ); + writeFileSync( + join(sliceDir, "tasks", "T01-SUMMARY.md"), + ["---", "id: T01", "---", "", "# T01", "Existing tests passed.", ""].join( + "\n", + ), + ); + + insertMilestone({ id: "M001", title: "Test Milestone" }); + insertSlice({ + id: "S05", + milestoneId: "M001", + title: "Final Verification", + status: "active", + }); + insertTask({ + id: "T01", + sliceId: "S05", + milestoneId: "M001", + title: "Run focused tests", + status: "complete", + }); + insertTask({ + id: "T02", + sliceId: "S05", + milestoneId: "M001", + title: "Validate docker-compose staging stack starts cleanly", + status: "skipped", + }); + + const prompt = await buildCompleteSlicePrompt( + "M001", + "Test Milestone", + "S05", + "Final Verification", + tmp, + ); + + assert.match(prompt, /### Skipped Tasks/); + assert.match( + prompt, + /T02: Validate docker-compose staging stack starts cleanly/, + ); + assert.match( + prompt, + /do not execute verification that belongs only to those skipped tasks/, + ); +});