Skip to main content

winx_code_agent/tools/
initialize.rs

1//! Implementation of the Initialize tool.
2
3use std::collections::VecDeque;
4use std::fmt::Write as FmtWrite;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex as StdMutex, OnceLock};
8use tokio::sync::Mutex;
9use tracing::{info, instrument, warn};
10
11use crate::errors::{Result, WinxError};
12use crate::runtime::{EmbeddedShellRuntime, ShellRuntime, ShellSessionTransition};
13use crate::state::bash_state::{generate_thread_id, BashState};
14use crate::types::{
15    normalize_thread_id, AllowedCommands, AllowedGlobs, BashCommandMode, BashMode,
16    CodeWriterConfig, FileEditMode, Initialize, InitializeType, Modes, WriteIfEmptyMode,
17};
18use crate::utils::mmap::read_file_to_string;
19use crate::utils::path::{ensure_directory_exists, expand_user, validate_path_in_workspace};
20
21const POLICY_WARNING_CACHE_CAPACITY: usize = 128;
22static WARNED_POLICY_CONFIGURATIONS: OnceLock<StdMutex<VecDeque<Vec<String>>>> = OnceLock::new();
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub(crate) enum InitializeTransition {
26    Created,
27    AttachedExisting,
28    ModeChanged,
29    ShellReset,
30    WorkspaceChanged,
31}
32
33impl InitializeTransition {
34    pub(crate) const fn as_str(self) -> &'static str {
35        match self {
36            Self::Created => "created",
37            Self::AttachedExisting => "attached_existing",
38            Self::ModeChanged => "mode_changed",
39            Self::ShellReset => "shell_reset",
40            Self::WorkspaceChanged => "workspace_changed",
41        }
42    }
43}
44
45#[derive(Debug)]
46pub(crate) struct InitializeOutcome {
47    pub(crate) text: String,
48    pub(crate) transition: InitializeTransition,
49    pub(crate) recovered_missing_session: bool,
50    pub(crate) context_bytes: usize,
51    pub(crate) guidelines_bytes: usize,
52    pub(crate) initial_files_count: usize,
53    pub(crate) compact_response: bool,
54    pub(crate) code_writer_policy_strength: Option<&'static str>,
55    pub(crate) shell_spawners_present: bool,
56    pub(crate) temporary_artifact_dir: PathBuf,
57    pub(crate) temporary_artifact_ttl_seconds: u64,
58    pub(crate) temporary_artifact_max_bytes: u64,
59    pub(crate) temporary_artifact_max_session_bytes: u64,
60    pub(crate) temporary_artifact_max_file_bytes: u64,
61    pub(crate) temporary_artifact_max_files: usize,
62    pub(crate) temporary_code_map_max_calls: usize,
63    pub(crate) temporary_code_map_max_unique_files: usize,
64}
65
66/// Create a unique scratch workspace under the system temp dir, used when the
67/// caller initializes without a workspace path.
68fn create_playground_dir(thread_id: &str) -> Result<PathBuf> {
69    #[cfg(unix)]
70    let owner = crate::os::unix::effective_uid().to_string();
71    #[cfg(not(unix))]
72    let owner = std::process::id().to_string();
73
74    let dir = std::env::temp_dir().join(format!("winx-playground-{owner}-{thread_id}"));
75    ensure_directory_exists(&dir)?;
76    Ok(dir)
77}
78
79/// Whether `cmd` is on PATH (best-effort, used only for advisory hints).
80fn command_exists(cmd: &str) -> bool {
81    std::process::Command::new("sh")
82        .args(["-c", &format!("command -v {cmd}")])
83        .output()
84        .is_ok_and(|o| o.status.success())
85}
86
87fn code_writer_state(
88    config: &CodeWriterConfig,
89    workspace_root: &Path,
90) -> (BashCommandMode, FileEditMode, WriteIfEmptyMode) {
91    let mut config = config.clone();
92    // Forgive the common `["all"]` mistake before turning relative globs absolute.
93    config.allowed_globs.normalize();
94    config.allowed_commands.normalize();
95    config.update_relative_globs(&workspace_root.to_string_lossy());
96
97    (
98        BashCommandMode {
99            bash_mode: BashMode::NormalMode,
100            allowed_commands: config.allowed_commands,
101        },
102        FileEditMode { allowed_globs: config.allowed_globs.clone() },
103        WriteIfEmptyMode { allowed_globs: config.allowed_globs },
104    )
105}
106
107fn mode_to_state(
108    mode: Modes,
109    config: Option<&CodeWriterConfig>,
110    workspace_root: &Path,
111) -> Result<(BashCommandMode, FileEditMode, WriteIfEmptyMode)> {
112    match mode {
113        Modes::Wcgw => Ok((
114            BashCommandMode {
115                bash_mode: BashMode::NormalMode,
116                allowed_commands: AllowedCommands::All("all".to_string()),
117            },
118            FileEditMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
119            WriteIfEmptyMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
120        )),
121        Modes::Architect => Ok((
122            BashCommandMode {
123                bash_mode: BashMode::RestrictedMode,
124                allowed_commands: AllowedCommands::List(
125                    crate::utils::bash_parser::architect_allowed_commands(),
126                ),
127            },
128            FileEditMode { allowed_globs: AllowedGlobs::List(vec![]) },
129            WriteIfEmptyMode { allowed_globs: AllowedGlobs::List(vec![]) },
130        )),
131        Modes::CodeWriter => {
132            let config = config.ok_or_else(|| {
133                WinxError::ArgumentParseError(
134                    "code_writer_config is required when mode_name is code_writer.".to_string(),
135                )
136            })?;
137            Ok(code_writer_state(config, workspace_root))
138        }
139    }
140}
141
142fn active_code_writer_config(state: &BashState) -> Option<CodeWriterConfig> {
143    (state.mode == Modes::CodeWriter).then(|| CodeWriterConfig {
144        allowed_globs: state.file_edit_mode.allowed_globs.clone(),
145        allowed_commands: state.bash_command_mode.allowed_commands.clone(),
146    })
147}
148
149fn active_code_writer_policy(state: &BashState) -> (Option<&'static str>, Vec<String>) {
150    if state.mode != Modes::CodeWriter {
151        return (None, Vec::new());
152    }
153
154    match &state.bash_command_mode.allowed_commands {
155        AllowedCommands::All(value) if value == "all" => (Some("unrestricted"), Vec::new()),
156        AllowedCommands::All(_) => (Some("restricted"), Vec::new()),
157        AllowedCommands::List(commands) => {
158            let bypass = crate::utils::bash_parser::detect_allowlist_bypass(commands);
159            let strength = if bypass.is_empty() { "restricted" } else { "weak" };
160            (Some(strength), bypass)
161        }
162    }
163}
164
165fn warn_code_writer_policy_once(state: &BashState, bypass: &[String]) {
166    if bypass.is_empty() {
167        return;
168    }
169    let AllowedCommands::List(commands) = &state.bash_command_mode.allowed_commands else {
170        return;
171    };
172
173    let cache = WARNED_POLICY_CONFIGURATIONS
174        .get_or_init(|| StdMutex::new(VecDeque::with_capacity(POLICY_WARNING_CACHE_CAPACITY)));
175    let mut cache = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
176    if cache.iter().any(|seen| seen == commands) {
177        return;
178    }
179    if cache.len() == POLICY_WARNING_CACHE_CAPACITY {
180        cache.pop_front();
181    }
182    cache.push_back(commands.clone());
183    drop(cache);
184
185    warn!(
186        commands = ?bypass,
187        "code_writer allowlist includes shell-spawning commands; the command allowlist is \
188         effectively bypassable and does not sandbox the agent"
189    );
190}
191
192fn append_code_writer_policy_warning(response: &mut String, bypass: &[String]) {
193    if bypass.is_empty() {
194        return;
195    }
196    let _ = writeln!(
197        response,
198        "\n⚠️  SECURITY: code_writer allowlist includes shell/eval commands ({}). \
199         They execute arbitrary code from string arguments (e.g. `bash -c …`, \
200         `find -exec …`), so the command allowlist is effectively bypassable and \
201         does NOT sandbox the agent. Drop them if you intended a hard restriction.",
202        bypass.join(", ")
203    );
204}
205
206fn read_initial_files_simple(files: &[String], workspace: &std::path::Path) -> String {
207    let mut output = String::new();
208    for file_path in files {
209        let expanded = expand_user(file_path);
210        let path = if std::path::Path::new(&expanded).is_absolute() {
211            PathBuf::from(&expanded)
212        } else {
213            workspace.join(&expanded)
214        };
215
216        if let Ok(validated) = validate_path_in_workspace(&path, workspace) {
217            if validated.exists() && validated.is_file() {
218                if let Ok(content) = read_file_to_string(&validated, 10_000_000) {
219                    let _ = write!(output, "\n{file_path}\n```\n{content}\n```\n");
220                }
221            }
222        }
223    }
224    output
225}
226
227fn prepare_workspace(
228    initialize: &Initialize,
229    thread_id: &str,
230    response: &mut String,
231) -> Result<PathBuf> {
232    let workspace_path_str = expand_user(&initialize.any_workspace_path);
233    if workspace_path_str.is_empty() {
234        // wcgw parity: no path given → spin up a scratch playground instead of
235        // forcing the agent to always supply a workspace.
236        let playground = create_playground_dir(thread_id)?;
237        let _ = writeln!(
238            response,
239            "No workspace path provided; created a playground at {}",
240            playground.display()
241        );
242        return Ok(playground);
243    }
244
245    let workspace_path = PathBuf::from(&workspace_path_str);
246    let mut folder_to_start = workspace_path.clone();
247
248    if workspace_path.exists() {
249        if workspace_path.is_file() {
250            folder_to_start = workspace_path.parent().unwrap_or(&workspace_path).to_path_buf();
251            let _ =
252                writeln!(response, "Using parent directory of file: {}", folder_to_start.display());
253        } else if workspace_path.is_dir() {
254            let _ = writeln!(response, "Using workspace directory: {}", folder_to_start.display());
255        }
256    } else if workspace_path.is_absolute() {
257        ensure_directory_exists(&workspace_path).map_err(|e| {
258            WinxError::WorkspacePathError(format!("Failed to create workspace: {e}"))
259        })?;
260        let _ = writeln!(response, "Created workspace directory: {}", workspace_path.display());
261    }
262
263    // Canonicalize so downstream comparisons (workspace checks, glob prefixes) match
264    // paths that were canonicalized via fs::canonicalize — important on macOS where
265    // /var, /tmp etc. are symlinks to /private/var, /private/tmp.
266    if folder_to_start.exists() {
267        if let Ok(canonical) = folder_to_start.canonicalize() {
268            folder_to_start = canonical;
269        }
270    }
271
272    Ok(folder_to_start)
273}
274
275fn initialize_thread_id(initialize: &Initialize) -> String {
276    let thread_id = normalize_thread_id(&initialize.thread_id);
277    if thread_id.is_empty() {
278        generate_thread_id()
279    } else {
280        thread_id
281    }
282}
283
284fn validate_thread_id(initialize: &Initialize) -> Result<()> {
285    if initialize.init_type != InitializeType::FirstCall
286        && normalize_thread_id(&initialize.thread_id).is_empty()
287    {
288        return Err(WinxError::ThreadIdMismatch(
289            "Thread id should be provided if type != 'first_call', including when resetting."
290                .to_string(),
291        ));
292    }
293
294    Ok(())
295}
296
297fn load_guidelines(workspace: &Path) -> String {
298    let mut output = String::new();
299    let mut candidates = Vec::new();
300    if let Some(home) = home::home_dir() {
301        candidates.push(home.join(".winx").join("AGENTS.md"));
302        candidates.push(home.join(".winx").join("CLAUDE.md"));
303        candidates.push(home.join(".wcgw").join("AGENTS.md"));
304        candidates.push(home.join(".wcgw").join("CLAUDE.md"));
305    }
306    candidates.push(workspace.join("AGENTS.md"));
307    candidates.push(workspace.join("CLAUDE.md"));
308
309    for path in candidates {
310        if path.is_file() {
311            if let Ok(content) = fs::read_to_string(&path) {
312                let _ = writeln!(output, "\n## {}\n{}", path.display(), content);
313            }
314        }
315    }
316    output
317}
318
319pub async fn handle_tool_call(
320    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
321    initialize: Initialize,
322) -> Result<String> {
323    handle_tool_call_with_runtime(&EmbeddedShellRuntime, bash_state_arc, initialize).await
324}
325
326pub async fn handle_tool_call_with_runtime(
327    runtime: &dyn ShellRuntime,
328    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
329    initialize: Initialize,
330) -> Result<String> {
331    Ok(handle_tool_call_with_runtime_detailed(runtime, bash_state_arc, initialize).await?.text)
332}
333
334#[instrument(level = "info", skip(runtime, bash_state_arc, initialize))]
335#[allow(clippy::too_many_lines)]
336pub(crate) async fn handle_tool_call_with_runtime_detailed(
337    runtime: &dyn ShellRuntime,
338    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
339    mut initialize: Initialize,
340) -> Result<InitializeOutcome> {
341    let mut response = String::new();
342    let mut context_bytes = 0;
343    let mut guidelines_bytes = 0;
344    let initial_files_count = initialize.initial_files_to_read.len();
345
346    info!("Initialize called for workspace: {}", initialize.any_workspace_path);
347
348    validate_thread_id(&initialize)?;
349    let thread_id = initialize_thread_id(&initialize);
350    let mut bash_state_guard = bash_state_arc.lock().await;
351    let recovered_request = if bash_state_guard.is_none() {
352        match initialize.init_type {
353            InitializeType::UserAskedModeChange => Some("user_asked_mode_change"),
354            InitializeType::ResetShell => Some("reset_shell"),
355            InitializeType::FirstCall | InitializeType::UserAskedChangeWorkspace => None,
356        }
357    } else {
358        None
359    };
360    if let Some(requested_transition) = recovered_request {
361        info!(requested_transition, "Initialize recovered a missing live session as first_call");
362        initialize.init_type = InitializeType::FirstCall;
363    }
364    if initialize.init_type != InitializeType::FirstCall && bash_state_guard.is_none() {
365        return Err(WinxError::BashStateNotInitialized);
366    }
367    let local_attach = initialize.init_type == InitializeType::FirstCall
368        && bash_state_guard
369            .as_ref()
370            .is_some_and(|state| state.initialized && state.current_thread_id == thread_id);
371    let folder_to_start = if local_attach {
372        bash_state_guard
373            .as_ref()
374            .map_or_else(|| PathBuf::from("/tmp"), |state| state.workspace_root.clone())
375    } else {
376        prepare_workspace(&initialize, &thread_id, &mut response)?
377    };
378    let requested_mode = Modes::from(&initialize.mode_name);
379    let preserve_active_mode =
380        local_attach || initialize.init_type == InitializeType::UserAskedChangeWorkspace;
381    let (mode, bash_command_mode, file_edit_mode, write_if_empty_mode) = if preserve_active_mode {
382        let Some(state) = bash_state_guard.as_ref() else {
383            return Err(WinxError::BashStateNotInitialized);
384        };
385        (
386            state.mode,
387            state.bash_command_mode.clone(),
388            state.file_edit_mode.clone(),
389            state.write_if_empty_mode.clone(),
390        )
391    } else {
392        let (bash_command_mode, file_edit_mode, write_if_empty_mode) = mode_to_state(
393            requested_mode,
394            initialize.code_writer_config.as_ref(),
395            &folder_to_start,
396        )?;
397        (requested_mode, bash_command_mode, file_edit_mode, write_if_empty_mode)
398    };
399
400    let transition;
401    let mut compact_response = false;
402
403    match initialize.init_type {
404        InitializeType::FirstCall => {
405            let configured = if local_attach {
406                let Some(state) = bash_state_guard.as_mut() else {
407                    return Err(WinxError::BashStateNotInitialized);
408                };
409                // Refresh guardian activity without resetting the PTY. Reusing the
410                // current snapshot as a mode transition is also compatible with
411                // protocol-1.2 guardians that predate attach-or-create.
412                let mut configured =
413                    runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
414                configured.attached_existing = true;
415                configured
416            } else {
417                let mut new_bash_state = BashState::new();
418                new_bash_state.current_thread_id.clone_from(&thread_id);
419                new_bash_state.mode = mode;
420                new_bash_state.bash_command_mode = bash_command_mode;
421                new_bash_state.file_edit_mode = file_edit_mode;
422                new_bash_state.write_if_empty_mode = write_if_empty_mode;
423                new_bash_state.initialized = true;
424
425                let resumed_context = if initialize.task_id_to_resume.is_empty() {
426                    None
427                } else {
428                    crate::tools::context_save::load_saved_context(&initialize.task_id_to_resume)?
429                };
430
431                if let Some((memory_data, snapshot)) = &resumed_context {
432                    if let Some(snapshot) = snapshot {
433                        new_bash_state.apply_snapshot(snapshot);
434                        new_bash_state.current_thread_id.clone_from(&thread_id);
435                    }
436                    let _ = writeln!(
437                        response,
438                        "\n# Resumed task {}\nFollowing is the retrieved task context:\n{}",
439                        initialize.task_id_to_resume, memory_data
440                    );
441                }
442
443                // A bash snapshot already carries cwd/workspace. Without one, prefer
444                // the project root recorded in the resumed memory (so the agent lands
445                // back in the right repo), then fall back to the provided folder.
446                if resumed_context.as_ref().and_then(|(_, snapshot)| snapshot.as_ref()).is_none() {
447                    let resumed_root = resumed_context
448                        .as_ref()
449                        .and_then(|(memory, _)| {
450                            crate::tools::context_save::extract_project_root(memory)
451                        })
452                        .filter(|root| root.exists());
453                    let target = resumed_root.as_deref().unwrap_or(folder_to_start.as_path());
454                    if target.exists() {
455                        new_bash_state.update_cwd(target)?;
456                        new_bash_state.update_workspace_root(target)?;
457                    }
458                }
459                let configured = runtime
460                    .configure_session(&mut new_bash_state, ShellSessionTransition::FirstCall)
461                    .await?;
462                *bash_state_guard = Some(new_bash_state);
463                configured
464            };
465
466            transition = if configured.attached_existing {
467                InitializeTransition::AttachedExisting
468            } else {
469                InitializeTransition::Created
470            };
471            compact_response = configured.attached_existing
472                && initialize.initial_files_to_read.is_empty()
473                && initialize.task_id_to_resume.is_empty();
474
475            if compact_response {
476                response.clear();
477                let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
478                let _ = writeln!(
479                    response,
480                    "Attached to the existing durable Winx session; PTY, cwd, mode, and prior \
481                     context were preserved.\ncwd={}\nmode={}",
482                    state.cwd.display(),
483                    state.mode
484                );
485                if let Some(attach_hint) = configured.attach_hint.as_deref() {
486                    let _ = writeln!(response, "Attach terminal: {attach_hint}");
487                }
488                response.push_str(
489                    "Context and instructions are unchanged. Continue with the existing thread; \
490                     use an explicit mode/workspace change or reset only when intended.\n",
491                );
492            } else {
493                if configured.attached_existing {
494                    response.push_str(
495                        "\nAttached to the existing durable session for this principal/workspace; \
496                         the guardian-owned PTY and cwd were preserved. Use `reset_shell` or an \
497                         explicit workspace/mode change when replacement is intended.\n",
498                    );
499                }
500
501                let _ = write!(
502                    response,
503                    "\n# Environment\nSystem: {}\nMachine: {}\nInitialized in directory: {}\n",
504                    std::env::consts::OS,
505                    std::env::consts::ARCH,
506                    bash_state_guard
507                        .as_ref()
508                        .map_or(folder_to_start.as_path(), |state| state.cwd.as_path())
509                        .display()
510                );
511
512                if command_exists("rg") {
513                    let _ = writeln!(
514                        response,
515                        "\n# Available commands\nUse ripgrep `rg` instead of `grep`/`find -name` — \
516                         it's much faster and respects .gitignore."
517                    );
518                }
519
520                if let Some(attach_hint) = configured.attach_hint.as_deref() {
521                    let _ = writeln!(response, "\nAttach terminal: {attach_hint}");
522                }
523
524                // Explain the actual active policy. A daemon reattach can restore a
525                // different mode than the model redundantly supplied.
526                let active_config = bash_state_guard.as_ref().and_then(active_code_writer_config);
527                let active_mode = bash_state_guard.as_ref().map_or(mode, |state| state.mode);
528                let _ = writeln!(
529                    response,
530                    "\n{}",
531                    crate::utils::mode_prompts::mode_prompt(active_mode, active_config.as_ref())
532                );
533
534                let active_workspace = bash_state_guard
535                    .as_ref()
536                    .map_or(folder_to_start.as_path(), |state| state.workspace_root.as_path());
537
538                let guidelines = load_guidelines(active_workspace);
539                guidelines_bytes = guidelines.len();
540                if !guidelines.is_empty() {
541                    let _ = writeln!(response, "\n# Agent guidelines\n{guidelines}");
542                }
543
544                if let Ok((repo_context, _)) =
545                    crate::utils::repo::get_repo_context(active_workspace)
546                {
547                    context_bytes = repo_context.len();
548                    let _ = writeln!(response, "\n# Workspace structure\n{repo_context}");
549                }
550
551                if !initialize.initial_files_to_read.is_empty() {
552                    let content = read_initial_files_simple(
553                        &initialize.initial_files_to_read,
554                        active_workspace,
555                    );
556                    if !content.is_empty() {
557                        let _ = writeln!(response, "\n# Requested files\n{content}");
558                    }
559                }
560            }
561            if let Some(requested_transition) = recovered_request {
562                let _ = writeln!(
563                    response,
564                    "\nRecovered missing live session: `{requested_transition}` arrived after \
565                     the adapter no longer had session state. Winx safely performed `first_call` \
566                     with the same thread and intended workspace; continue with the returned \
567                     thread_id/workspace_root pair."
568                );
569            }
570        }
571        InitializeType::UserAskedModeChange => {
572            transition = InitializeTransition::ModeChanged;
573            if let Some(state) = bash_state_guard.as_mut() {
574                state.mode = mode;
575                state.bash_command_mode = bash_command_mode;
576                state.file_edit_mode = file_edit_mode;
577                state.write_if_empty_mode = write_if_empty_mode;
578                runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
579                let _ = writeln!(response, "Changed mode to: {mode:?}");
580                let active_config = active_code_writer_config(state);
581                let _ = writeln!(
582                    response,
583                    "\n{}",
584                    crate::utils::mode_prompts::mode_prompt(mode, active_config.as_ref())
585                );
586            } else {
587                return Err(WinxError::BashStateNotInitialized);
588            }
589        }
590        InitializeType::ResetShell => {
591            transition = InitializeTransition::ShellReset;
592            if let Some(state) = bash_state_guard.as_mut() {
593                state.mode = mode;
594                state.bash_command_mode = bash_command_mode;
595                state.file_edit_mode = file_edit_mode;
596                state.write_if_empty_mode = write_if_empty_mode;
597                runtime.configure_session(state, ShellSessionTransition::Reset).await?;
598                response.push_str("Reset shell (new PTY created)\n");
599            } else {
600                return Err(WinxError::BashStateNotInitialized);
601            }
602        }
603        InitializeType::UserAskedChangeWorkspace => {
604            transition = InitializeTransition::WorkspaceChanged;
605            if let Some(state) = bash_state_guard.as_mut() {
606                if folder_to_start.exists() {
607                    state.update_cwd(&folder_to_start)?;
608                    state.update_workspace_root(&folder_to_start)?;
609                    runtime
610                        .configure_session(state, ShellSessionTransition::WorkspaceChange)
611                        .await?;
612                    let _ =
613                        writeln!(response, "Changed workspace to: {}", folder_to_start.display());
614                } else {
615                    let _ = writeln!(
616                        response,
617                        "Warning: Workspace path {} does not exist",
618                        folder_to_start.display()
619                    );
620                }
621            } else {
622                return Err(WinxError::BashStateNotInitialized);
623            }
624        }
625    }
626
627    let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
628    let temporary_artifact = if compact_response {
629        crate::utils::agent_temp::session_info(&state.workspace_root, &state.current_thread_id)
630    } else {
631        crate::utils::agent_temp::prepare_session(&state.workspace_root, &state.current_thread_id)
632    };
633    let temporary_artifact_instruction = if compact_response {
634        format!(
635            "Use temporary_artifact_dir={} for a small stable set of session-local derived \
636             helpers; BashCommand exports the same path as WINX_TEMP_DIR. Reuse helpers instead \
637             of creating CodeMap carriers; helper maps accept one existing file and are limited \
638             to {} unique files / {} calls. Winx audits the directory after Bash; if it exceeds \
639             budget, explicitly remove obsolete helpers before running ordinary commands.",
640            temporary_artifact.directory.display(),
641            crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
642            crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
643        )
644    } else {
645        format!(
646            "Use temporary_artifact_dir={} for session-local derived helpers only; keep names \
647             short, reuse one stable file per purpose, preserve source-path/line provenance, and \
648             treat helpers as non-canonical. Never create a carrier solely for CodeMap. The \
649             directory is created on demand, limited to {} files / {} bytes for this session, and \
650             expired after {} seconds of inactivity. Every BashCommand PTY exports the same path \
651             as WINX_TEMP_DIR; shell-generated helpers must stay beneath it. Winx audits actual \
652             usage after Bash; an overage never triggers deletion automatically. If exceeded, \
653             explicitly inspect and remove obsolete helpers before ordinary commands can continue. \
654             Helper CodeMap accepts one existing file and is limited to {} unique files / {} calls \
655             in this live session.",
656            temporary_artifact.directory.display(),
657            temporary_artifact.max_session_files,
658            temporary_artifact.max_session_bytes,
659            temporary_artifact.ttl_seconds,
660            crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
661            crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
662        )
663    };
664    let _ = writeln!(
665        response,
666        "\nUse thread_id={thread_id} for all winx tool calls.\nUse workspace_root={} for all winx tool calls.\n{temporary_artifact_instruction}\nBefore every reuse, confirm workspace_root still matches the user's current project. Keep this exact pair together. workspace_root identifies this project session; it does not restrict target paths allowed by policy.",
667        state.workspace_root.display(),
668    );
669
670    let (code_writer_policy_strength, bypass) =
671        bash_state_guard.as_ref().map_or((None, Vec::new()), active_code_writer_policy);
672    if let Some(state) = bash_state_guard.as_ref() {
673        warn_code_writer_policy_once(state, &bypass);
674    }
675    if compact_response {
676        if let Some(strength) = code_writer_policy_strength {
677            let _ = writeln!(
678                response,
679                "code_writer_policy={strength} shell_spawners_present={}",
680                !bypass.is_empty()
681            );
682        }
683    } else {
684        append_code_writer_policy_warning(&mut response, &bypass);
685        crate::utils::orchestration::append_initialize_instructions(&mut response);
686    }
687
688    Ok(InitializeOutcome {
689        text: response,
690        transition,
691        recovered_missing_session: recovered_request.is_some(),
692        context_bytes,
693        guidelines_bytes,
694        initial_files_count,
695        compact_response,
696        code_writer_policy_strength,
697        shell_spawners_present: !bypass.is_empty(),
698        temporary_artifact_dir: temporary_artifact.directory,
699        temporary_artifact_ttl_seconds: temporary_artifact.ttl_seconds,
700        temporary_artifact_max_bytes: temporary_artifact.max_total_bytes,
701        temporary_artifact_max_session_bytes: temporary_artifact.max_session_bytes,
702        temporary_artifact_max_file_bytes: temporary_artifact.max_file_bytes,
703        temporary_artifact_max_files: temporary_artifact.max_session_files,
704        temporary_code_map_max_calls: crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_CALLS,
705        temporary_code_map_max_unique_files:
706            crate::utils::agent_temp::MAX_DERIVED_CODE_MAP_UNIQUE_FILES,
707    })
708}