diff --git a/src/resources/extensions/sf/auto/loop.js b/src/resources/extensions/sf/auto/loop.js index 8befae5a8..b69eaa196 100644 --- a/src/resources/extensions/sf/auto/loop.js +++ b/src/resources/extensions/sf/auto/loop.js @@ -275,6 +275,63 @@ async function drainSleeptimeQueue(basePath) { } } } +/** + * Auto-triage root TODO.md if it contains raw dump notes. + * + * Purpose: check for untriaged human dump notes at the start of each autonomous + * cycle and promote them into reviewable artifacts (eval candidates, harness + * gaps, memory requirements, docs/tests, implementation tasks) before the agent + * picks the next unit. This prevents raw TODO.md notes from being treated as + * durable instructions. + * + * Consumer: autoLoop between-unit boundary. + * + * @param {string} basePath + * @param {number} iteration + * @param {object} deps + * @param {object} ctx + */ +async function autoTriageTodo(basePath, iteration, deps, ctx) { + const todoPath = join(basePath, "TODO.md"); + if (!existsSync(todoPath)) return; + const raw = readFileSync(todoPath, "utf-8"); + // Fast-path: skip if TODO.md matches the empty template + const _EMPTY_TODO = "# TODO\n\nDump anything here.\n"; + if (raw.trim() === _EMPTY_TODO.trim()) return; + // Skip if there's no real dump content (just header + empty prompt) + const body = raw + .replace(/\r\n/g, "\n") + .split("\n") + .filter((line, idx) => { + if (idx === 0 && line.trim().toLowerCase() === "# todo") return false; + if (line.trim() === "Dump anything here.") return false; + return true; + }) + .join("\n") + .trim(); + if (!body) return; + // Lazy-import triage function to avoid loading the full commands-todo module + // at startup. It pulls in sf-db, crypto, etc. + const { buildTodoTriageLLMCall, triageTodoDump } = await import( + "../commands-todo.js" + ); + const llmCall = buildTodoTriageLLMCall(ctx); + if (!llmCall) return; + const result = await triageTodoDump(basePath, llmCall, { + clear: true, + backlog: true, + }); + if (result.skipped) return; + ctx.ui.notify( + `TODO.md auto-triage: ${result.backlogItemsAdded} backlog items, ${result.result.eval_candidates.length} eval candidates`, + "info", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: `auto-triage-${iteration}`, + }, + ); +} + /** * Wrap a phase function with a timeout. Rejects with an Error whose message * starts with "phase-timeout:" so the blanket catch can handle it specially. @@ -573,6 +630,15 @@ export async function autoLoop(ctx, pi, s, deps) { } catch { /* best-effort — never block autonomous loop on consolidation */ } + // ── Auto-triage TODO.md dump inbox ── + // If root TODO.md contains raw dump notes beyond the empty template, + // triage them into reviewable artifacts before picking the next unit. + // This runs cheaply (one hash check + one LLM call only when content exists). + try { + await autoTriageTodo(s.basePath, iteration, deps, ctx); + } catch { + /* best-effort — never block autonomous loop on triage */ + } try { // ── Blanket try/catch: one bad iteration must not kill the session const prefs = deps.loadEffectiveSFPreferences()?.preferences;