Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/cli/src/hooks/maxsim-task-completed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/hooks/maxsim-teammate-idle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/src/hooks/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()`,
],
Comment on lines +106 to 115
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PowerShell -Command string now uses a double-quoted literal ($p="..."). In PowerShell, double-quoted strings perform variable/subexpression expansion (e.g. $, $(), and backtick escapes), so valid Windows path characters like $ or ` can be misinterpreted and break playback (and it also makes the “handles special chars” comment inaccurate). Consider avoiding interpolation entirely by passing the path as a separate argument and reading it via $args[0] (or revert to a single-quoted literal with proper escaping).

Copilot uses AI. Check for mistakes.
{ stdio: 'ignore' },
Comment on lines +106 to 116
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows-specific quoting/PowerShell command behavior in playSound() changed, but the existing hooks/shared.ts unit tests don’t assert the PowerShell argument string (and won’t exercise the win32 branch on non-Windows runners). Consider adding a unit test that mocks os.platform() to win32 and asserts the spawnSync('powershell', [...]) args so quoting regressions are caught.

Copilot uses AI. Check for mistakes.
);
Expand All @@ -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);
Comment on lines +151 to +152
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stopTeammate() writes to stdout and immediately calls process.exit(0). process.exit() can terminate the process before stdout is flushed when stdout is a pipe, which risks dropping the JSON control payload. To make the stop signal reliable, exit only after the write callback fires (or end stdout) instead of exiting synchronously right after write().

Suggested change
process.stdout.write(`${payload}\n`);
process.exit(0);
process.stdout.write(`${payload}\n`, () => {
process.exit(0);
});

Copilot uses AI. Check for mistakes.
}
Comment on lines +149 to +153
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stopTeammate() introduces new hook-control behavior but isn’t covered by the existing hooks/shared.ts unit tests. Adding a small test that spies on process.stdout.write and process.exit would prevent regressions in the JSON payload format and exit semantics.

Copilot uses AI. Check for mistakes.
Loading