From 8870d84012b140ee888075971033c88071047362 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Fri, 27 Mar 2026 17:04:31 -0600 Subject: [PATCH] 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) --- src/headless-events.ts | 3 ++ src/resources/extensions/gsd/guided-flow.ts | 1 + src/tests/headless-events.test.ts | 36 +++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src/headless-events.ts b/src/headless-events.ts index d2199ef64..190ac99a1 100644 --- a/src/headless-events.ts +++ b/src/headless-events.ts @@ -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': diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 2cda89d72..2ad23e473 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -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); diff --git a/src/tests/headless-events.test.ts b/src/tests/headless-events.test.ts index 12a6e8ca0..60c0695e7 100644 --- a/src/tests/headless-events.test.ts +++ b/src/tests/headless-events.test.ts @@ -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) +})