feat(auto): auto-triage TODO.md on each autonomous cycle

- Add autoTriageTodo() helper that checks root TODO.md for raw dump notes
  beyond the empty template before each autonomous cycle
- Lazy-imports buildTodoTriageLLMCall + triageTodoDump from commands-todo.js
  to avoid startup overhead
- Triage results written to DB backlog with clear=true + backlog=true
- Best-effort: never blocks autonomous loop on triage failure
- Fast-path skips when TODO.md is empty template or doesn't exist

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-15 03:19:13 +02:00
parent ca7ff554c3
commit 72c3811a7b

View file

@ -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;