diff --git a/docs/claude-mod.md b/docs/claude-mod.md index fa6bc43e6..01147b7a8 100644 --- a/docs/claude-mod.md +++ b/docs/claude-mod.md @@ -1,7 +1,8 @@ # Claude mod entry point -UG's smart-routed Claude launcher loads `hooks/register.ts` through the existing -launch-scoped `ug-smart-router` plugin. Claude Code 2.1.290 or newer is required. +UG's Claude launcher loads `hooks/register.ts` through the existing launch-scoped +`ug-smart-router` plugin in sessions launched with smart routing. Claude Code 2.1.290 or +newer is required. The existing `_write_routed_claude_plugin()` copies the packaged TypeScript entry point from `src/ucode/agents/claude_mods/` and writes `hooks/hooks.json` into that plugin. @@ -63,13 +64,48 @@ rollout setting or the user's explicit `1`/`0` or `true`/`false` override. As wi boolean environment flags, `1`, `true`, `yes`, and `on` enable it (case-insensitive, with surrounding whitespace ignored); other values disable it. It controls Isaac feedback availability, so UG's routing presets and on/off toggles leave it unchanged. -The native form keeps routing on until Enter accepts a valid reason, then disables -routing and uploads the exact text in the background with `isaac feedback`. The -transcript shows the feedback ID the mod passes with `--feedback-id`; a toast reports -the result. Cancel or Escape leaves routing on and sends nothing. A missing -`isaac` CLI or an unavailable form leaves routing on. A feedback submission failure after a valid +The native form keeps routing on until Enter accepts a valid reason. It closes the +form and shows the sending message and feedback ID before starting Isaac. The existing +native background callback runs `isaac feedback --with-evidence` to capture and upload +the report while the user continues working. Routing is turned off independently of +that process. Delivery produces both a persistent message and a success toast; +a failed upload offers `/isaac-feedback retry ` when a local snapshot was saved. +Before submitting, Cancel or Escape leaves routing on and sends nothing. A missing +`isaac` executable at launch or an unavailable form leaves routing on. A feedback submission failure after a valid reason leaves routing off and reports the failure in the transcript. The form makes no model requests. +In these sessions, with the same flag enabled, `/isaac-feedback` uses the same native +form, capture, and background uploader as `/smart-router off`. It remains available +after routing is turned off so users can report on an earlier routed task. It asks +for a category when none was supplied, and for feedback text when the invocation is empty. +`/isaac-feedback --category smart-routing ` submits directly. Smart-routing +reports pin the current session's most recent task by its native user-message ID. +The `session.append` hook remembers that ID; a session with no observed prompt uses +Isaac's existing target inspection before opening the form. Empty sessions use an +unmatched `no-prior-task` selector so subsequent work cannot become their target. +The native flow does not generate an agent note. Other reports retain their text-only +transport and do not receive an evidence feedback ID. `/isaac-feedback retry ` uploads the saved +snapshot in the background without capturing it again. + +The submission and its sending/completion messages make no model requests and +need no updated Isaac skill instructions. Explicit inspection and local-preview +requests still use the existing skill. Earlier targets, agent notes, and additional +files remain available through the existing Isaac CLI options. + +Claude sessions launched without smart routing use the existing feedback skill. + +Each submission passes Claude's native session ID and the current routing controls +to Isaac. Valid session-file overrides are copied into that child process's environment, +so turning routing off or back on cannot change the report's recorded settings. +These settings describe submission time; subagent execution still requires routing-log +evidence. An invalid controls file remains unverified. + +These uploads belong to the running Claude session. Keep it open until completion; +if interrupted, retry the saved evidence ID on the same machine. If capture did not +finish, submit the feedback again. No Isaac background worker or new CLI options are +required. Evidence is collected in the background: the selected task stays fixed, +while transcript and file snapshots reflect capture time and available file history. + `isaac-feedback.ts` owns this Databricks-specific policy, form, and submission. `register.ts` binds the routing callbacks before Isaac's command guard, then handles toggles the guard passes through. Claude's mod compiler requires engine diff --git a/src/ucode/agents/claude_mods/isaac-feedback.ts b/src/ucode/agents/claude_mods/isaac-feedback.ts index fc4ecaa73..ba13e9db1 100644 --- a/src/ucode/agents/claude_mods/isaac-feedback.ts +++ b/src/ucode/agents/claude_mods/isaac-feedback.ts @@ -1,11 +1,34 @@ -import type { On } from 'claude-code'; +import type { EngineInterface, On } from 'claude-code'; import { envBool } from './constants.ts'; const FEEDBACK_PANE = 'smart-router-feedback'; +const FEEDBACK_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i; +type FeedbackCategory = 'smart-routing' | 'other'; + +function feedbackSuffix(feedbackId: string): string { + return feedbackId ? ` Feedback ID: ${feedbackId}` : ''; +} + +function showSending($: EngineInterface, feedbackId: string): void { + $.ui.log(`Sending feedback in the background. You can keep working.${feedbackSuffix(feedbackId)}`); +} + +function showResult($: EngineInterface, feedbackId: string, sent: boolean): void { + if (sent) { + $.ui.log(`Feedback submitted successfully.${feedbackSuffix(feedbackId)}`); + $.ui.toast('Feedback submitted successfully ✅'); + } else { + const retry = feedbackId ? `/isaac-feedback retry ${feedbackId}` : '/isaac-feedback'; + $.ui.log(`Feedback delivery is unconfirmed. Retry with ${retry}.`); + } +} type FeedbackState = { submitted: boolean; reason: string; + lastPromptId?: string | null; + category?: FeedbackCategory; + turnOff?: boolean; readRoutingEnabled?: () => Promise; disableRouting?: (sessionControlArgs: readonly string[]) => Promise; }; @@ -14,29 +37,206 @@ export function createIsaacFeedbackState(): FeedbackState { return { submitted: false, reason: '' }; } -export function registerIsaacFeedback(on: On, state: FeedbackState): void { +async function feedbackEnvironment($: EngineInterface): Promise> { + const env: Record = { + CLAUDE_CODE_SESSION_ID: await $.session.id(), + // A nested Claude launch can inherit another harness's identity markers. + CODEX_THREAD_ID: '', CODEX_CI: '', CODEX_HOME: '', CODEX_SANDBOX: '', OPENCODE: '', CURSOR_AGENT: '', + ENABLE_SMART_ROUTING_V2: (await $.env.get('ENABLE_SMART_ROUTING_V2')) === '1' ? '1' : '0', + ENABLE_SMART_ROUTING_SUBAGENT_ONLY: (await $.env.get('ENABLE_SMART_ROUTING_SUBAGENT_ONLY')) === '1' ? '1' : '0', + ENABLE_SMART_ROUTER_ORCHESTRATOR: (await $.env.get('ENABLE_SMART_ROUTER_ORCHESTRATOR')) ?? '', + SMART_ROUTER_NAME: (await $.env.get('SMART_ROUTER_NAME')) ?? '', + SMART_ROUTER_CONFIG_VERSION: (await $.env.get('SMART_ROUTER_CONFIG_VERSION')) ?? '', + UCODE_SESSION_ENV_FILE: '', + }; + const controls = await $.env.get('UCODE_SESSION_ENV_FILE'); + if (controls) { + try { + const overrides = JSON.parse(await $.fs.read(controls)); + const keys = ['ENABLE_SMART_ROUTING_V2', 'ENABLE_SMART_ROUTING_SUBAGENT_ONLY', 'ENABLE_SMART_ROUTER_ORCHESTRATOR']; + if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides) + || Object.entries(overrides).some(([key, value]) => !keys.includes(key) || (value !== '0' && value !== '1'))) { + throw new Error('Invalid routing controls.'); + } + Object.assign(env, overrides); + } catch { + // Let Isaac report unreadable controls instead of claiming a verified state. + env.UCODE_SESSION_ENV_FILE = controls; + } + } + // Valid controls are frozen here; a later toggle must not change this report. + return env; +} + +async function prepareFeedbackTarget( + $: EngineInterface, state: FeedbackState, isaacExecutable: string, +): Promise { + if (state.lastPromptId !== undefined) return; + // Resumed sessions may not have emitted a prompt during this plugin's lifetime. + const result = await $.process.run( + [isaacExecutable, 'feedback', '--inspect-target', '--harness', 'claude', '--session-id', await $.session.id()], + { timeoutMs: 10000 }, + ); + if (result.exitCode !== 0) throw new Error('Could not identify the feedback target.'); + if (state.lastPromptId === undefined) state.lastPromptId = JSON.parse(result.stdout).message_id ?? null; +} + +async function uploadFeedback( + $: EngineInterface, isaacExecutable: string, isaacFeedbackDirectory: string, feedbackId: string, + args: string[], message?: string, env?: Record, +): Promise { + try { + const result = await $.process.run([isaacExecutable, 'feedback', ...args], { stdin: message, env, timeoutMs: 120000 }); + if (result.exitCode === 0) { + if (result.stderr.trim()) $.ui.log(result.stderr.trim()); + showResult($, feedbackId, true); + return; + } + } catch { + // A timeout can leave a saved report, which should be retried with its original ID. + } + const saved = feedbackId && await $.fs.exists(`${isaacFeedbackDirectory}/${feedbackId}/receipt.json`).catch(() => false); + showResult($, saved ? feedbackId : '', false); +} + +async function submitFeedback( + $: EngineInterface, state: FeedbackState, isaacExecutable: string, + isaacFeedbackDirectory: string, message: string, +): Promise { + if (state.submitted) return; + const minimum = state.turnOff ? 16 : 1; + if (Array.from(message.trim()).length < minimum) { + return state.turnOff + ? 'Enter at least 16 characters, excluding surrounding whitespace.' + : 'Enter feedback before sending.'; + } + if (!state.category || !isaacExecutable || !isaacFeedbackDirectory) return 'Isaac feedback is unavailable.'; + state.submitted = true; + const feedbackId = state.category === 'smart-routing' ? crypto.randomUUID() : ''; + const { turnOff, disableRouting } = state; + let env: Record | undefined; + let promptId: string | null | undefined; + let args = ['--category', 'other']; + try { + if (feedbackId) { + await prepareFeedbackTarget($, state, isaacExecutable); + promptId = state.lastPromptId; + env = await feedbackEnvironment($); + args = ['--category', 'smart-routing', '--harness', 'claude', + '--session-id', env.CLAUDE_CODE_SESSION_ID, '--feedback-id', feedbackId, '--with-evidence', + // An unmatched selector keeps an empty session from picking up the user's next task. + '--turn-id', promptId ?? 'no-prior-task']; + } + } catch { + state.submitted = false; + return 'Could not prepare feedback. Nothing was uploaded; retry /isaac-feedback.'; + } + await $.ui.close({ id: FEEDBACK_PANE }).catch(() => {}); + showSending($, feedbackId); + if (feedbackId && promptId === null) { + $.ui.log('No earlier task was found. This feedback can help diagnose setup or controls, but has no task or model request to analyze.'); + } + void uploadFeedback($, isaacExecutable, isaacFeedbackDirectory, feedbackId, args, message, env); + if (turnOff) { + try { + if (!disableRouting) throw new Error('Smart Router controls are unavailable.'); + await disableRouting(['--smart-router-feedback', message]); + } catch { + $.ui.log('Could not turn Smart Router off. Feedback is still being sent; retry /smart-router off or restart through UG.'); + } + } + state.submitted = false; +} + +async function openFeedbackForm( + $: EngineInterface, state: FeedbackState, isaacExecutable: string, isaacFeedbackDirectory: string, +): Promise { + if (!isaacExecutable || !isaacFeedbackDirectory) return false; + if (state.category !== 'other') await prepareFeedbackTarget($, state, isaacExecutable); + $.ui.invalidate('ui.render'); + const pane = await $.ui.open({ + id: FEEDBACK_PANE, title: state.turnOff ? 'Turn Smart Router off' : 'Isaac feedback', + focus: true, closeOnEscape: true, rows: 6, columns: 56, + }); + if (pane.isPlaced) return true; + await $.ui.close({ id: FEEDBACK_PANE }); + return false; +} + +export function registerIsaacFeedback( + on: On, state: FeedbackState, isaacExecutable: string, isaacFeedbackDirectory: string, +): void { + on('session.append', { door: 'prompt' }, ($, e, next) => { + if (!e.agentId && !e.message.isMeta) state.lastPromptId = e.uuid; + return next(e); + }); + on('session.end', ($, e, next) => { + state.lastPromptId = undefined; + return next(e); + }); + on('command.run', { command: 'isaac-feedback' }, async ($, e, next) => { + if (!envBool(await $.env.get('ISAAC_SMART_ROUTER_FEEDBACK'))) return next(e); + if (state.submitted) return { text: 'Feedback is being prepared.' }; + let message = e.args; + let category: FeedbackCategory | undefined; + if (message.startsWith('-- ')) { + message = message.slice(3); + } else { + for (const value of ['smart-routing', 'other'] as const) { + const prefix = `--category ${value}`; + if (message === prefix || message.startsWith(prefix + ' ')) { + category = value; + message = message.slice(prefix.length + 1); + break; + } + } + if (!category) { + const [action, feedbackId, ...rest] = message.trim().split(/\s+/); + if (action === 'retry' || action === '--retry') { + if (!isaacExecutable) return { text: 'Isaac feedback is unavailable.' }; + if (!feedbackId || rest.length || !FEEDBACK_ID.test(feedbackId)) { + return { text: 'Usage: /isaac-feedback retry ' }; + } + showSending($, feedbackId); + void uploadFeedback($, isaacExecutable, isaacFeedbackDirectory, feedbackId, ['--retry', feedbackId]); + return {}; + } + // Keep explicit inspection and local-preview requests with the existing skill. + if (message.startsWith('--') + || (action === 'status' && feedbackId && !rest.length && FEEDBACK_ID.test(feedbackId))) return next(e); + } + } + state.turnOff = false; + state.category = category; + state.reason = message; + state.submitted = false; + if (category && message.trim()) { + const error = await submitFeedback($, state, isaacExecutable, isaacFeedbackDirectory, message); + return error ? { text: error } : {}; + } + try { + if (await openFeedbackForm($, state, isaacExecutable, isaacFeedbackDirectory)) return {}; + } catch { + // An unavailable form must not submit feedback or leave a delayed question. + } + return { text: 'Could not open the feedback form. Retry /isaac-feedback in the Claude terminal.' }; + }); + on('command.run', {command: 'smart-router'}, async ($, e, next) => { if (e.args.trim() !== 'off') return next(e); const feedbackRequired = envBool(await $.env.get('ISAAC_SMART_ROUTER_FEEDBACK')); if (!feedbackRequired) return next(e); + if (state.submitted) return { text: 'Feedback is being prepared.' }; try { if (!state.readRoutingEnabled || !state.disableRouting) { throw new Error('Smart Router controls are unavailable.'); } if (!await state.readRoutingEnabled()) return {text: 'Smart Router is off'}; - // The form uploads through Isaac's CLI; without it, keep routing on. `isaac --version` launches a harness. - if ((await $.process.run(['isaac', 'feedback', '--help'], { timeoutMs: 10000 })).exitCode !== 0) { - throw new Error('Isaac CLI is unavailable.'); - } state.submitted = false; state.reason = ''; - const pane = await $.ui.open({ - id: FEEDBACK_PANE, title: 'Turn Smart Router off', focus: true, - closeOnEscape: true, rows: 6, columns: 56, - }); - if (pane.isPlaced) return {text: 'Smart Router is off'}; - // Do not leave a delayed question waiting for an unrelated task. - await $.ui.close({ id: FEEDBACK_PANE }); + state.category = 'smart-routing'; + state.turnOff = true; + if (await openFeedbackForm($, state, isaacExecutable, isaacFeedbackDirectory)) return {}; } catch { // No reason was collected, so leave routing unchanged. } @@ -46,50 +246,44 @@ export function registerIsaacFeedback(on: On, state: FeedbackState): void { on('ui.render', { component: 'Pane' }, ($, e, next) => { if (e.requestId !== FEEDBACK_PANE || e.surface === 'mobile') return next(e); const { Box, Text, Input, Button } = $.ui.resolve(e); + if (!state.category) { + const choose = async (category: FeedbackCategory) => { + state.category = category; + if (state.reason.trim()) { + const error = await submitFeedback($, state, isaacExecutable, isaacFeedbackDirectory, state.reason); + if (error) $.ui.toast(error); + } + $.ui.invalidate('ui.render'); + }; + return Box({ flexDirection: 'column', children: [ + Text({ children: ['What is your feedback about?'] }), + Button({ key: 'routing', label: 'Smart routing (with session details)', autoFocus: true, onPress: () => choose('smart-routing') }), + Button({ key: 'other', label: 'Other (text only)', onPress: () => choose('other') }), + Button({ key: 'cancel', label: 'Cancel', onPress: () => $.ui.close({ id: FEEDBACK_PANE }) }), + ] }); + } return Box({ flexDirection: 'column', children: [ - Text({ children: ['Why are you turning Smart Router off?'] }), - Text({ children: ['Enter at least 16 characters. Your reason is sent to the Isaac team with this session\'s details.'] }), + Text({ children: [state.turnOff ? 'Why are you turning Smart Router off?' : 'What would you like to share?'] }), + Text({ children: [state.turnOff + ? 'Enter at least 16 characters. Your reason is sent to the Isaac team with this session\'s details.' + : state.category === 'smart-routing' + ? 'Your feedback is sent to the Isaac team with this session\'s details.' + : 'Your feedback is sent to the Isaac team without session evidence.'] }), Input({ key: 'reason', - label: 'Reason', + label: state.turnOff ? 'Reason' : 'Feedback', value: state.reason, autoFocus: true, - submitLabel: 'Send feedback and turn off', + submitLabel: state.turnOff ? 'Send feedback and turn off' : 'Send feedback', onInput: (value) => { state.reason = value; }, onSubmit: async (value) => { - if (state.submitted || !state.disableRouting) return; - if (Array.from(value.trim()).length < 16) { - $.ui.toast('Enter at least 16 characters, excluding surrounding whitespace.'); - return; - } - state.submitted = true; - try { - await state.disableRouting(['--smart-router-feedback', value]); - } catch { - state.submitted = false; - $.ui.toast('Could not update Smart Router. Smart Router is still on; retry or restart through UG.'); - return; - } - await $.ui.close({ id: FEEDBACK_PANE }).catch(() => {}); - // Upload in the background through Isaac's CLI, without a model turn. - const feedbackId = crypto.randomUUID(); - $.ui.log(`Sending feedback in the background. You can keep working. Feedback ID: ${feedbackId}`); - void $.session.id() - .then((sessionId) => $.process.run( - ['isaac', 'feedback', '--category', 'smart-routing', '--harness', 'claude', - '--session-id', sessionId, '--feedback-id', feedbackId], - { stdin: value, timeoutMs: 30000 }, - )) - .then((result) => result.exitCode === 0, () => false) - // A failure goes to the transcript, where it outlasts a toast. - .then((sent) => sent - ? $.ui.toast('Feedback submitted successfully ✅') - : $.ui.log('Smart Router is off, but feedback could not be sent. Retry with /isaac-feedback --category smart-routing.')); + const error = await submitFeedback($, state, isaacExecutable, isaacFeedbackDirectory, value); + if (error) $.ui.toast(error); }, }), - Button({ key: 'cancel', label: 'Cancel (keep routing on)', onPress: () => $.ui.close({ id: FEEDBACK_PANE }) }), + Button({ key: 'cancel', label: state.turnOff ? 'Cancel (keep routing on)' : 'Cancel', onPress: () => $.ui.close({ id: FEEDBACK_PANE }) }), ], }); }); diff --git a/src/ucode/agents/claude_mods/register.ts b/src/ucode/agents/claude_mods/register.ts index c9452106a..8cf646767 100644 --- a/src/ucode/agents/claude_mods/register.ts +++ b/src/ucode/agents/claude_mods/register.ts @@ -117,7 +117,7 @@ export function register(on: On, options: PluginOptions): void { } return next(e); }); - registerIsaacFeedback(on, feedback); + registerIsaacFeedback(on, feedback, options.isaacExecutable as string, options.isaacFeedbackDirectory as string); on('command.run', {command: 'smart-router'}, async ($, e) => { const action = e.args.trim(); try { diff --git a/src/ucode/smart_routing/v2.py b/src/ucode/smart_routing/v2.py index 7a31eed3c..69149d210 100644 --- a/src/ucode/smart_routing/v2.py +++ b/src/ucode/smart_routing/v2.py @@ -3,6 +3,7 @@ import hashlib import json import os +import shutil import signal import socket import subprocess @@ -340,12 +341,15 @@ def _routed_claude_agent_definitions(model_ids: list[str]) -> dict[str, dict[str def _claude_mod_launch_options(env: Mapping[str, str] | None = None) -> dict: source = resolve_environment(env, agent=AGENT_CLAUDE) + isaac = shutil.which("isaac") return { **{key: source.get(key, "") for key in SMART_ROUTING_ENV_KEYS}, # Native plugin options cannot represent null; preserve absent versus empty values. "unset": [key for key in SMART_ROUTING_ENV_KEYS if key not in source], "recipe": source.get(routing.ROUTER_NAME_ENV_VAR, "").strip() or routing.ROUTER_NAME, "routeCommand": [], + "isaacExecutable": str(Path(isaac).absolute()) if isaac else "", + "isaacFeedbackDirectory": str(Path.home() / ".isaac" / "feedback"), } @@ -359,6 +363,18 @@ def _write_routed_claude_plugin(plugin_dir: Path, model_ids: list[str]) -> None: "description": "Launch-scoped agents for Unity Gateway smart routing.", "author": {"name": "Databricks"}, "userConfig": { + "isaacExecutable": { + "type": "string", + "default": "", + "title": "Isaac executable", + "description": "Isaac CLI resolved at launch for feedback submission", + }, + "isaacFeedbackDirectory": { + "type": "string", + "default": "", + "title": "Isaac feedback directory", + "description": "Absolute directory for locally captured feedback", + }, "routeCommand": { "type": "string", "multiple": True, diff --git a/tests/README.md b/tests/README.md index c14c2e96f..c1a4c54bd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -34,9 +34,19 @@ TypeScript event-forwarding and launch-option tests in CI. The existing Claude skill-toggle integration journey checks the live recipe payload field across on/off/on. See [the mod guide](../docs/claude-mod.md). -The required Isaac feedback form needs manual validation of the 16-character minimum, -cancellation, repeated toggles, and submission failures. The existing routing-toggle -journeys do not cover this form or a live Isaac evidence upload. +The required Isaac feedback form and background delivery notifications need manual +validation using the released Isaac skill and existing capture/retry CLI. Check +category selection, empty and inline feedback, native retries, the off form's +16-character minimum, cancellation, repeated toggles, and submission success/failure. +Also check that the form closes immediately and submission makes no model request. +Start a new task during capture and confirm the report retains the original task; +toggle routing during upload and confirm its recorded settings stay fixed. Check that +saved evidence stays unchanged, upload permission denials are honored, local +previews stay local, and Other stays text-only. In a session launched with routing, +check `/isaac-feedback` before and after turning routing off. A session launched +without routing should use the existing feedback skill without loading the router +plugin. The routing-toggle journeys do not cover the form, notification timing, or +live evidence uploads. `test_claude_answer_delivery.py` covers Claude answer selection, composition, ownership, failed tasks and resumed revisions using transcript inputs, plus real subprocess I/O, diff --git a/tests/integration/README.md b/tests/integration/README.md index 4e188c967..83be43d97 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -93,8 +93,17 @@ The Claude mod entry point has separate native component checks in `../native_claude/test_mod.py`. Claude 2.1.290 validates the module and runs its TypeScript event-forwarding and launch-option tests in a dedicated CI job. The existing Claude skill-toggle journey checks the live recipe payload field across on/off/on. -The required Isaac feedback form and evidence submission need a separate manual -Isaac check. The gateway toggle journey does not cover the form. +The required Isaac feedback form, evidence submission, and background delivery +notifications need a separate manual Isaac check. The gateway toggle journey does +not cover the form or notification timing. Use the released Isaac skill with the +existing capture/retry CLI to check native category selection and retries, +immediate form dismissal without model requests, and completion during another task. +Verify the original task stays selected when a new prompt precedes capture, and +routing settings stay fixed across subsequent toggles. Check immutable saved +evidence, upload permission denials, local previews, and text-only Other reports. +In a session launched with routing, check that `/isaac-feedback` remains asynchronous +after routing is turned off. A session launched without routing should use the +existing feedback skill without loading the router plugin. The existing unit tests keep their fixtures. Integration has an independent pytest configuration and uses `--confcutdir` so those fixtures cannot leak in. diff --git a/tests/test_claude_mod.py b/tests/test_claude_mod.py index e122fe319..d8e236d2a 100644 --- a/tests/test_claude_mod.py +++ b/tests/test_claude_mod.py @@ -1,6 +1,7 @@ """Generated plugin contents for the native Claude mod entry point.""" import json +import shutil from pathlib import Path import pytest @@ -50,17 +51,21 @@ def test_launch_options_resolve_versions_without_mutating_input(version): expected = config._VERSIONS.get(version, {}).get( AGENT_CLAUDE, dict.fromkeys(SMART_ROUTING_ENV_KEYS, "0") ) + isaac = shutil.which("isaac") assert v2._claude_mod_launch_options(env) == { **expected, "unset": [], "recipe": "custom_recipe", "routeCommand": [], + "isaacExecutable": str(Path(isaac).absolute()) if isaac else "", + "isaacFeedbackDirectory": str(Path.home() / ".isaac" / "feedback"), } # Taking the baseline must not alter the caller's launch environment. assert env == before def test_launch_options_preserve_empty_and_absent_values(): + isaac = shutil.which("isaac") assert v2._claude_mod_launch_options({"ENABLE_SMART_ROUTING_V2": ""}) == { "ENABLE_SMART_ROUTING_V2": "", "ENABLE_SMART_ROUTING_SUBAGENT_ONLY": "", @@ -68,4 +73,6 @@ def test_launch_options_preserve_empty_and_absent_values(): "unset": ["ENABLE_SMART_ROUTING_SUBAGENT_ONLY", "ENABLE_SMART_ROUTER_ORCHESTRATOR"], "recipe": "task_v3", "routeCommand": [], + "isaacExecutable": str(Path(isaac).absolute()) if isaac else "", + "isaacFeedbackDirectory": str(Path.home() / ".isaac" / "feedback"), }