feat(headless): autonomous mode auto-drains self-feedback triage queue first

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) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-16 19:01:50 +02:00
parent 8d0f41436b
commit 5a57549591

View file

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