From 5a57549591ff39cb2ea093ff41f39762dbe8d444 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 16 May 2026 19:01:50 +0200 Subject: [PATCH] feat(headless): autonomous mode auto-drains self-feedback triage queue first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, `sf headless autonomous` only dispatched units for the active milestone — never touched .sf/self-feedback.jsonl. The existing `sf headless triage --apply` was a manual operator path required for self-feedback to become actionable work. Defeats the "SF self-heals" thesis: 146 entries can sit in the queue indefinitely while the autonomous loop happily cranks on M005. Now: at autonomous startup (not on resume, not on initial bootstrap) SF calls handleTriage({ apply: true, max: 5 }) to drain the top-5 candidates from the triage queue before entering the dispatch loop. The bound at max=5 keeps the upfront cost bounded; remaining items process on the next session_start. The comment on the existing triage handler in headless.ts:917-921 explicitly acknowledged the gap — autonomous-loop followUp delivery was broken (sf-mp4rxkwb-l4baga). Wiring the deterministic triage path BEFORE the dispatch loop closes that gap. Opt-out: pass --skip-triage on the autonomous command (e.g. when debugging a specific milestone without backlog churn). Triage failures are non-fatal — they log a warning and the autonomous loop continues with its existing milestone dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/headless.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/headless.ts b/src/headless.ts index dc924b79a..134a0215a 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -634,6 +634,41 @@ async function runHeadlessOnce( options.command = "new-milestone"; options.chainAutonomous = true; options.contextText = buildAutoBootstrapContext(process.cwd()); + } else if (!options.commandArgs.includes("--skip-triage")) { + // Auto-drain the self-feedback triage queue before entering the + // autonomous dispatch loop. Without this, items in + // .sf/self-feedback.jsonl sit unprocessed and SF can only work on + // the active milestone — defeating the self-heal thesis. + // Comment on headless-triage at line 917-921 acknowledges that + // autonomous-loop followUp delivery was unreliable (sf-mp4rxkwb-l4baga), + // hence the deterministic operator path. This wires the deterministic + // path BEFORE the dispatch loop so autonomous == triage-then-dispatch. + // Skipped when resuming (resumeSession check above) or when the user + // passes --skip-triage to opt out (e.g. to debug a specific milestone + // without backlog churn). + try { + const { handleTriage } = await import("./headless-triage.js"); + if (!options.json) { + process.stderr.write( + "[headless] autonomous: draining self-feedback triage queue first...\n", + ); + } + await handleTriage(process.cwd(), { + apply: true, + json: !!options.json, + max: 5, // bound the up-front cost; remainder flushes on next session_start + }); + } catch (err) { + // Triage failure must not block autonomous mode — the loop's own + // dispatch will keep going; backlog will just stay until next run. + if (!options.json) { + process.stderr.write( + `[headless] autonomous: triage drain failed (non-fatal): ${ + err instanceof Error ? err.message : String(err) + }\n`, + ); + } + } } } const isNewMilestone = options.command === "new-milestone";