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