From 88e8a28785a5abd378bce2eccbb46bef142f503e Mon Sep 17 00:00:00 2001 From: hopeatina Date: Tue, 22 Sep 2026 21:00:32 -0500 Subject: [PATCH] Pin the Cursor work capture mode explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan v3 §5.10: the bridge loaded the Wizard hook without --work_episode_capture, so an ambient ORGX_SESSION_WORK_EPISODE_CAPTURE could widen capture. It now always passes the mode (metadata-only unless ORGX_CURSOR_WORK_CAPTURE=bounded) and strips the generic variable. Checks: npm test 54 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 --- scripts/hooks/session-summary-bridge.mjs | 33 ++++--- scripts/hooks/session-summary-bridge.test.mjs | 91 +++++++++++-------- 2 files changed, 74 insertions(+), 50 deletions(-) diff --git a/scripts/hooks/session-summary-bridge.mjs b/scripts/hooks/session-summary-bridge.mjs index d7e80f0..0911c20 100644 --- a/scripts/hooks/session-summary-bridge.mjs +++ b/scripts/hooks/session-summary-bridge.mjs @@ -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 @@ -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, @@ -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()); } @@ -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 = diff --git a/scripts/hooks/session-summary-bridge.test.mjs b/scripts/hooks/session-summary-bridge.test.mjs index c07ad87..14c6164 100644 --- a/scripts/hooks/session-summary-bridge.test.mjs +++ b/scripts/hooks/session-summary-bridge.test.mjs @@ -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, { @@ -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', () => { @@ -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', @@ -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-'));