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 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 01:36:29 +02:00
parent 356d1d1f99
commit ed85252fc5
4 changed files with 77 additions and 1 deletions

View file

@ -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<void> {
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;
}

View file

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

View file

@ -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" },
],

View file

@ -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=<file>` 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<void> => {
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;
}