sf: unify milestone discuss dispatch + todo.md seed injection
Replace separate dispatchHeadlessBootstrap with one flow:
- dispatchNewMilestoneDiscuss({ auto }) — auto=true uses headless
prompt + rootFiles seed, no pendingAutoStartMap; auto=false uses
discuss prompt with preparation, sets pendingAutoStartMap
- bootstrapNewMilestone() — project setup + ID reservation, called
directly from bootstrapAutoSession instead of the old wrapper
- injectTodoContext() — reads and deletes todo.md/TODO.md/SPEC.md at
project root, injects content as spec into any preamble; called
identically in auto and interactive flows
Removes dispatchHeadlessBootstrap entirely. auto-start.ts now calls
the primitives directly. All three showWorkflowEntry new-milestone
sites use dispatchNewMilestoneDiscuss({ auto: false }).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
67d25f95f2
commit
c940ebc16f
2 changed files with 106 additions and 69 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
nextId: string,
|
||||
options: { auto: boolean; preamble: string; step?: boolean },
|
||||
): Promise<void> {
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue