diff --git a/src/help-text.ts b/src/help-text.ts index 4ad077a4a..e82abc2d8 100644 --- a/src/help-text.ts +++ b/src/help-text.ts @@ -225,6 +225,7 @@ const SUBCOMMAND_HELP: Record = { " query Machine snapshot: JSON state + next dispatch + costs (no LLM)", " usage Live LLM-provider usage snapshot (today: gemini-cli tier + per-model quota)", " reflect Assemble reflection corpus + render prompt for cross-corpus pattern analysis (--json for raw, --run to dispatch to gemini-cli, --model to override)", + " triage Render canonical self-feedback triage prompt for piping into a model (--list for digest, --json for structured, --max N to cap, --run to dispatch + write decisions to .sf/triage/decisions/, --model to override)", "", "new-milestone flags:", " --context Path to spec/PRD file (use '-' for stdin)", @@ -255,6 +256,11 @@ const SUBCOMMAND_HELP: Record = { " sf headless query Instant machine JSON state snapshot", " sf headless status uok UOK gate health table (last 24h)", " sf headless status uok --json UOK gate health as JSON", + " sf headless triage --list Self-feedback queue digest (impact↓ effort↑ ts↑)", + " sf headless triage | sf-some-model Pipe triage prompt to any model", + " sf headless triage --run Dispatch triage to default model + write decisions", + " sf headless reflect Render reflection prompt for piping", + " sf headless reflect --run Dispatch reflection + write report", "", "Exit codes: 0 = success, 1 = error/timeout, 10 = blocked, 11 = cancelled", ].join("\n"), diff --git a/src/resources/extensions/sf/auto/phases-pre-dispatch.js b/src/resources/extensions/sf/auto/phases-pre-dispatch.js index 522ca7b39..12edd8211 100644 --- a/src/resources/extensions/sf/auto/phases-pre-dispatch.js +++ b/src/resources/extensions/sf/auto/phases-pre-dispatch.js @@ -63,6 +63,7 @@ import { rollbackToCheckpoint, } from "../safety/git-checkpoint.js"; import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js"; +import { selectInlineFixCandidates } from "../self-feedback-drain.js"; import { recordSelfFeedback } from "../self-feedback.js"; import { checkpointWal, @@ -123,6 +124,35 @@ import { } from "./types.js"; import { closeoutAndStop, generateMilestoneReport, maybeFireProductAudit, shouldRunPlanningFlowGate } from "./phases-helpers.js"; +/** + * Surface the open self-feedback queue to the operator at idle-bail time. + * + * Why here (sf-mp4rxkwb-l4baga partial): the autonomous loop has historically + * exited silently on "no-active-milestone" / "milestone-complete" while the + * self-feedback queue still had unresolved entries. Operators didn't know + * the queue was waiting, so triage never ran. This helper makes the queue + * visible at the exact moment the loop is about to bail — the proper fix + * (triage as a real unit type) is the larger remaining slice of the parent + * entry. + * + * Best-effort: never throws, never blocks. If candidate selection fails or + * the basePath isn't a forge repo, this is a no-op. + */ +function surfaceSelfFeedbackQueueOnIdle(ctx, basePath, exitReason) { + try { + const candidates = selectInlineFixCandidates(basePath); + if (!Array.isArray(candidates) || candidates.length === 0) return; + const n = candidates.length; + const noun = n === 1 ? "entry" : "entries"; + ctx.ui.notify( + `Idle (${exitReason}) but ${n} self-feedback ${noun} still open. Run \`sf headless triage --list\` to scan, or \`sf headless triage --run\` to dispatch the canonical triage prompt.`, + "warning", + ); + } catch { + // Best-effort — never block the loop's bail path on a queue probe. + } +} + // ─── runPreDispatch ─────────────────────────────────────────────────────────── /** * Phase 1: Pre-dispatch — resource guard, health gate, state derivation, @@ -616,6 +646,7 @@ export async function runPreDispatch(ic, loopState) { `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`, ); } + surfaceSelfFeedbackQueueOnIdle(ctx, s.basePath, "no-active-milestone"); debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" }); deps.emitJournalEvent({ ts: new Date().toISOString(), @@ -709,6 +740,7 @@ export async function runPreDispatch(ic, loopState) { ); deps.logCmuxEvent(prefs, `Milestone ${mid} complete.`, "success"); await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`); + surfaceSelfFeedbackQueueOnIdle(ctx, s.basePath, "milestone-complete"); debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" }); deps.emitJournalEvent({ ts: new Date().toISOString(),