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
33 changes: 22 additions & 11 deletions scripts/hooks/session-summary-bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ function finiteDuration(...values) {
return value === undefined ? undefined : Math.max(0, Math.round(value));
}

function workEpisodeCaptureEnabled(value) {
return ['bounded', 'on', 'true', '1'].includes(
String(value ?? '').trim().toLowerCase()
);
}

function boundedPrompt(value) {
const prompt = string(value);
return prompt
Expand Down Expand Up @@ -150,7 +144,7 @@ export function sanitizeCursorPayload(
tool_use_id: string(payload.tool_use_id, payload.toolUseId),
duration_ms: finiteDuration(payload.duration_ms, payload.duration),
permission_mode: string(payload.permission_mode, payload.permissionMode),
prompt: workEpisodeCaptureEnabled(env.ORGX_SESSION_WORK_EPISODE_CAPTURE)
prompt: resolveCursorWorkCapture(env) === 'bounded'
? boundedPrompt(
string(
payload.prompt,
Expand Down Expand Up @@ -183,6 +177,24 @@ function defaultHookPath(env) {
);
}

/**
* 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 Cursor captures. Bounded
* capture (redacted request excerpts) is a Cursor-specific opt-in.
*/
export function resolveCursorWorkCapture(env = process.env) {
const value = String(env.ORGX_CURSOR_WORK_CAPTURE ?? '').trim().toLowerCase();
return value === 'bounded' ? 'bounded' : 'metadata-only';
}

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

function autoFlushDisabled(value) {
return ['off', 'false', '0'].includes(String(value ?? '').trim().toLowerCase());
}
Expand Down Expand Up @@ -224,16 +236,15 @@ export async function bridgeCursorSessionSummary({
}

const queueDir = string(env.ORGX_SESSION_SUMMARY_QUEUE_DIR);
const workCapture = resolveCursorWorkCapture(env);
const result = await hook.main({
argv: [
`--event=${canonicalEvent}`,
'--source_client=cursor',
`--work_episode_capture=${workCapture}`,
...(queueDir ? [`--queue_dir=${queueDir}`] : []),
],
// The Wizard owns capture consent. Passing the environment through lets
// ORGX_SESSION_WORK_EPISODE_CAPTURE select bounded capture when the user
// opted in; omitting a CLI override preserves its metadata-only default.
env,
env: withoutAmbientCaptureMode(env),
stdinText: JSON.stringify(sanitizeCursorPayload(payload, cwd, env)),
});
const fallbackDeliveryTriggered =
Expand Down
91 changes: 52 additions & 39 deletions scripts/hooks/session-summary-bridge.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test('allowlists bounded user intent and lineage while dropping tool and identit
error_message: 'private error',
},
'/work/repo',
{ ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }
{ ORGX_CURSOR_WORK_CAPTURE: 'bounded' }
);

assert.deepEqual(payload, {
Expand Down Expand Up @@ -71,10 +71,19 @@ test('defaults to metadata-only capture and bounds explicitly enabled prompts',
sanitizeCursorPayload(
{ prompt },
'/work/repo',
{ ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }
{ ORGX_CURSOR_WORK_CAPTURE: 'bounded' }
).prompt.length,
600
);
assert.equal(
sanitizeCursorPayload(
{ prompt },
'/work/repo',
{ ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }
).prompt,
undefined,
'an ambient generic capture variable must not widen Cursor capture'
);
});

test('resolves the active workspace instead of the installed plugin cwd', () => {
Expand Down Expand Up @@ -148,6 +157,7 @@ test('delegates to the installed Wizard hook and starts fallback delivery', asyn
assert.deepEqual(calls[0].argv, [
'--event=RunEnd',
'--source_client=cursor',
'--work_episode_capture=metadata-only',
]);
assert.deepEqual(JSON.parse(calls[0].stdinText), {
session_id: 'conversation-1',
Expand All @@ -168,45 +178,48 @@ test('delegates to the installed Wizard hook and starts fallback delivery', asyn
}
});

test('leaves bounded Work Episode capture to explicit Wizard environment consent', async () => {
const dir = mkdtempSync(join(tmpdir(), 'orgx-cursor-bridge-'));
const hookPath = join(dir, 'orgx-session-summary.mjs');
writeFileSync(hookPath, 'export async function main() {}\n', 'utf8');
const calls = [];
try {
await bridgeCursorSessionSummary({
event: 'user_prompt',
payload: {
conversation_id: 'conversation-consent',
prompt: 'bounded only after explicit consent',
},
hookPath,
env: {
PATH: process.env.PATH,
ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded',
},
moduleLoader: async () => ({
main: async (input) => {
calls.push(input);
return { ok: true };
for (const [label, env, expected] of [
['an ambient generic variable', { ORGX_SESSION_WORK_EPISODE_CAPTURE: 'bounded' }, 'metadata-only'],
['the Cursor-specific opt-in', { ORGX_CURSOR_WORK_CAPTURE: 'bounded' }, 'bounded'],
]) {
test(`pins the capture mode explicitly under ${label} (plan v3 §5.10)`, async () => {
const dir = mkdtempSync(join(tmpdir(), 'orgx-cursor-bridge-'));
const hookPath = join(dir, 'orgx-session-summary.mjs');
writeFileSync(hookPath, 'export async function main() {}\n', 'utf8');
const calls = [];
try {
await bridgeCursorSessionSummary({
event: 'user_prompt',
payload: {
conversation_id: 'conversation-consent',
prompt: 'bounded only after explicit consent',
},
}),
});
hookPath,
env: { PATH: process.env.PATH, ...env },
moduleLoader: async () => ({
main: async (input) => {
calls.push(input);
return { ok: true };
},
}),
});

assert.equal(calls.length, 1);
assert.equal(calls[0].env.ORGX_SESSION_WORK_EPISODE_CAPTURE, 'bounded');
assert.deepEqual(calls[0].argv, [
'--event=UserPromptSubmit',
'--source_client=cursor',
]);
assert.equal(
calls[0].argv.some((arg) => arg.startsWith('--work_episode_capture=')),
false
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
assert.equal(calls.length, 1);
assert.deepEqual(calls[0].argv, [
'--event=UserPromptSubmit',
'--source_client=cursor',
`--work_episode_capture=${expected}`,
]);
assert.equal(calls[0].env.ORGX_SESSION_WORK_EPISODE_CAPTURE, undefined);
assert.equal(
JSON.parse(calls[0].stdinText).prompt !== undefined,
expected === 'bounded'
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
}

test('keeps an offline run queued without starting fallback delivery', async () => {
const dir = mkdtempSync(join(tmpdir(), 'orgx-cursor-bridge-'));
Expand Down
Loading