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