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