From ed85252fc5fdbe9a9aba162f1b562c837e115bfa Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 01:36:29 +0200 Subject: [PATCH] feat: plumb /sf autonomous full + add docstrings on the auto-command path `/sf autonomous full` (or `--full`) plumbs through to AutoSession.fullAutonomy, to be consumed at milestone-complete to skip the human-review pause and auto-merge + chain to the next milestone. Git revert is the safety net (see ADR-019/021 conversation on autonomy and reversibility). Plumbing path: - commands/handlers/auto.ts: parses `full` / `--full` modifier, threads fullAutonomy through launchAuto options - commands/catalog.ts: completion entries for `full` and `--full` - auto.ts: startAuto and startAutoDetached accept fullAutonomy in options; startAuto pins it on the session up-front so resume paths preserve it - auto/session.ts: AutoSession.fullAutonomy field with full docstring Behavior change is staged: the milestone-complete consumer that auto-merges and chains is intentionally not in this commit (parallel session is active in auto-post-unit.ts and auto/loop.ts; will land in a follow-up). Also adds JSDoc to the functions on the touched path: - handleAutoCommand (full command-family doc) - launchAuto (headless vs detached routing) - startAutoDetached (fire-and-forget rationale, why it diverges from startAuto) - AutoSession.fullAutonomy (full inline doc) Typecheck clean. Co-Authored-By: Claude Sonnet 4.6 --- src/resources/extensions/sf/auto.ts | 32 +++++++++++++++++ src/resources/extensions/sf/auto/session.ts | 7 ++++ .../extensions/sf/commands/catalog.ts | 4 +++ .../extensions/sf/commands/handlers/auto.ts | 35 ++++++++++++++++++- 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/sf/auto.ts b/src/resources/extensions/sf/auto.ts index 17a5427bc..43a90a5b8 100644 --- a/src/resources/extensions/sf/auto.ts +++ b/src/resources/extensions/sf/auto.ts @@ -307,6 +307,23 @@ function normalizeSessionFilePath(raw: unknown): string | null { return candidate; } +/** + * Fire-and-forget wrapper around {@link startAuto} for the interactive shell. + * + * The interactive REPL cannot block on the long-running auto loop, so the + * command handler calls this synchronously: the loop runs in the background, + * UI events fire through `ctx.ui.notify`, and any startup failure surfaces as + * an error notification rather than an unhandled rejection. + * + * The headless code path uses {@link startAuto} directly because `sf headless` + * needs to await loop completion to set its exit code. + * + * @param ctx Extension command context (for notify, status, widgets) + * @param pi Extension API (for engine calls and sessions) + * @param base Project root path + * @param verboseMode Verbose execution output + * @param options Optional run modifiers — see {@link startAuto} + */ export function startAutoDetached( ctx: ExtensionCommandContext, pi: ExtensionAPI, @@ -316,6 +333,11 @@ export function startAutoDetached( step?: boolean; interrupted?: InterruptedSessionAssessment; milestoneLock?: string | null; + /** + * Full-autonomy mode: auto-merge milestone branches and chain to the + * next milestone without pausing for human review. See `/sf autonomous full`. + */ + fullAutonomy?: boolean; }, ): void { void startAuto(ctx, pi, base, verboseMode, options).catch((err) => { @@ -1467,6 +1489,11 @@ export async function startAuto( step?: boolean; interrupted?: InterruptedSessionAssessment; milestoneLock?: string | null; + /** + * Full-autonomy mode: auto-merge milestone branches and chain to the + * next milestone without pausing for human review. See `/sf autonomous full`. + */ + fullAutonomy?: boolean; }, ): Promise { if (s.active) { @@ -1485,6 +1512,11 @@ export async function startAuto( const requestedStepMode = options?.step ?? false; const interruptedAssessment = options?.interrupted ?? null; + // Pin full-autonomy on the session up-front. The branches below that set + // stepMode never override fullAutonomy — it carries through resume paths, + // fresh starts, and crash recovery so the milestone-complete code path can + // consult it without re-reading command-line options. + s.fullAutonomy = options?.fullAutonomy === true; if (options?.milestoneLock !== undefined) { s.sessionMilestoneLock = options.milestoneLock ?? null; } diff --git a/src/resources/extensions/sf/auto/session.ts b/src/resources/extensions/sf/auto/session.ts index 2ce58b821..ab9d08a92 100644 --- a/src/resources/extensions/sf/auto/session.ts +++ b/src/resources/extensions/sf/auto/session.ts @@ -83,6 +83,13 @@ export class AutoSession { active = false; paused = false; stepMode = false; + /** + * Full-autonomy mode: auto-merge milestone branches and chain to the next + * milestone without pausing for human review. Set from the `/sf autonomous full` + * command line. Consumed at milestone-complete to skip the review pause and + * auto-trigger merge + next-milestone dispatch. Git revert is the safety net. + */ + fullAutonomy = false; verbose = false; activeEngineId: string | null = null; activeRunDir: string | null = null; diff --git a/src/resources/extensions/sf/commands/catalog.ts b/src/resources/extensions/sf/commands/catalog.ts index df94ba2d1..228b4a97a 100644 --- a/src/resources/extensions/sf/commands/catalog.ts +++ b/src/resources/extensions/sf/commands/catalog.ts @@ -163,10 +163,14 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly SfCommandDefinition[] = [ const NESTED_COMPLETIONS: CompletionMap = { autonomous: [ + { cmd: "full", desc: "Auto-merge milestones; chain end-to-end without review" }, + { cmd: "--full", desc: "Auto-merge milestones; chain end-to-end without review" }, { cmd: "--verbose", desc: "Show detailed execution output" }, { cmd: "--debug", desc: "Enable debug logging" }, ], auto: [ + { cmd: "full", desc: "Auto-merge milestones; chain end-to-end without review" }, + { cmd: "--full", desc: "Auto-merge milestones; chain end-to-end without review" }, { cmd: "--verbose", desc: "Show detailed execution output" }, { cmd: "--debug", desc: "Enable debug logging" }, ], diff --git a/src/resources/extensions/sf/commands/handlers/auto.ts b/src/resources/extensions/sf/commands/handlers/auto.ts index d87cd9a51..8fe2fc1d9 100644 --- a/src/resources/extensions/sf/commands/handlers/auto.ts +++ b/src/resources/extensions/sf/commands/handlers/auto.ts @@ -60,6 +60,24 @@ export function parseMilestoneTarget(input: string): { return { milestoneId: match[1], rest }; } +/** + * Dispatch entry point for the auto-mode command family. + * + * Handles `/sf auto`, `/sf autonomous`, `/sf next`, `/sf stop`, `/sf pause`, and + * their flag variants. Returns `true` when the command was recognised and + * routed (caller stops searching), `false` when the command isn't auto-related. + * + * Recognised flags on autonomous/auto: + * - `full` or `--full` — full-autonomy mode (auto-merge + chain milestones) + * - `--verbose` — verbose execution output + * - `--debug` — enable debug logging via SF_DEBUG + * - `M001` (positional) — milestone target lock (only run that milestone) + * - `--yolo=` — yolo seed; bootstraps a fresh milestone from a brief + * + * The handler validates milestone targets exist, gates remote sessions, then + * dispatches via `launchAuto` (which routes between headless and detached + * spawn paths). + */ export async function handleAutoCommand( trimmed: string, ctx: ExtensionCommandContext, @@ -71,11 +89,20 @@ export async function handleAutoCommand( trimmed === "autonomous" || trimmed.startsWith("autonomous "); + /** + * Route an auto-mode launch through either the headless (in-process) or + * detached (spawned subprocess) entry point depending on `SF_HEADLESS`. + * + * Headless mode runs the auto loop in the current process (used by CI, + * tests, and `sf headless`); detached mode forks a long-running child so + * the interactive shell stays responsive while auto-mode runs. + */ const launchAuto = async ( verboseMode: boolean, options?: { step?: boolean; milestoneLock?: string | null; + fullAutonomy?: boolean; }, ): Promise => { if (process.env.SF_HEADLESS === "1") { @@ -123,6 +150,11 @@ export async function handleAutoCommand( parseMilestoneTarget(afterYolo); const verboseMode = afterMilestone.includes("--verbose"); const debugMode = afterMilestone.includes("--debug"); + // `/sf autonomous full` (or `--full`): full-autonomy mode — auto-merges + // milestone branches and chains to the next milestone without pausing + // for human review. Git revert is the safety net. + const fullAutonomy = + /\bfull\b/.test(afterMilestone) || afterMilestone.includes("--full"); if (debugMode) enableDebug(projectRoot()); if (!(await guardRemoteSession(ctx, pi))) return true; @@ -159,9 +191,10 @@ export async function handleAutoCommand( } else if (milestoneId) { await launchAuto(verboseMode, { milestoneLock: milestoneId, + fullAutonomy, }); } else { - await launchAuto(verboseMode); + await launchAuto(verboseMode, fullAutonomy ? { fullAutonomy } : undefined); } return true; }