Skip to content
Open
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
9 changes: 4 additions & 5 deletions src/cli/services/__tests__/hookDispatchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ describe('HookDispatchService session lifecycle', () => {
expect(stopResult.output).toEqual({ continue: true });
});

it('emits Stop workflow guidance when a PREVC workflow is active', async () => {
it('emits Codex Stop workflow guidance as a systemMessage when a PREVC workflow is active', async () => {
const workflowService = await WorkflowService.create(tempDir);
await workflowService.init({
name: 'feature-x',
Expand All @@ -615,12 +615,11 @@ describe('HookDispatchService session lifecycle', () => {

expect(stopResult.exitCode).toBe(0);
expect(stopResult.output).toMatchObject({
hookSpecificOutput: {
hookEventName: 'Stop',
additionalContext: expect.stringContaining('feature-x'),
},
continue: true,
systemMessage: expect.stringContaining('feature-x'),
});
Comment on lines +618 to 620

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

We should prevent this to happen to other harnesses as well, this is why it was a generic message, we should map to Codex output and so others instead of only covering it here for Codex, because we support other tools, @cursoragent can you help us here?

expect(stopResult.output).not.toHaveProperty('source');
expect(stopResult.output).not.toHaveProperty('hookSpecificOutput');
});

it.each(['Stop', 'SubagentStop'])(
Expand Down
25 changes: 23 additions & 2 deletions src/integrations/__tests__/hookRoundTrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,26 @@ describe('hook mapper unit tests', () => {
});
});

it('emits Stop workflow guidance as a Codex systemMessage', () => {
const output = mapCodexResponse(codexStopFixture, {
ok: true,
tool: 'workflow-guide',
source: 'codex',
result: {
kind: 'json',
data: {
workflow: { active: true },
excerpt: 'dotcontext workflow guide:\nWorkflow "feature-x" - phase V.',
},
},
});

expect(output).toEqual({
continue: true,
systemMessage: expect.stringContaining('feature-x'),
});
});

it('suppresses Stop guidance during reentry', () => {
const output = mapCodexResponse(
{ ...codexStopFixture, sessionEndActive: true },
Expand Down Expand Up @@ -535,8 +555,9 @@ describe('hook round-trip integrations', () => {
},
(output: ReturnType<typeof mapCodexResponse>) => {
expect(output).not.toHaveProperty('source');
expect(output.hookSpecificOutput?.hookEventName).toBe('Stop');
expect(output.hookSpecificOutput?.additionalContext).toContain('feature-x');
expect(output).not.toHaveProperty('hookSpecificOutput');
expect(output.continue).toBe(true);
expect(output.systemMessage).toContain('feature-x');
},
],
])('%s envelope round-trips through adapter and response mapper', async (
Expand Down
23 changes: 22 additions & 1 deletion src/integrations/codex/hooks/mapCodexResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,35 @@ import type { CodexHookInput } from './mapCodexEvent';

export type CodexHookOutput = Omit<HostHookOutput, 'source'>;

function mapCodexStopOutput(output: CodexHookOutput): CodexHookOutput {
const { hookSpecificOutput, ...rest } = output;
const additionalContext = hookSpecificOutput?.additionalContext;

if (!additionalContext) {
return output;
}

return {
...rest,
continue: rest.continue ?? true,
systemMessage: rest.systemMessage ?? additionalContext,
};
}

export function mapCodexResponse(
event: CodexHookInput,
response: HarnessHookResponse
): CodexHookOutput {
const hookEventName =
typeof event.hook_event_name === 'string' ? event.hook_event_name : 'unknown';

return mapHostHookResponseForSource('codex', hookEventName, response, {
const output = mapHostHookResponseForSource('codex', hookEventName, response, {
suppressAdditionalContext: isSessionEndReentry(event),
}) as CodexHookOutput;

if (hookEventName === 'Stop') {
return mapCodexStopOutput(output);
}

return output;
}
Loading