fix: separate headless transport from autonomous mode
This commit is contained in:
parent
4f3020da21
commit
a1fd6cfc05
7 changed files with 36 additions and 43 deletions
|
|
@ -189,9 +189,12 @@ Enable with `github.enabled: true` in preferences. Requires `gh` CLI installed a
|
|||
`sf headless` runs `/sf` commands without a TUI — designed for CI, cron jobs, and scripted automation. It spawns a child process in RPC mode, auto-responds to interactive prompts, detects completion, and exits with meaningful exit codes.
|
||||
|
||||
```bash
|
||||
# Run autonomous mode (default)
|
||||
# Show headless command help
|
||||
sf headless
|
||||
|
||||
# Run autonomous mode without the TUI
|
||||
sf headless autonomous
|
||||
|
||||
# Run a single unit
|
||||
sf headless next
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ function printNonTtyErrorAndExit(
|
|||
);
|
||||
if (includeWebHint) {
|
||||
process.stderr.write(
|
||||
"[sf] sf headless Autonomous mode without TUI\n",
|
||||
"[sf] sf headless autonomous Autonomous mode without TUI\n",
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
|
|
@ -561,7 +561,7 @@ if (cliFlags.messages[0] === "sessions") {
|
|||
cliFlags._selectedSessionPath = selected.path;
|
||||
}
|
||||
|
||||
// `sf headless` — run autonomous mode without TUI
|
||||
// `sf headless ...` — run explicit /sf commands without the TUI
|
||||
if (cliFlags.messages[0] === "headless") {
|
||||
await ensureRtkBootstrap();
|
||||
// Sync bundled resources before headless runs (#3471). Without this,
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
timeout: 300_000,
|
||||
json: false,
|
||||
outputFormat: "text",
|
||||
command: "autonomous",
|
||||
command: "help",
|
||||
commandExplicit: false,
|
||||
commandArgs: [],
|
||||
};
|
||||
|
|
@ -488,6 +488,11 @@ async function runHeadlessOnce(
|
|||
): Promise<{ exitCode: number; interrupted: boolean }> {
|
||||
let interrupted = false;
|
||||
const startTime = Date.now();
|
||||
if (options.command === "help") {
|
||||
const { printSubcommandHelp } = await import("./help-text.js");
|
||||
printSubcommandHelp("headless", process.env.SF_VERSION || "0.0.0");
|
||||
return { exitCode: EXIT_SUCCESS, interrupted: false };
|
||||
}
|
||||
if (options.command === "autonomous" && !options.resumeSession) {
|
||||
bootstrapProject(process.cwd());
|
||||
if (!hasMilestones(process.cwd())) {
|
||||
|
|
@ -521,29 +526,12 @@ async function runHeadlessOnce(
|
|||
options.command === "discuss" ||
|
||||
options.command === "plan";
|
||||
|
||||
// Auto-mode defaults to supervised: wait for user input instead of exiting on questions
|
||||
// This is the desired behavior - auto should wait, not exit on blocked
|
||||
// Can be disabled via --no-supervised or preferences.auto_supervisor.supervised_mode: false
|
||||
// Headless uses the same SF command flow as TUI, but it is unattended by
|
||||
// default: questions are answered by headless policy, not by waiting for a
|
||||
// separate orchestrator. Opt into external question forwarding with
|
||||
// --supervised when a caller really wants stdin/stdout mediation.
|
||||
if (options.command === "autonomous" && options.supervised === undefined) {
|
||||
// Check preferences for default
|
||||
try {
|
||||
const { loadEffectiveSFPreferences } = await import(
|
||||
"./resources/extensions/sf/preferences.js"
|
||||
);
|
||||
const prefs = loadEffectiveSFPreferences();
|
||||
// Default to true unless explicitly set to false in preferences
|
||||
const autoSupervisor = prefs?.preferences
|
||||
? (prefs.preferences as Record<string, unknown>)["auto_supervisor"]
|
||||
: undefined;
|
||||
options.supervised =
|
||||
autoSupervisor !== undefined
|
||||
? (((autoSupervisor as Record<string, unknown>)?.[
|
||||
"supervised_mode"
|
||||
] as boolean | undefined) ?? true)
|
||||
: true;
|
||||
} catch {
|
||||
options.supervised = true;
|
||||
}
|
||||
options.supervised = false;
|
||||
}
|
||||
|
||||
if (isAutoMode && options.timeout === 300_000) {
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
headless: [
|
||||
"Usage: sf headless [flags] [command] [args...]",
|
||||
"",
|
||||
"Run /sf commands without the TUI. Default command: autonomous",
|
||||
"Run /sf commands without the TUI. Pass an explicit command such as autonomous, next, status, or query.",
|
||||
"",
|
||||
"Flags:",
|
||||
" --timeout N Overall timeout in ms (default: 300000)",
|
||||
|
|
@ -186,7 +186,7 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
" --events <types> Filter JSONL output to specific event types (comma-separated)",
|
||||
"",
|
||||
"Commands:",
|
||||
" autonomous Run all queued product units continuously (default)",
|
||||
" autonomous Run all queued product units continuously",
|
||||
" next Run one unit",
|
||||
" status Show progress dashboard",
|
||||
" new-milestone Create a milestone from a specification document",
|
||||
|
|
@ -204,7 +204,8 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
" stream-json Stream JSONL events to stdout in real time (same as --json)",
|
||||
"",
|
||||
"Examples:",
|
||||
" sf headless Run /sf autonomous",
|
||||
" sf headless Show this help",
|
||||
" sf headless autonomous Run /sf autonomous without the TUI",
|
||||
" sf headless next Run one unit",
|
||||
" sf headless --output-format json autonomous Structured JSON result on stdout",
|
||||
" sf headless --json status Machine-readable JSONL stream",
|
||||
|
|
@ -288,7 +289,7 @@ export function printHelp(version: string): void {
|
|||
" autonomous [args] Run autonomous mode without TUI (pipeable)\n",
|
||||
);
|
||||
process.stdout.write(
|
||||
" headless [cmd] [args] Run /sf commands without TUI (default: autonomous)\n",
|
||||
" headless [cmd] [args] Run explicit /sf commands without TUI\n",
|
||||
);
|
||||
process.stdout.write(
|
||||
" graph <subcommand> Manage knowledge graph (build, query, status, diff)\n",
|
||||
|
|
|
|||
|
|
@ -82,9 +82,10 @@ if (
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
// Fast-path invalid headless invocations before importing cli.ts. These paths
|
||||
// are commonly used by smoke tests and orchestrators; they should return a
|
||||
// clear diagnostic without paying extension/resource startup cost.
|
||||
// Fast-path invalid headless flags before importing cli.ts. Project-state
|
||||
// validation belongs to headless.ts because bare `sf headless` is help, `init`
|
||||
// is allowed before .sf exists, and explicit commands decide their own state
|
||||
// requirements.
|
||||
if (firstArg === "headless") {
|
||||
for (let i = 1; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
|
|
@ -98,13 +99,6 @@ if (firstArg === "headless") {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length === 1 && !existsSync(join(process.cwd(), ".sf"))) {
|
||||
process.stderr.write(
|
||||
"[headless] Error: No .sf/ directory found. Run 'sf headless init' or pass an explicit headless subcommand.\n",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule due-items banner — lightweight check before heavy imports
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
timeout: 300_000,
|
||||
json: false,
|
||||
outputFormat: "text",
|
||||
command: "autonomous",
|
||||
command: "help",
|
||||
commandArgs: [],
|
||||
};
|
||||
|
||||
|
|
@ -126,6 +126,13 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
|
||||
// ─── --output-format flag parsing ──────────────────────────────────────────
|
||||
|
||||
test("bare headless defaults to help, not autonomous", () => {
|
||||
const opts = parseHeadlessArgs(["node", "sf", "headless"]);
|
||||
assert.equal(opts.command, "help");
|
||||
assert.equal(opts.auto, undefined);
|
||||
assert.deepEqual(opts.commandArgs, []);
|
||||
});
|
||||
|
||||
test("--output-format text sets outputFormat to text", () => {
|
||||
const opts = parseHeadlessArgs([
|
||||
"node",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
const options: HeadlessOptions = {
|
||||
timeout: 300_000,
|
||||
json: false,
|
||||
command: "auto",
|
||||
command: "help",
|
||||
commandArgs: [],
|
||||
};
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ function parseHeadlessArgs(argv: string[]): HeadlessOptions {
|
|||
}
|
||||
} else if (!positionalStarted) {
|
||||
positionalStarted = true;
|
||||
options.command = arg === "autonomous" ? "auto" : arg;
|
||||
options.command = arg;
|
||||
} else {
|
||||
options.commandArgs.push(arg);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue