diff --git a/packages/cli/src/hooks/maxsim-task-completed.ts b/packages/cli/src/hooks/maxsim-task-completed.ts index 0e0a9414..26a57dd4 100644 --- a/packages/cli/src/hooks/maxsim-task-completed.ts +++ b/packages/cli/src/hooks/maxsim-task-completed.ts @@ -10,6 +10,16 @@ * - Always handles errors gracefully — never crashes. */ +/** + * Output formats: + * - Exit 0: Allow task completion (all gates passed) + * - Exit 2 + stderr: Block completion — gates failed, agent must fix before completing + * - JSON stdout { continue: false, stopReason: "..." } + exit 0: Stop the teammate entirely + * + * Currently uses exit 2 (block + report) when test/build/lint gates fail. + * Use stopTeammate() from shared.ts for permanent stop scenarios. + */ + import * as fs from 'node:fs'; import * as path from 'node:path'; import { spawnSync } from 'node:child_process'; diff --git a/packages/cli/src/hooks/maxsim-teammate-idle.ts b/packages/cli/src/hooks/maxsim-teammate-idle.ts index 96aa0ecd..ef85b17d 100644 --- a/packages/cli/src/hooks/maxsim-teammate-idle.ts +++ b/packages/cli/src/hooks/maxsim-teammate-idle.ts @@ -9,6 +9,16 @@ * - Always handles errors gracefully — never crashes. */ +/** + * Output formats: + * - Exit 0: Allow the teammate to remain idle (no pending tasks) + * - Exit 2 + stderr: Block idle and redirect — "Pick up the next available task." + * - JSON stdout { continue: false, stopReason: "..." } + exit 0: Stop the teammate entirely + * + * Currently uses exit 2 (block + redirect) when pending tasks exist. + * Use stopTeammate() from shared.ts for permanent stop scenarios. + */ + import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; diff --git a/packages/cli/src/hooks/shared.ts b/packages/cli/src/hooks/shared.ts index 99d86136..8e2c19bd 100644 --- a/packages/cli/src/hooks/shared.ts +++ b/packages/cli/src/hooks/shared.ts @@ -103,13 +103,15 @@ export function playSound(soundFile: string): void { // For named system sounds (no extension) fall back to rundll32. const isWav = soundFile.toLowerCase().endsWith('.wav'); if (isWav) { + // Use double-quoted string which handles spaces and most special chars + const escaped = soundFile.replace(/"/g, '\\"'); spawnSync( 'powershell', [ '-NoProfile', '-NonInteractive', '-Command', - `$p='${soundFile.replace(/'/g, "''")}'; (New-Object System.Media.SoundPlayer $p).PlaySync()`, + `$p="${escaped}"; (New-Object System.Media.SoundPlayer $p).PlaySync()`, ], { stdio: 'ignore' }, ); @@ -135,3 +137,17 @@ export function playSound(soundFile: string): void { // Never crash on sound failure } } + +/** + * Send a JSON stop signal to terminate a teammate. + * Outputs the stop payload to stdout and exits cleanly. + * Use this when a teammate should be permanently stopped (not just blocked). + * + * For blocking (retry behavior), use: process.stderr.write(msg); process.exit(2); + * For stopping (permanent), use: stopTeammate(reason); + */ +export function stopTeammate(reason: string): never { + const payload = JSON.stringify({ continue: false, stopReason: reason }); + process.stdout.write(`${payload}\n`); + process.exit(0); +}