fix(#829): enforce repository-wide runtime launch choke point
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
41
packages/coord/src/__tests__/runtime-launch-gate.test.ts
Normal file
41
packages/coord/src/__tests__/runtime-launch-gate.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveLaunchCommand } from '../runner.js';
|
||||
|
||||
describe('coord consequential-runtime launch gate', () => {
|
||||
it('routes default and direct configured Claude commands through mosaic', () => {
|
||||
expect(resolveLaunchCommand('claude', 'continue', undefined)).toEqual([
|
||||
'mosaic',
|
||||
'claude',
|
||||
'-p',
|
||||
'continue',
|
||||
]);
|
||||
expect(resolveLaunchCommand('claude', 'continue', ['claude', '-p', '{prompt}'])).toEqual([
|
||||
'mosaic',
|
||||
'claude',
|
||||
'-p',
|
||||
'continue',
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves an already-gated Claude command and rejects unknown launchers', () => {
|
||||
expect(
|
||||
resolveLaunchCommand('claude', 'continue', ['mosaic', 'yolo', 'claude', '{prompt}']),
|
||||
).toEqual(['mosaic', 'yolo', 'claude', 'continue']);
|
||||
expect(() => resolveLaunchCommand('claude', 'continue', ['custom-launcher'])).toThrow(
|
||||
/must use `mosaic claude`/,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not change the out-of-scope Codex command contract', () => {
|
||||
expect(resolveLaunchCommand('codex', 'continue', undefined)).toEqual([
|
||||
'codex',
|
||||
'-p',
|
||||
'continue',
|
||||
]);
|
||||
expect(resolveLaunchCommand('codex', 'continue', ['codex', '{prompt}'])).toEqual([
|
||||
'codex',
|
||||
'continue',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -179,32 +179,41 @@ function buildContinuationPrompt(params: {
|
||||
`3. Read \`${mission.scratchpadFile}\` for session history and decisions`,
|
||||
`4. Read \`${mission.tasksFile}\` for current task state`,
|
||||
'5. `git pull --rebase` to sync latest changes',
|
||||
`6. Launch runtime with \`${runtime} -p\``,
|
||||
`6. Launch runtime with \`mosaic ${runtime} -p\``,
|
||||
`7. Continue execution from task **${taskId}**`,
|
||||
'8. Follow Two-Phase Completion Protocol',
|
||||
`9. You are the SOLE writer of \`${mission.tasksFile}\``,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function resolveLaunchCommand(
|
||||
export function resolveLaunchCommand(
|
||||
runtime: 'claude' | 'codex',
|
||||
prompt: string,
|
||||
configuredCommand: string[] | undefined,
|
||||
): string[] {
|
||||
if (configuredCommand === undefined || configuredCommand.length === 0) {
|
||||
return [runtime, '-p', prompt];
|
||||
return runtime === 'claude' ? ['mosaic', 'claude', '-p', prompt] : [runtime, '-p', prompt];
|
||||
}
|
||||
|
||||
const hasPromptPlaceholder = configuredCommand.some((value) => value === '{prompt}');
|
||||
const withInterpolation = configuredCommand.map((value) =>
|
||||
value === '{prompt}' ? prompt : value,
|
||||
);
|
||||
const command = hasPromptPlaceholder ? withInterpolation : [...withInterpolation, prompt];
|
||||
|
||||
if (hasPromptPlaceholder) {
|
||||
return withInterpolation;
|
||||
if (runtime !== 'claude') return command;
|
||||
if (
|
||||
command[0] === 'mosaic' &&
|
||||
(command[1] === 'claude' || (command[1] === 'yolo' && command[2] === 'claude'))
|
||||
) {
|
||||
return command;
|
||||
}
|
||||
|
||||
return [...withInterpolation, prompt];
|
||||
if (command[0] === 'claude') {
|
||||
return ['mosaic', 'claude', ...command.slice(1)];
|
||||
}
|
||||
throw new Error(
|
||||
'Custom Claude task commands must use `mosaic claude` so lease registration cannot be bypassed.',
|
||||
);
|
||||
}
|
||||
|
||||
async function writeAtomicJson(filePath: string, payload: unknown): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user