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
87 changes: 50 additions & 37 deletions src/sessionSummaryBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('OpenCode session summary bridge', () => {
error: 'private error',
},
'/work/repo',
{ ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }
{ ORGX_OPENCODE_WORK_CAPTURE: 'bounded' }
);
expect(result).toEqual({
session_id: 'session-1',
Expand Down Expand Up @@ -119,9 +119,14 @@ describe('OpenCode session summary bridge', () => {
expect(sanitizeOpenCodePayload(payload, '/work/repo').prompt).toBeUndefined();
expect(
sanitizeOpenCodePayload(payload, '/work/repo', {
ORGX_SESSION_WORK_EPISODE_CAPTURE: '1',
ORGX_OPENCODE_WORK_CAPTURE: 'bounded',
}).prompt
).toBe('Only retain this when bounded capture is enabled.');
expect(
sanitizeOpenCodePayload(payload, '/work/repo', {
ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded',
}).prompt
).toBeUndefined();
});

it('bounds explicitly enabled prompt capture to 600 Unicode characters', () => {
Expand All @@ -130,7 +135,7 @@ describe('OpenCode session summary bridge', () => {
const sanitized = sanitizeOpenCodePayload(
{ sessionID: 'session-bounded', prompt },
'/work/repo',
{ ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }
{ ORGX_OPENCODE_WORK_CAPTURE: 'bounded' }
);

expect(Array.from(sanitized.prompt as string)).toHaveLength(600);
Expand Down Expand Up @@ -165,6 +170,7 @@ describe('OpenCode session summary bridge', () => {
argv: [
'--event=RunEnd',
'--source_client=opencode',
'--work_episode_capture=metadata-only',
],
env: { PATH: process.env.PATH },
stdinText: JSON.stringify({
Expand All @@ -184,42 +190,49 @@ describe('OpenCode session summary bridge', () => {
}
});

it('leaves Work Episode consent to the explicit Wizard environment setting', async () => {
const dir = mkdtempSync(join(tmpdir(), 'orgx-opencode-bridge-'));
const hookPath = join(dir, 'orgx-session-summary.mjs');
await writeFile(hookPath, 'export async function main() {}\n', 'utf8');
const main = vi.fn(async () => ({ ok: true }));
try {
await bridgeOpenCodeSessionSummary({
nativeEvent: 'chat.message',
payload: {
sessionID: 'session-consent',
prompt: 'Retain this bounded intent.',
},
directory: '/work/repo',
hookPath,
env: {
PATH: process.env.PATH,
ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded',
},
importHook: async () => ({ main }),
});
it.each([
['an ambient generic variable', { ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }, 'metadata-only'],
['the OpenCode-specific opt-in', { ORGX_OPENCODE_WORK_CAPTURE: 'bounded' }, 'bounded'],
] as const)(
'pins the capture mode explicitly under %s (plan v3 §5.10)',
async (_label, extraEnv, expected) => {
const dir = mkdtempSync(join(tmpdir(), 'orgx-opencode-bridge-'));
const hookPath = join(dir, 'orgx-session-summary.mjs');
await writeFile(hookPath, 'export async function main() {}\n', 'utf8');
const main = vi.fn(async () => ({ ok: true }));
try {
await bridgeOpenCodeSessionSummary({
nativeEvent: 'chat.message',
payload: {
sessionID: 'session-consent',
prompt: 'Retain this bounded intent.',
},
directory: '/work/repo',
hookPath,
env: { PATH: process.env.PATH, ...extraEnv },
importHook: async () => ({ main }),
});

expect(main).toHaveBeenCalledTimes(1);
expect(main.mock.calls[0][0].argv).toEqual([
'--event=UserPromptSubmit',
'--source_client=opencode',
]);
expect(main.mock.calls[0][0].env.ORGX_SESSION_WORK_EPISODE_CAPTURE).toBe(
'bounded'
);
expect(JSON.parse(main.mock.calls[0][0].stdinText).prompt).toBe(
'Retain this bounded intent.'
);
} finally {
rmSync(dir, { recursive: true, force: true });
expect(main).toHaveBeenCalledTimes(1);
const call = main.mock.calls[0][0] as unknown as {
argv: string[];
env: Record<string, string | undefined>;
stdinText: string;
};
expect(call.argv).toEqual([
'--event=UserPromptSubmit',
'--source_client=opencode',
`--work_episode_capture=${expected}`,
]);
expect(call.env.ORGX_SESSION_WORK_EPISODE_CAPTURE).toBeUndefined();
expect(JSON.parse(call.stdinText).prompt).toBe(
expected === 'bounded' ? 'Retain this bounded intent.' : undefined
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
});
);

it('keeps an offline run queued without starting fallback delivery', async () => {
const dir = mkdtempSync(join(tmpdir(), 'orgx-opencode-bridge-'));
Expand Down
29 changes: 23 additions & 6 deletions src/sessionSummaryBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,26 @@ function duration(...values: unknown[]): number | undefined {
return value === undefined ? undefined : Math.max(0, Math.round(value));
}

function workEpisodeCaptureEnabled(value: string | undefined): boolean {
return ['bounded', 'on', 'true', '1'].includes(
String(value ?? '').trim().toLowerCase()
);
/**
* Capture mode is pinned explicitly on every call (plan v3 §5.10), matching the
* Wizard's installed Claude/Codex hooks: an ambient
* ORGX_SESSION_WORK_EPISODE_CAPTURE — set for another client, or inherited
* from a parent process — must not widen what OpenCode captures. Bounded
* capture (redacted request excerpts) is an OpenCode-specific opt-in.
*/
export function resolveOpenCodeWorkCapture(
env: Env = process.env
): 'bounded' | 'metadata-only' {
return String(env.ORGX_OPENCODE_WORK_CAPTURE ?? '').trim().toLowerCase() ===
'bounded'
? 'bounded'
: 'metadata-only';
}

function withoutAmbientCaptureMode(env: Env): Env {
const next = { ...env };
delete next.ORGX_SESSION_WORK_EPISODE_CAPTURE;
return next;
}

function boundedPrompt(value: unknown): string | undefined {
Expand Down Expand Up @@ -145,7 +161,7 @@ export function sanitizeOpenCodePayload(
tool_use_id: string(root.callID, properties.callID),
duration_ms: duration(root.duration_ms, root.duration, properties.duration),
permission_mode: string(root.permission, properties.permission),
prompt: workEpisodeCaptureEnabled(env.ORGX_SESSION_WORK_EPISODE_CAPTURE)
prompt: resolveOpenCodeWorkCapture(env) === 'bounded'
? boundedPrompt(string(root.prompt, root.message, properties.prompt))
: undefined,
root_session_id: string(root.rootSessionID, root.root_session_id),
Expand Down Expand Up @@ -244,9 +260,10 @@ export async function bridgeOpenCodeSessionSummary({
argv: [
`--event=${canonicalEvent}`,
'--source_client=opencode',
`--work_episode_capture=${resolveOpenCodeWorkCapture(env)}`,
...(queueDir ? [`--queue_dir=${queueDir}`] : []),
],
env,
env: withoutAmbientCaptureMode(env),
stdinText: JSON.stringify(sanitizeOpenCodePayload(payload, directory, env)),
});
const fallbackDeliveryTriggered =
Expand Down
Loading