diff --git a/src/resources/extensions/sf/auto-start.ts b/src/resources/extensions/sf/auto-start.ts index fd407539e..738988b5b 100644 --- a/src/resources/extensions/sf/auto-start.ts +++ b/src/resources/extensions/sf/auto-start.ts @@ -550,8 +550,12 @@ export async function bootstrapAutoSession( // Auto mode: autonomously map the codebase and create milestones // without waiting for user answers. Uses discuss-headless prompt. ctx.ui.notify("No milestones found. Bootstrapping from codebase analysis.", "info"); - const { dispatchHeadlessBootstrap } = await import("./guided-flow.js"); - await dispatchHeadlessBootstrap(ctx, pi, base); + const { bootstrapNewMilestone, dispatchNewMilestoneDiscuss, injectTodoContext } = await import("./guided-flow.js"); + const nextId = bootstrapNewMilestone(base); + await dispatchNewMilestoneDiscuss(ctx, pi, base, nextId, { + auto: true, + preamble: injectTodoContext(base, "This is an autonomous session."), + }); invalidateAllCaches(); const postState = await deriveState(base); @@ -593,8 +597,12 @@ export async function bootstrapAutoSession( const hasContext = !!(contextFile && (await loadFile(contextFile))); if (!hasContext) { ctx.ui.notify(`Milestone ${mid} has no context. Bootstrapping from codebase analysis.`, "info"); - const { dispatchHeadlessBootstrap } = await import("./guided-flow.js"); - await dispatchHeadlessBootstrap(ctx, pi, base); + const { bootstrapNewMilestone, dispatchNewMilestoneDiscuss, injectTodoContext } = await import("./guided-flow.js"); + const nextId = bootstrapNewMilestone(base); + await dispatchNewMilestoneDiscuss(ctx, pi, base, nextId, { + auto: true, + preamble: injectTodoContext(base, "This is an autonomous session."), + }); invalidateAllCaches(); const postState = await deriveState(base); diff --git a/src/resources/extensions/sf/guided-flow.ts b/src/resources/extensions/sf/guided-flow.ts index e931c430d..31d3c7079 100644 --- a/src/resources/extensions/sf/guided-flow.ts +++ b/src/resources/extensions/sf/guided-flow.ts @@ -73,6 +73,30 @@ export { } from "./guided-flow-queue.js"; import { logWarning } from "./workflow-logger.js"; +// ─── Todo/Spec File Detection ──────────────────────────────────────────────── + +const TODO_FILE_NAMES = ["todo.md", "TODO.md", "SPEC.md", "spec.md"]; + +/** + * If a todo/spec file exists at the project root, read and delete it, then + * append its contents to `preamble` so any discuss or bootstrap prompt treats + * it as the primary specification. Returns the (possibly enriched) preamble. + * + * Called identically in auto-mode bootstrap and interactive discuss — one flow. + */ +export function injectTodoContext(basePath: string, preamble: string): string { + for (const fname of TODO_FILE_NAMES) { + const fpath = join(basePath, fname); + if (!existsSync(fpath)) continue; + try { + const content = readFileSync(fpath, "utf-8").slice(0, 8000); + try { unlinkSync(fpath); } catch { /* non-fatal */ } + return `${preamble}\n\n### ${fname} (user-provided specification)\n\n${content}`; + } catch { /* non-fatal — fall through */ } + } + return preamble; +} + // ─── ID Generation with Reservation ───────────────────────────────────────── /** @@ -556,7 +580,7 @@ async function prepareAndBuildDiscussPrompt( * Bootstrap a .sf/ project from scratch for headless use. * Ensures git repo, .sf/ structure, gitignore, and preferences all exist. */ -function bootstrapProject(basePath: string): void { +export function bootstrapProject(basePath: string): void { if (!nativeIsRepo(basePath) || isInheritedRepo(basePath)) { const mainBranch = loadEffectiveSFPreferences()?.preferences?.git?.main_branch || "main"; nativeInit(basePath, mainBranch); @@ -609,63 +633,69 @@ export async function showHeadlessMilestoneCreation( /** - * Headless auto-mode bootstrap: dispatch the discuss-headless unit without - * triggering pendingAutoStartMap (caller is already inside bootstrapAutoSession). - * - * Seeds the prompt by auto-reading README.md / package.json / go.mod from the - * project root so the model can map the codebase and plan autonomously. + * Single discuss-dispatch entry point for new milestones. + * auto=true → headless prompt, rootFiles seed, plan-milestone workflow, no pendingAutoStartMap + * auto=false → discuss prompt with preparation, discuss-milestone workflow, sets pendingAutoStartMap */ -export async function dispatchHeadlessBootstrap( +export async function dispatchNewMilestoneDiscuss( ctx: ExtensionCommandContext, pi: ExtensionAPI, basePath: string, -): Promise { + nextId: string, + options: { auto: boolean; preamble: string; step?: boolean }, +): Promise { + if (options.auto) { + const seedParts: string[] = [options.preamble, ""]; + const rootFiles = ["README.md", "README.rst", "package.json", "go.mod", "Cargo.toml", "pyproject.toml"]; + for (const fname of rootFiles) { + try { + const fpath = join(basePath, fname); + if (existsSync(fpath)) { + const content = readFileSync(fpath, "utf-8").slice(0, 4000); + seedParts.push(`### ${fname}\n\n${content}`); + } + } catch { /* non-fatal */ } + } + seedParts.push( + "", + [ + "Autonomously analyze this codebase to plan what needs to be built or improved.", + "", + "Investigation approach:", + "1. Scout the codebase deeply — use rg, find, ast-grep, and file reads to understand structure, patterns, and tech stack", + "2. Run existing tests (go test, cargo test, npm test, etc.) to measure current quality", + "3. Web search for industry best practices for this type of software — testing strategies, architecture patterns, operational requirements", + "4. Research any libraries, frameworks, or external services involved — get current API docs and constraints", + "5. Identify gaps: missing tests, incomplete features, error handling, observability, security, documentation", + "", + "Goal: define milestones that represent the highest-value work to make this software production-ready, well-tested, and complete.", + "Use all available models and research tools. Treat your findings as the specification.", + ].join("\n"), + ); + const prompt = buildHeadlessDiscussPrompt(nextId, seedParts.join("\n"), basePath); + // Do NOT set pendingAutoStartMap — caller (bootstrapAutoSession) manages the loop + await dispatchWorkflow(pi, prompt, "sf-run", ctx, "plan-milestone"); + } else { + pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: options.step, createdAt: Date.now() }); + const prompt = await prepareAndBuildDiscussPrompt(ctx, pi, nextId, options.preamble, basePath); + await dispatchWorkflow(pi, prompt, "sf-run", ctx, "discuss-milestone"); + } +} + +/** + * Bootstrap a new milestone: ensure .sf/ structure exists, clear stale + * reservations, generate + reserve the next milestone ID, and create its + * slice directory. Returns the reserved ID. + * + * Call this before `dispatchNewMilestoneDiscuss` when starting from auto-start. + */ +export function bootstrapNewMilestone(basePath: string): string { clearReservedMilestoneIds(); bootstrapProject(basePath); - const existingIds = findMilestoneIds(basePath); const prefs = loadEffectiveSFPreferences(); const nextId = nextMilestoneIdReserved(existingIds, prefs?.preferences?.unique_milestone_ids ?? false); - - const milestoneDir = join(sfRoot(basePath), "milestones", nextId, "slices"); - mkdirSync(milestoneDir, { recursive: true }); - - // Build seed context from project root files - const seedParts: string[] = [ - "This is an autonomous headless session. No specification document was provided.", - "", - ]; - const rootFiles = ["README.md", "README.rst", "package.json", "go.mod", "Cargo.toml", "pyproject.toml"]; - for (const fname of rootFiles) { - try { - const fpath = join(basePath, fname); - if (existsSync(fpath)) { - const content = readFileSync(fpath, "utf-8").slice(0, 4000); - seedParts.push(`### ${fname}\n\n${content}`); - } - } catch { /* non-fatal */ } - } - seedParts.push( - "", - [ - "Autonomously analyze this codebase to plan what needs to be built or improved.", - "", - "Investigation approach:", - "1. Scout the codebase deeply — use rg, find, ast-grep, and file reads to understand structure, patterns, and tech stack", - "2. Run existing tests (go test, cargo test, npm test, etc.) to measure current quality", - "3. Web search for industry best practices for this type of software — testing strategies, architecture patterns, operational requirements", - "4. Research any libraries, frameworks, or external services involved — get current API docs and constraints", - "5. Identify gaps: missing tests, incomplete features, error handling, observability, security, documentation", - "", - "Goal: define milestones that represent the highest-value work to make this software production-ready, well-tested, and complete.", - "Use all available models and research tools. Treat your findings as the specification.", - ].join("\n"), - ); - const seedContext = seedParts.join("\n"); - - const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath); - // Do NOT set pendingAutoStartMap — caller (bootstrapAutoSession) handles the loop - await dispatchWorkflow(pi, prompt, "sf-run", ctx, "plan-milestone"); + mkdirSync(join(sfRoot(basePath), "milestones", nextId, "slices"), { recursive: true }); return nextId; } @@ -1465,12 +1495,11 @@ export async function showWorkflowEntry( const isFirst = milestoneIds.length === 0; if (isFirst) { - // First ever — skip wizard, just ask directly - pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() }); - await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, - `New project, milestone ${nextId}. Do NOT read or explore .sf/ — it's empty scaffolding.`, - basePath - ), "sf-run", ctx, "discuss-milestone"); + await dispatchNewMilestoneDiscuss(ctx, pi, basePath, nextId, { + auto: false, + preamble: injectTodoContext(basePath, `New project, milestone ${nextId}. Do NOT read or explore .sf/ — it's empty scaffolding.`), + step: stepMode, + }); } else { const choice = await showNextAction(ctx, { title: "SF — Singularity Forge", @@ -1487,11 +1516,11 @@ export async function showWorkflowEntry( }); if (choice === "new_milestone") { - pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() }); - await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, - `New milestone ${nextId}.`, - basePath - ), "sf-run", ctx, "discuss-milestone"); + await dispatchNewMilestoneDiscuss(ctx, pi, basePath, nextId, { + auto: false, + preamble: injectTodoContext(basePath, `New milestone ${nextId}.`), + step: stepMode, + }); } } return; @@ -1526,11 +1555,11 @@ export async function showWorkflowEntry( const uniqueMilestoneIds = !!loadEffectiveSFPreferences()?.preferences?.unique_milestone_ids; const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds); - pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() }); - await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, - `New milestone ${nextId}.`, - basePath - ), "sf-run", ctx, "discuss-milestone"); + await dispatchNewMilestoneDiscuss(ctx, pi, basePath, nextId, { + auto: false, + preamble: injectTodoContext(basePath, `New milestone ${nextId}.`), + step: stepMode, + }); } else if (choice === "status") { const { fireStatusViaCommand } = await import("./commands.js"); await fireStatusViaCommand(ctx);