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"}.`,