From a0b43c9a7bc5be098afd55125616773f874d720f Mon Sep 17 00:00:00 2001 From: Adrian Komorek Date: Tue, 5 May 2026 13:15:49 +0200 Subject: [PATCH 1/2] fix(smithy): validate sf agent start headless invocation upfront Headless workers/stewards spawned without --prompt or --resume have no input source and exit immediately, producing the cryptic 'Session exited before init' error. Refuse the invocation with a clear message that names the cause and lists the three fixes (--prompt, --resume, --mode interactive). Refs #61. --- .../smithy/src/cli/commands/agent.bun.test.ts | 35 +++++++++++++++++++ packages/smithy/src/cli/commands/agent.ts | 26 ++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/packages/smithy/src/cli/commands/agent.bun.test.ts b/packages/smithy/src/cli/commands/agent.bun.test.ts index d762c70b..2d0d840f 100644 --- a/packages/smithy/src/cli/commands/agent.bun.test.ts +++ b/packages/smithy/src/cli/commands/agent.bun.test.ts @@ -482,4 +482,39 @@ describe('agent disable / enable behavioural', () => { rmSync(tmpRoot, { recursive: true, force: true }); } }); + + test('start refuses headless invocation without --prompt or --resume', async () => { + const tmpRoot = mkdtempSync(join(tmpdir(), 'sf-start-validation-test-')); + const cwdBefore = process.cwd(); + try { + mkdirSync(join(tmpRoot, '.stoneforge'), { recursive: true }); + const dbPath = join(tmpRoot, '.stoneforge', 'stoneforge.db'); + const tmpBackend = createStorage({ path: dbPath, create: true }); + initializeSchema(tmpBackend); + const tmpApi = createOrchestratorAPI(tmpBackend); + const registered = await tmpApi.registerWorker({ + name: 'naked-worker', + workerMode: 'ephemeral', + createdBy: CREATOR, + }); + + process.chdir(tmpRoot); + try { + const result = await agentStartCommand.handler!( + [registered.id as unknown as string], + { db: dbPath } as never + ); + expect(result.exitCode).not.toBe(0); + const errMsg = String(result.error ?? ''); + expect(errMsg).toContain('headless'); + expect(errMsg).toContain('--prompt'); + expect(errMsg).toContain('--resume'); + expect(errMsg).toContain('--mode interactive'); + } finally { + process.chdir(cwdBefore); + } + } finally { + rmSync(tmpRoot, { recursive: true, force: true }); + } + }); }); diff --git a/packages/smithy/src/cli/commands/agent.ts b/packages/smithy/src/cli/commands/agent.ts index 6d69e943..b9947fd0 100644 --- a/packages/smithy/src/cli/commands/agent.ts +++ b/packages/smithy/src/cli/commands/agent.ts @@ -950,6 +950,30 @@ async function agentStartHandler( spawnMode = options.mode as 'headless' | 'interactive'; } + // Resolve effective spawn mode using the same rule as + // SpawnerServiceImpl.determineSpawnMode: director → interactive, + // worker/steward → headless, unless explicitly overridden. + const effectiveMode: 'headless' | 'interactive' = + spawnMode ?? (agentRole === 'director' ? 'interactive' : 'headless'); + + if (effectiveMode === 'headless' && !options.prompt && !options.resume) { + return failure( + [ + `Cannot start a headless ${agentRole} without an initial prompt.`, + `Headless agents read input via stdin; without --prompt or --resume the process exits immediately.`, + ``, + `Provide one of:`, + ` --prompt "..." Initial instructions for the agent`, + ` --resume Resume a previous session`, + ` --mode interactive Start as an interactive (PTY) session`, + ``, + `Note: in dispatch-driven workflows, the daemon spawns workers automatically when tasks are ready.`, + `Manual restart of ephemeral workers is usually unnecessary.`, + ].join('\n'), + ExitCode.VALIDATION + ); + } + // Spawn the agent const result = await spawner.spawn(id as EntityId, agentRole, { initialPrompt: options.prompt, @@ -1041,6 +1065,8 @@ Options: --provider Override agent provider for this session --model Override model for this session +For headless workers/stewards (default), one of --prompt, --resume, or --mode interactive is required. + Examples: sf agent start el-abc123 sf agent start el-abc123 --mode interactive From 3c7ef08ecba691a95ac907a1d229efe37501837d Mon Sep 17 00:00:00 2001 From: Adrian Komorek Date: Tue, 5 May 2026 13:22:26 +0200 Subject: [PATCH 2/2] chore: add changeset for sf agent start error clarity --- .changeset/sf-agent-start-clarity.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/sf-agent-start-clarity.md diff --git a/.changeset/sf-agent-start-clarity.md b/.changeset/sf-agent-start-clarity.md new file mode 100644 index 00000000..4f30da97 --- /dev/null +++ b/.changeset/sf-agent-start-clarity.md @@ -0,0 +1,10 @@ +--- +"@stoneforge/smithy": patch +--- + +fix: clearer error from `sf agent start` when headless invocation has no input source + +`sf agent start ` for a worker/steward in headless mode now refuses +upfront when invoked without --prompt or --resume, with an explanation +and the three fixes (--prompt, --resume, --mode interactive). Replaces +the cryptic "Session exited before init" error users previously saw.