fix(worker): add JSON status output for hook framework (#655)
* fix(worker): add JSON status output for hook framework (#638) Adds JSON output before all process.exit() calls in the start command so Claude Code's hook framework can track worker startup progress. - Add exitWithStatus() helper function - Output {"continue":true,"suppressOutput":true,"status":"ready"|"error"} - Maintains exit code 0 for Windows Terminal compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(worker): add unit tests for buildStatusOutput function Extract buildStatusOutput() as pure function for testability and add comprehensive unit tests validating JSON structure for hook framework. Tests cover: - Ready/error status variants - Required fields (continue, suppressOutput, status) - Optional message field inclusion logic - Edge cases (empty strings, special characters, long messages) - JSON serialization roundtrip Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(worker): add CLI output capture tests for start command Add integration tests that spawn actual worker-service start command and verify JSON output matches hook framework contract. Tests: - Valid JSON with required fields (continue, suppressOutput, status) - JSON structure matches expected format when worker healthy - Skipped placeholders for error scenarios requiring complex setup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(worker): add Claude Code hook framework compatibility tests Add contract tests documenting the hook framework requirements: - Exit code 0 (Windows Terminal compatibility) - JSON output on stdout (not stderr) - Valid JSON structure with required fields - Critical 'continue: true' for Claude Code to proceed - 'suppressOutput: true' for transcript mode These tests serve as living documentation of the hook output contract, explaining both WHAT and WHY for each requirement. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,30 @@ import { LogsRoutes } from './worker/http/routes/LogsRoutes.js';
|
||||
// Re-export updateCursorContextForProject for SDK agents
|
||||
export { updateCursorContextForProject };
|
||||
|
||||
/**
|
||||
* Build JSON status output for hook framework communication.
|
||||
* This is a pure function extracted for testability.
|
||||
*
|
||||
* @param status - 'ready' for successful startup, 'error' for failures
|
||||
* @param message - Optional error message (only included when provided)
|
||||
* @returns JSON object with continue, suppressOutput, status, and optionally message
|
||||
*/
|
||||
export interface StatusOutput {
|
||||
continue: true;
|
||||
suppressOutput: true;
|
||||
status: 'ready' | 'error';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function buildStatusOutput(status: 'ready' | 'error', message?: string): StatusOutput {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true,
|
||||
status,
|
||||
...(message && { message })
|
||||
};
|
||||
}
|
||||
|
||||
export class WorkerService {
|
||||
private server: Server;
|
||||
private startTime: number = Date.now();
|
||||
@@ -622,6 +646,14 @@ async function main() {
|
||||
const command = process.argv[2];
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Helper for JSON status output in 'start' command
|
||||
// Exit code 0 ensures Windows Terminal doesn't keep tabs open
|
||||
function exitWithStatus(status: 'ready' | 'error', message?: string): never {
|
||||
const output = buildStatusOutput(status, message);
|
||||
console.log(JSON.stringify(output));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'start': {
|
||||
if (await waitForHealth(port, 1000)) {
|
||||
@@ -636,14 +668,12 @@ async function main() {
|
||||
const freed = await waitForPortFree(port, getPlatformTimeout(15000));
|
||||
if (!freed) {
|
||||
logger.error('SYSTEM', 'Port did not free up after shutdown for version mismatch restart', { port });
|
||||
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
|
||||
// The wrapper/plugin will handle restart logic if needed
|
||||
process.exit(0);
|
||||
exitWithStatus('error', 'Port did not free after version mismatch restart');
|
||||
}
|
||||
removePidFile();
|
||||
} else {
|
||||
logger.info('SYSTEM', 'Worker already running and healthy');
|
||||
process.exit(0);
|
||||
exitWithStatus('ready');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,21 +683,17 @@ async function main() {
|
||||
const healthy = await waitForHealth(port, getPlatformTimeout(15000));
|
||||
if (healthy) {
|
||||
logger.info('SYSTEM', 'Worker is now healthy');
|
||||
process.exit(0);
|
||||
exitWithStatus('ready');
|
||||
}
|
||||
logger.error('SYSTEM', 'Port in use but worker not responding to health checks');
|
||||
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
|
||||
// The wrapper/plugin will handle restart logic if needed
|
||||
process.exit(0);
|
||||
exitWithStatus('error', 'Port in use but worker not responding');
|
||||
}
|
||||
|
||||
logger.info('SYSTEM', 'Starting worker daemon');
|
||||
const pid = spawnDaemon(__filename, port);
|
||||
if (pid === undefined) {
|
||||
logger.error('SYSTEM', 'Failed to spawn worker daemon');
|
||||
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
|
||||
// The wrapper/plugin will handle restart logic if needed
|
||||
process.exit(0);
|
||||
exitWithStatus('error', 'Failed to spawn worker daemon');
|
||||
}
|
||||
|
||||
writePidFile({ pid, port, startedAt: new Date().toISOString() });
|
||||
@@ -676,13 +702,11 @@ async function main() {
|
||||
if (!healthy) {
|
||||
removePidFile();
|
||||
logger.error('SYSTEM', 'Worker failed to start (health check timeout)');
|
||||
// Exit gracefully: Windows Terminal won't keep tab open on exit 0
|
||||
// The wrapper/plugin will handle restart logic if needed
|
||||
process.exit(0);
|
||||
exitWithStatus('error', 'Worker failed to start (health check timeout)');
|
||||
}
|
||||
|
||||
logger.info('SYSTEM', 'Worker started successfully');
|
||||
process.exit(0);
|
||||
exitWithStatus('ready');
|
||||
}
|
||||
|
||||
case 'stop': {
|
||||
|
||||
Reference in New Issue
Block a user