fix(headless): match "completed" status from RPC v2 in exit code mapper

mapStatusToExitCode only handled "complete" but RPC v2 emits "completed",
causing all headless sessions to falsely timeout and restart.

Also emits milestone-ready notification in checkAutoStartAfterDiscuss so
headless parent can detect and chain into auto-mode.

Closes #2914

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-27 17:04:31 -06:00
parent b5715c20bb
commit 8870d84012
3 changed files with 40 additions and 0 deletions

View file

@ -20,6 +20,8 @@ export const EXIT_CANCELLED = 11
* Map a headless session status string to its standardized exit code.
*
* success 0
* complete 0
* completed 0
* error 1
* timeout 1
* blocked 10
@ -31,6 +33,7 @@ export function mapStatusToExitCode(status: string): number {
switch (status) {
case 'success':
case 'complete':
case 'completed':
return EXIT_SUCCESS
case 'error':
case 'timeout':

View file

@ -178,6 +178,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
try { unlinkSync(manifestPath); } catch { /* may not exist for single-milestone */ }
pendingAutoStart = null;
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "info");
startAuto(ctx, pi, basePath, false, { step }).catch((err) => {
ctx.ui.notify(`Auto-start failed: ${getErrorMessage(err)}`, "error");
if (process.env.GSD_DEBUG) console.error('[gsd] auto start error:', err);

View file

@ -149,3 +149,39 @@ test('empty filter blocks all events', () => {
assert.ok(!shouldEmit('agent_end'))
assert.ok(!shouldEmit('message_update'))
})
import { mapStatusToExitCode, EXIT_SUCCESS, EXIT_ERROR, EXIT_BLOCKED, EXIT_CANCELLED } from '../headless-events.js'
// ─── mapStatusToExitCode ─────────────────────────────────────────────────
test('mapStatusToExitCode: "complete" returns EXIT_SUCCESS', () => {
assert.equal(mapStatusToExitCode('complete'), EXIT_SUCCESS)
})
test('mapStatusToExitCode: "completed" returns EXIT_SUCCESS', () => {
assert.equal(mapStatusToExitCode('completed'), EXIT_SUCCESS)
})
test('mapStatusToExitCode: "success" returns EXIT_SUCCESS', () => {
assert.equal(mapStatusToExitCode('success'), EXIT_SUCCESS)
})
test('mapStatusToExitCode: "error" returns EXIT_ERROR', () => {
assert.equal(mapStatusToExitCode('error'), EXIT_ERROR)
})
test('mapStatusToExitCode: "timeout" returns EXIT_ERROR', () => {
assert.equal(mapStatusToExitCode('timeout'), EXIT_ERROR)
})
test('mapStatusToExitCode: "blocked" returns EXIT_BLOCKED', () => {
assert.equal(mapStatusToExitCode('blocked'), EXIT_BLOCKED)
})
test('mapStatusToExitCode: "cancelled" returns EXIT_CANCELLED', () => {
assert.equal(mapStatusToExitCode('cancelled'), EXIT_CANCELLED)
})
test('mapStatusToExitCode: unknown status returns EXIT_ERROR', () => {
assert.equal(mapStatusToExitCode('unknown'), EXIT_ERROR)
})