From 7e1631618afa42f7ad1f5572a376395673f3c6c7 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 14 May 2026 21:39:47 +0200 Subject: [PATCH] fix(self-feedback-drain): route inline-fix dispatch via 'sf headless triage --apply' when SF_HEADLESS=1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing dispatch used pi.sendMessage to queue a chat followUp. That works in interactive sf sessions but no chat agent is listening in 'sf headless' / autonomous flows — the message is queued and never delivered, leaving the high/critical blocker active on every iteration. When SF_HEADLESS=1, spawn the same triage-decider → review-code pipeline (via the already-shipped 'sf headless triage --apply' subprocess) instead. The autonomous loop then sees resolved entries via DB on the next gate check, no chat agent required. Forge-only: the dispatcher still only operates in the SF repo itself — `readAllSelfFeedback` for non-forge repos returns the upstream-feedback log (SF developer work), which must not be auto-dispatched from inside consumer projects. Documented that constraint inline. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../extensions/sf/self-feedback-drain.js | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/sf/self-feedback-drain.js b/src/resources/extensions/sf/self-feedback-drain.js index b461e152e..8c3f9dcc0 100644 --- a/src/resources/extensions/sf/self-feedback-drain.js +++ b/src/resources/extensions/sf/self-feedback-drain.js @@ -7,7 +7,7 @@ * * Consumer: session_start hook in bootstrap/register-hooks.ts. */ -import { execFileSync } from "node:child_process"; +import { execFileSync, spawn } from "node:child_process"; import { existsSync, mkdirSync, @@ -207,6 +207,11 @@ function effectiveEffort(entry) { } export function selectInlineFixCandidates(basePath) { + // Forge-only: in consumer projects, `readAllSelfFeedback` returns the + // global upstream-feedback log (~/.sf/agent/upstream-feedback.jsonl), + // which lists SF-developer work items. We must not auto-dispatch a + // triage agent against those from inside a consumer project — that + // would let any downstream project mark SF's own work as resolved. if (!isForgeRepo(basePath)) return []; return readAllSelfFeedback(basePath) .filter((entry) => { @@ -306,6 +311,38 @@ export function dispatchSelfFeedbackInlineFixIfNeeded(basePath, ctx, pi) { // Mode transition is best-effort } writeClaim(basePath, ids); + // Headless / autonomous surfaces have no interactive chat agent listening + // for `pi.sendMessage` — the followUp gets queued but never delivered, so + // the blocker persists across iterations. Route through `sf headless triage + // --apply` instead, which dispatches the same triage-decider → review-code + // pipeline through SF's own subprocess machinery (router-resolved model, + // watchdog, trust gate). Fire-and-forget: the autonomous loop will see the + // resolved entries via DB on the next iteration's gate check. + if (process.env.SF_HEADLESS === "1") { + ctx.ui.notify( + `Dispatching self-feedback inline fix via 'sf headless triage --apply' for ${ids.length} high/critical entr${ids.length === 1 ? "y" : "ies"} (headless surface).`, + "warning", + ); + const sfBin = process.env.SF_BIN_PATH || process.argv[1]; + if (!sfBin) { + writeFailedClaim(basePath, ids, "SF_BIN_PATH unavailable"); + return 0; + } + try { + const child = spawn( + process.execPath, + [sfBin, "headless", "triage", "--apply", "--json"], + { cwd: basePath, stdio: ["ignore", "ignore", "ignore"], detached: true }, + ); + child.on("error", (err) => { + writeFailedClaim(basePath, ids, getErrorMessage(err)); + }); + child.unref(); + } catch (err) { + writeFailedClaim(basePath, ids, getErrorMessage(err)); + } + return candidates.length; + } const prompt = buildInlineFixPrompt(candidates); ctx.ui.notify( `Queueing self-feedback inline fix for ${ids.length} high/critical entr${ids.length === 1 ? "y" : "ies"}.`,