Skip to main content

kaish_kernel/
kernel.rs

1//! The Kernel (核) — the heart of kaish.
2//!
3//! The Kernel owns and coordinates all core components:
4//! - Interpreter state (scope, $?)
5//! - Tool registry (builtins, user tools, MCP)
6//! - VFS router (mount points)
7//! - Job manager (background jobs)
8//!
9//! # Architecture
10//!
11//! ```text
12//! ┌────────────────────────────────────────────────────────────┐
13//! │                         Kernel (核)                         │
14//! │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
15//! │  │   Scope      │  │ ToolRegistry │  │  VfsRouter       │  │
16//! │  │  (variables) │  │   (builtins, │  │  (mount points)  │  │
17//! │  │              │  │    MCP, user)│  │                  │  │
18//! │  └──────────────┘  └──────────────┘  └──────────────────┘  │
19//! │  ┌──────────────────────────────┐  ┌──────────────────┐    │
20//! │  │  JobManager (background)     │  │  ExecResult ($?) │    │
21//! │  └──────────────────────────────┘  └──────────────────┘    │
22//! └────────────────────────────────────────────────────────────┘
23//! ```
24
25use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use anyhow::{Context, Result};
30use tokio::sync::RwLock;
31
32use async_trait::async_trait;
33
34use crate::ast::{Arg, Command, Expr, FileTestOp, Stmt, StringPart, TestExpr, ToolDef, Value, BinaryOp};
35use crate::backend::{BackendError, KernelBackend};
36use kaish_glob::glob_match;
37use crate::dispatch::{CommandDispatcher, PipelinePosition};
38use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_bool, value_to_string, ControlFlow, ExecResult, Scope};
39use crate::parser::parse;
40use crate::scheduler::{drain_to_stream, is_bool_type, schema_param_lookup, stderr_stream, BoundedStream, JobManager, PipelineRunner, StderrReceiver, DEFAULT_STREAM_MAX_SIZE};
41use crate::tools::{extract_output_format, register_builtins, resolve_in_path, ExecContext, ToolArgs, ToolRegistry};
42use crate::validator::{Severity, Validator};
43use crate::vfs::{BuiltinFs, JobFs, LocalFs, MemoryFs, VfsRouter};
44
45/// VFS mount mode determines how the local filesystem is exposed.
46///
47/// Different modes trade off convenience vs. security:
48/// - `Passthrough` gives native path access (best for human REPL use)
49/// - `Sandboxed` restricts access to a subtree (safer for agents)
50/// - `NoLocal` provides complete isolation (tests, pure memory mode)
51#[derive(Debug, Clone)]
52pub enum VfsMountMode {
53    /// LocalFs at "/" — native paths work directly.
54    ///
55    /// Full filesystem access. Use for human-operated REPL sessions where
56    /// native paths like `/home/user/project` should just work.
57    ///
58    /// Mounts:
59    /// - `/` → LocalFs("/")
60    /// - `/v` → MemoryFs (blob storage)
61    Passthrough,
62
63    /// Transparent sandbox — paths look native but access is restricted.
64    ///
65    /// The local filesystem is mounted at its real path (e.g., `/home/user`),
66    /// so `/home/user/src/project` just works. But paths outside the sandbox
67    /// root are not accessible.
68    ///
69    /// **Note:** This only restricts VFS (builtin) operations. External commands
70    /// bypass the sandbox entirely — see [`KernelConfig::allow_external_commands`].
71    ///
72    /// Mounts:
73    /// - `/` → MemoryFs (catches paths outside sandbox)
74    /// - `{root}` → LocalFs(root)  (e.g., `/home/user` → LocalFs)
75    /// - `/tmp` → LocalFs("/tmp")
76    /// - `/v` → MemoryFs (blob storage)
77    Sandboxed {
78        /// Root path for local filesystem. Defaults to `$HOME`.
79        /// Can be restricted further, e.g., `~/src`.
80        root: Option<PathBuf>,
81    },
82
83    /// No local filesystem. Memory only.
84    ///
85    /// Complete isolation — no access to the host filesystem.
86    /// Useful for tests or pure sandboxed execution.
87    ///
88    /// Mounts:
89    /// - `/` → MemoryFs
90    /// - `/tmp` → MemoryFs
91    /// - `/v` → MemoryFs
92    NoLocal,
93}
94
95impl Default for VfsMountMode {
96    fn default() -> Self {
97        VfsMountMode::Sandboxed { root: None }
98    }
99}
100
101/// Configuration for kernel initialization.
102#[derive(Debug, Clone)]
103pub struct KernelConfig {
104    /// Name of this kernel (for identification).
105    pub name: String,
106
107    /// VFS mount mode — controls how local filesystem is exposed.
108    pub vfs_mode: VfsMountMode,
109
110    /// Initial working directory (VFS path).
111    pub cwd: PathBuf,
112
113    /// Whether to skip pre-execution validation.
114    ///
115    /// When false (default), scripts are validated before execution to catch
116    /// errors early. Set to true to skip validation for performance or to
117    /// allow dynamic/external commands.
118    pub skip_validation: bool,
119
120    /// When true, standalone external commands inherit stdio for real-time output.
121    ///
122    /// Set by script runner and REPL for human-visible output.
123    /// Not set by MCP server (output must be captured for structured responses).
124    pub interactive: bool,
125
126    /// Ignore file configuration for file-walking tools.
127    pub ignore_config: crate::ignore_config::IgnoreConfig,
128
129    /// Output size limit configuration for agent safety.
130    pub output_limit: crate::output_limit::OutputLimitConfig,
131
132    /// Whether external command execution (PATH lookup, `exec`, `spawn`) is allowed.
133    ///
134    /// When `true` (default), commands not found as builtins are resolved via PATH
135    /// and executed as child processes. When `false`, only kaish builtins and
136    /// backend-registered tools (MCP) are available.
137    ///
138    /// **Security:** External commands bypass the VFS sandbox entirely — they see
139    /// the real filesystem, network, and environment. Set to `false` when running
140    /// untrusted input.
141    pub allow_external_commands: bool,
142
143    /// Enable confirmation latch for dangerous operations (set -o latch).
144    ///
145    /// When enabled, destructive operations like `rm` require nonce confirmation.
146    /// Can also be enabled at runtime with `set -o latch` or via `KAISH_LATCH=1`.
147    pub latch_enabled: bool,
148
149    /// Enable trash-on-delete for rm (set -o trash).
150    ///
151    /// When enabled, small files are moved to freedesktop.org Trash instead of
152    /// being permanently deleted. Can also be enabled at runtime with `set -o trash`
153    /// or via `KAISH_TRASH=1`.
154    pub trash_enabled: bool,
155
156    /// Shared nonce store for cross-request confirmation latch.
157    ///
158    /// When `Some`, the kernel uses this store instead of creating a fresh one.
159    /// This allows nonces issued in one MCP `execute()` call to be validated
160    /// in a subsequent call. When `None` (default), a fresh store is created.
161    pub nonce_store: Option<crate::nonce::NonceStore>,
162}
163
164/// Get the default sandbox root ($HOME).
165fn default_sandbox_root() -> PathBuf {
166    std::env::var("HOME")
167        .map(PathBuf::from)
168        .unwrap_or_else(|_| PathBuf::from("/"))
169}
170
171impl Default for KernelConfig {
172    fn default() -> Self {
173        let home = default_sandbox_root();
174        Self {
175            name: "default".to_string(),
176            vfs_mode: VfsMountMode::Sandboxed { root: None },
177            cwd: home,
178            skip_validation: false,
179            interactive: false,
180            ignore_config: crate::ignore_config::IgnoreConfig::none(),
181            output_limit: crate::output_limit::OutputLimitConfig::none(),
182            allow_external_commands: true,
183            latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
184            trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
185            nonce_store: None,
186        }
187    }
188}
189
190impl KernelConfig {
191    /// Create a transient kernel config (sandboxed, for temporary use).
192    pub fn transient() -> Self {
193        let home = default_sandbox_root();
194        Self {
195            name: "transient".to_string(),
196            vfs_mode: VfsMountMode::Sandboxed { root: None },
197            cwd: home,
198            skip_validation: false,
199            interactive: false,
200            ignore_config: crate::ignore_config::IgnoreConfig::none(),
201            output_limit: crate::output_limit::OutputLimitConfig::none(),
202            allow_external_commands: true,
203            latch_enabled: false,
204            trash_enabled: false,
205            nonce_store: None,
206        }
207    }
208
209    /// Create a kernel config with the given name (sandboxed by default).
210    pub fn named(name: &str) -> Self {
211        let home = default_sandbox_root();
212        Self {
213            name: name.to_string(),
214            vfs_mode: VfsMountMode::Sandboxed { root: None },
215            cwd: home,
216            skip_validation: false,
217            interactive: false,
218            ignore_config: crate::ignore_config::IgnoreConfig::none(),
219            output_limit: crate::output_limit::OutputLimitConfig::none(),
220            allow_external_commands: true,
221            latch_enabled: false,
222            trash_enabled: false,
223            nonce_store: None,
224        }
225    }
226
227    /// Create a REPL config with passthrough filesystem access.
228    ///
229    /// Native paths like `/home/user/project` work directly.
230    /// The cwd is set to the actual current working directory.
231    pub fn repl() -> Self {
232        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
233        Self {
234            name: "repl".to_string(),
235            vfs_mode: VfsMountMode::Passthrough,
236            cwd,
237            skip_validation: false,
238            interactive: false,
239            ignore_config: crate::ignore_config::IgnoreConfig::none(),
240            output_limit: crate::output_limit::OutputLimitConfig::none(),
241            allow_external_commands: true,
242            latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
243            trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
244            nonce_store: None,
245        }
246    }
247
248    /// Create an MCP server config with sandboxed filesystem access.
249    ///
250    /// Local filesystem is accessible at its real path (e.g., `/home/user`),
251    /// but sandboxed to `$HOME`. Paths outside the sandbox are not accessible
252    /// through builtins. External commands still access the real filesystem —
253    /// use `.with_allow_external_commands(false)` to block them.
254    pub fn mcp() -> Self {
255        let home = default_sandbox_root();
256        Self {
257            name: "mcp".to_string(),
258            vfs_mode: VfsMountMode::Sandboxed { root: None },
259            cwd: home,
260            skip_validation: false,
261            interactive: false,
262            ignore_config: crate::ignore_config::IgnoreConfig::mcp(),
263            output_limit: crate::output_limit::OutputLimitConfig::mcp(),
264            allow_external_commands: true,
265            latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
266            trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
267            nonce_store: None,
268        }
269    }
270
271    /// Create an MCP server config with a custom sandbox root.
272    ///
273    /// Use this to restrict access to a subdirectory like `~/src`.
274    pub fn mcp_with_root(root: PathBuf) -> Self {
275        Self {
276            name: "mcp".to_string(),
277            vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
278            cwd: root,
279            skip_validation: false,
280            interactive: false,
281            ignore_config: crate::ignore_config::IgnoreConfig::mcp(),
282            output_limit: crate::output_limit::OutputLimitConfig::mcp(),
283            allow_external_commands: true,
284            latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
285            trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
286            nonce_store: None,
287        }
288    }
289
290    /// Create a config with no local filesystem (memory only).
291    ///
292    /// Complete isolation: no local filesystem and external commands are disabled.
293    /// Useful for tests or pure sandboxed execution.
294    pub fn isolated() -> Self {
295        Self {
296            name: "isolated".to_string(),
297            vfs_mode: VfsMountMode::NoLocal,
298            cwd: PathBuf::from("/"),
299            skip_validation: false,
300            interactive: false,
301            ignore_config: crate::ignore_config::IgnoreConfig::none(),
302            output_limit: crate::output_limit::OutputLimitConfig::none(),
303            allow_external_commands: false,
304            latch_enabled: false,
305            trash_enabled: false,
306            nonce_store: None,
307        }
308    }
309
310    /// Set the VFS mount mode.
311    pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
312        self.vfs_mode = mode;
313        self
314    }
315
316    /// Set the initial working directory.
317    pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
318        self.cwd = cwd;
319        self
320    }
321
322    /// Skip pre-execution validation.
323    pub fn with_skip_validation(mut self, skip: bool) -> Self {
324        self.skip_validation = skip;
325        self
326    }
327
328    /// Enable interactive mode (external commands inherit stdio).
329    pub fn with_interactive(mut self, interactive: bool) -> Self {
330        self.interactive = interactive;
331        self
332    }
333
334    /// Set the ignore file configuration.
335    pub fn with_ignore_config(mut self, config: crate::ignore_config::IgnoreConfig) -> Self {
336        self.ignore_config = config;
337        self
338    }
339
340    /// Set the output limit configuration.
341    pub fn with_output_limit(mut self, config: crate::output_limit::OutputLimitConfig) -> Self {
342        self.output_limit = config;
343        self
344    }
345
346    /// Set whether external command execution is allowed.
347    ///
348    /// When `false`, commands not found as builtins produce "command not found"
349    /// instead of searching PATH. The `exec` and `spawn` builtins also return
350    /// errors. Use this to prevent VFS sandbox bypass via external binaries.
351    pub fn with_allow_external_commands(mut self, allow: bool) -> Self {
352        self.allow_external_commands = allow;
353        self
354    }
355
356    /// Enable or disable confirmation latch at startup.
357    pub fn with_latch(mut self, enabled: bool) -> Self {
358        self.latch_enabled = enabled;
359        self
360    }
361
362    /// Enable or disable trash-on-delete at startup.
363    pub fn with_trash(mut self, enabled: bool) -> Self {
364        self.trash_enabled = enabled;
365        self
366    }
367
368    /// Use a shared nonce store for cross-request confirmation latch.
369    ///
370    /// Pass a `NonceStore` that outlives individual kernel instances so nonces
371    /// issued in one MCP `execute()` call can be validated in subsequent calls.
372    pub fn with_nonce_store(mut self, store: crate::nonce::NonceStore) -> Self {
373        self.nonce_store = Some(store);
374        self
375    }
376}
377
378/// The Kernel (核) — executes kaish code.
379///
380/// This is the primary interface for running kaish commands. It owns all
381/// the runtime state: variables, tools, VFS, jobs, and persistence.
382pub struct Kernel {
383    /// Kernel name.
384    name: String,
385    /// Variable scope.
386    scope: RwLock<Scope>,
387    /// Tool registry.
388    tools: Arc<ToolRegistry>,
389    /// User-defined tools (from `tool name { body }` statements).
390    user_tools: RwLock<HashMap<String, ToolDef>>,
391    /// Virtual filesystem router.
392    vfs: Arc<VfsRouter>,
393    /// Background job manager.
394    jobs: Arc<JobManager>,
395    /// Pipeline runner.
396    runner: PipelineRunner,
397    /// Execution context (cwd, stdin, etc.).
398    exec_ctx: RwLock<ExecContext>,
399    /// Whether to skip pre-execution validation.
400    skip_validation: bool,
401    /// When true, standalone external commands inherit stdio for real-time output.
402    interactive: bool,
403    /// Whether external command execution is allowed.
404    allow_external_commands: bool,
405    /// Receiver for the kernel stderr stream.
406    ///
407    /// Pipeline stages write to the corresponding `StderrStream` (set on ExecContext).
408    /// The kernel drains this after each statement in `execute_streaming`.
409    stderr_receiver: tokio::sync::Mutex<StderrReceiver>,
410    /// Cancellation token for interrupting execution (Ctrl-C).
411    ///
412    /// Protected by `std::sync::Mutex` (not tokio) because the SIGINT handler
413    /// needs sync access. Each `execute()` call gets a fresh child token;
414    /// `cancel()` cancels the current token and replaces it.
415    cancel_token: std::sync::Mutex<tokio_util::sync::CancellationToken>,
416    /// Terminal state for job control (interactive mode only, Unix only).
417    #[cfg(unix)]
418    terminal_state: Option<Arc<crate::terminal::TerminalState>>,
419}
420
421impl Kernel {
422    /// Create a new kernel with the given configuration.
423    pub fn new(config: KernelConfig) -> Result<Self> {
424        let mut vfs = Self::setup_vfs(&config);
425        let jobs = Arc::new(JobManager::new());
426
427        // Mount JobFs for job observability at /v/jobs
428        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
429
430        Self::assemble(config, vfs, jobs, |_| {}, |vfs_ref, tools| {
431            ExecContext::with_vfs_and_tools(vfs_ref.clone(), tools.clone())
432        })
433    }
434
435    /// Set up VFS based on mount mode.
436    fn setup_vfs(config: &KernelConfig) -> VfsRouter {
437        let mut vfs = VfsRouter::new();
438
439        match &config.vfs_mode {
440            VfsMountMode::Passthrough => {
441                // LocalFs at "/" — native paths work directly
442                vfs.mount("/", LocalFs::new(PathBuf::from("/")));
443                // Memory for blobs
444                vfs.mount("/v", MemoryFs::new());
445            }
446            VfsMountMode::Sandboxed { root } => {
447                // Memory at root for safety (catches paths outside sandbox)
448                vfs.mount("/", MemoryFs::new());
449                vfs.mount("/v", MemoryFs::new());
450
451                // Real /tmp for interop with other processes
452                vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
453
454                // Mount XDG runtime dir for spill files and socket access
455                let runtime = crate::paths::xdg_runtime_dir();
456                if runtime.exists() {
457                    let runtime_str = runtime.to_string_lossy().to_string();
458                    vfs.mount(&runtime_str, LocalFs::new(runtime));
459                }
460
461                // Resolve the sandbox root (defaults to $HOME)
462                let local_root = root.clone().unwrap_or_else(|| {
463                    std::env::var("HOME")
464                        .map(PathBuf::from)
465                        .unwrap_or_else(|_| PathBuf::from("/"))
466                });
467
468                // Mount at the real path for transparent access
469                // e.g., /home/atobey → LocalFs("/home/atobey")
470                // so /home/atobey/src/kaish just works
471                let mount_point = local_root.to_string_lossy().to_string();
472                vfs.mount(&mount_point, LocalFs::new(local_root));
473            }
474            VfsMountMode::NoLocal => {
475                // Pure memory mode — no local filesystem
476                vfs.mount("/", MemoryFs::new());
477                vfs.mount("/tmp", MemoryFs::new());
478                vfs.mount("/v", MemoryFs::new());
479            }
480        }
481
482        vfs
483    }
484
485    /// Create a transient kernel (no persistence).
486    pub fn transient() -> Result<Self> {
487        Self::new(KernelConfig::transient())
488    }
489
490    /// Create a kernel with a custom backend and `/v/*` virtual path support.
491    ///
492    /// This is the constructor for embedding kaish in other systems that provide
493    /// their own storage backend (e.g., CRDT-backed storage in kaijutsu).
494    ///
495    /// A `VirtualOverlayBackend` routes paths automatically:
496    /// - `/v/*` → Internal VFS (JobFs at `/v/jobs`, MemoryFs at `/v/blobs`)
497    /// - Everything else → Your custom backend
498    ///
499    /// The optional `configure_vfs` closure lets you add additional virtual mounts
500    /// (e.g., `/v/docs` for CRDT blocks) after the built-in mounts are set up.
501    ///
502    /// **Note:** The config's `vfs_mode` is ignored — all non-`/v/*` path routing
503    /// is handled by your custom backend. The config is only used for `name`, `cwd`,
504    /// `skip_validation`, and `interactive`.
505    ///
506    /// # Example
507    ///
508    /// ```ignore
509    /// // Simple: default /v/* mounts only
510    /// let kernel = Kernel::with_backend(backend, config, |_| {}, |_| {})?;
511    ///
512    /// // With custom mounts
513    /// let kernel = Kernel::with_backend(backend, config, |vfs| {
514    ///     vfs.mount_arc("/v/docs", docs_fs);
515    ///     vfs.mount_arc("/v/g", git_fs);
516    /// }, |_| {})?;
517    ///
518    /// // With custom tools
519    /// let kernel = Kernel::with_backend(backend, config, |_| {}, |tools| {
520    ///     tools.register(MyCustomTool::new());
521    /// })?;
522    /// ```
523    pub fn with_backend(
524        backend: Arc<dyn KernelBackend>,
525        config: KernelConfig,
526        configure_vfs: impl FnOnce(&mut VfsRouter),
527        configure_tools: impl FnOnce(&mut ToolRegistry),
528    ) -> Result<Self> {
529        use crate::backend::VirtualOverlayBackend;
530
531        let mut vfs = VfsRouter::new();
532        let jobs = Arc::new(JobManager::new());
533
534        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
535        vfs.mount("/v/blobs", MemoryFs::new());
536
537        // Let caller add custom mounts (e.g., /v/docs, /v/g)
538        configure_vfs(&mut vfs);
539
540        Self::assemble(config, vfs, jobs, configure_tools, |vfs_arc: &Arc<VfsRouter>, _: &Arc<ToolRegistry>| {
541            let overlay: Arc<dyn KernelBackend> =
542                Arc::new(VirtualOverlayBackend::new(backend, vfs_arc.clone()));
543            ExecContext::with_backend(overlay)
544        })
545    }
546
547    /// Shared assembly: wires up tools, runner, scope, and ExecContext.
548    ///
549    /// The `make_ctx` closure receives the VFS and tools so backends that need
550    /// them (like `LocalBackend::with_tools`) can capture them. Custom backends
551    /// that already have their own storage can ignore these parameters.
552    fn assemble(
553        config: KernelConfig,
554        mut vfs: VfsRouter,
555        jobs: Arc<JobManager>,
556        configure_tools: impl FnOnce(&mut ToolRegistry),
557        make_ctx: impl FnOnce(&Arc<VfsRouter>, &Arc<ToolRegistry>) -> ExecContext,
558    ) -> Result<Self> {
559        let KernelConfig { name, cwd, skip_validation, interactive, ignore_config, output_limit, allow_external_commands, latch_enabled, trash_enabled, nonce_store, .. } = config;
560
561        let mut tools = ToolRegistry::new();
562        register_builtins(&mut tools);
563        configure_tools(&mut tools);
564        let tools = Arc::new(tools);
565
566        // Mount BuiltinFs so `ls /v/bin` lists builtins
567        vfs.mount("/v/bin", BuiltinFs::new(tools.clone()));
568
569        let vfs = Arc::new(vfs);
570
571        let runner = PipelineRunner::new(tools.clone());
572
573        let (stderr_writer, stderr_receiver) = stderr_stream();
574
575        let mut exec_ctx = make_ctx(&vfs, &tools);
576        exec_ctx.set_cwd(cwd);
577        exec_ctx.set_job_manager(jobs.clone());
578        exec_ctx.set_tool_schemas(tools.schemas());
579        exec_ctx.set_tools(tools.clone());
580        exec_ctx.stderr = Some(stderr_writer);
581        exec_ctx.ignore_config = ignore_config;
582        exec_ctx.output_limit = output_limit;
583        exec_ctx.allow_external_commands = allow_external_commands;
584        if let Some(store) = nonce_store {
585            exec_ctx.nonce_store = store;
586        }
587
588        Ok(Self {
589            name,
590            scope: RwLock::new({
591                let mut scope = Scope::new();
592                if let Ok(home) = std::env::var("HOME") {
593                    scope.set("HOME", Value::String(home));
594                }
595                scope.set_latch_enabled(latch_enabled);
596                scope.set_trash_enabled(trash_enabled);
597                scope
598            }),
599            tools,
600            user_tools: RwLock::new(HashMap::new()),
601            vfs,
602            jobs,
603            runner,
604            exec_ctx: RwLock::new(exec_ctx),
605            skip_validation,
606            interactive,
607            allow_external_commands,
608            stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
609            cancel_token: std::sync::Mutex::new(tokio_util::sync::CancellationToken::new()),
610            #[cfg(unix)]
611            terminal_state: None,
612        })
613    }
614
615    /// Get the kernel name.
616    pub fn name(&self) -> &str {
617        &self.name
618    }
619
620    /// Initialize terminal state for interactive job control.
621    ///
622    /// Call this after kernel creation when running as an interactive REPL
623    /// and stdin is a TTY. Sets up process groups and signal handling.
624    #[cfg(unix)]
625    pub fn init_terminal(&mut self) {
626        if !self.interactive {
627            return;
628        }
629        match crate::terminal::TerminalState::init() {
630            Ok(state) => {
631                let state = Arc::new(state);
632                self.terminal_state = Some(state.clone());
633                // Set on exec_ctx so builtins (fg, bg, kill) can access it
634                self.exec_ctx.get_mut().terminal_state = Some(state);
635                tracing::debug!("terminal job control initialized");
636            }
637            Err(e) => {
638                tracing::warn!("failed to initialize terminal job control: {}", e);
639            }
640        }
641    }
642
643    /// Cancel the current execution.
644    ///
645    /// This cancels the current cancellation token, causing any execution
646    /// loop to exit at the next checkpoint with exit code 130 (SIGINT).
647    /// A fresh token is installed for the next `execute()` call.
648    pub fn cancel(&self) {
649        #[allow(clippy::expect_used)]
650        let token = self.cancel_token.lock().expect("cancel_token poisoned");
651        token.cancel();
652    }
653
654    /// Check if the current execution has been cancelled.
655    pub fn is_cancelled(&self) -> bool {
656        #[allow(clippy::expect_used)]
657        let token = self.cancel_token.lock().expect("cancel_token poisoned");
658        token.is_cancelled()
659    }
660
661    /// Reset the cancellation token (called at the start of each execute).
662    fn reset_cancel(&self) -> tokio_util::sync::CancellationToken {
663        #[allow(clippy::expect_used)]
664        let mut token = self.cancel_token.lock().expect("cancel_token poisoned");
665        if token.is_cancelled() {
666            *token = tokio_util::sync::CancellationToken::new();
667        }
668        token.clone()
669    }
670
671    /// Execute kaish source code.
672    ///
673    /// Returns the result of the last statement executed.
674    pub async fn execute(&self, input: &str) -> Result<ExecResult> {
675        self.execute_streaming(input, &mut |_| {}).await
676    }
677
678    /// Execute kaish source code with a per-statement callback.
679    ///
680    /// Each statement's result is passed to `on_output` as it completes,
681    /// allowing callers to flush output incrementally (e.g., print builtin
682    /// output immediately rather than buffering until the script finishes).
683    ///
684    /// External commands in interactive mode already stream to the terminal
685    /// via `Stdio::inherit()`, so the callback mainly handles builtins.
686    #[tracing::instrument(level = "info", skip(self, on_output), fields(input_len = input.len()))]
687    pub async fn execute_streaming(
688        &self,
689        input: &str,
690        on_output: &mut dyn FnMut(&ExecResult),
691    ) -> Result<ExecResult> {
692        let program = parse(input).map_err(|errors| {
693            let msg = errors
694                .iter()
695                .map(|e| e.to_string())
696                .collect::<Vec<_>>()
697                .join("; ");
698            anyhow::anyhow!("parse error: {}", msg)
699        })?;
700
701        // AST display mode: show AST instead of executing
702        {
703            let scope = self.scope.read().await;
704            if scope.show_ast() {
705                let output = format!("{:#?}\n", program);
706                return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(output)));
707            }
708        }
709
710        // Pre-execution validation
711        if !self.skip_validation {
712            let user_tools = self.user_tools.read().await;
713            let validator = Validator::new(&self.tools, &user_tools);
714            let issues = validator.validate(&program);
715
716            // Collect errors (warnings are logged but don't prevent execution)
717            let errors: Vec<_> = issues
718                .iter()
719                .filter(|i| i.severity == Severity::Error)
720                .collect();
721
722            if !errors.is_empty() {
723                let error_msg = errors
724                    .iter()
725                    .map(|e| e.format(input))
726                    .collect::<Vec<_>>()
727                    .join("\n");
728                return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
729            }
730
731            // Log warnings via tracing (trace level to avoid noise)
732            for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
733                tracing::trace!("validation: {}", warning.format(input));
734            }
735        }
736
737        let mut result = ExecResult::success("");
738
739        // Reset cancellation token for this execution.
740        let cancel = self.reset_cancel();
741
742        for stmt in program.statements {
743            if matches!(stmt, Stmt::Empty) {
744                continue;
745            }
746
747            // Cancellation checkpoint
748            if cancel.is_cancelled() {
749                result.code = 130;
750                return Ok(result);
751            }
752
753            let flow = self.execute_stmt_flow(&stmt).await?;
754
755            // Drain any stderr written by pipeline stages during this statement.
756            // This captures stderr from intermediate pipeline stages that would
757            // otherwise be lost (only the last stage's result is returned).
758            let drained_stderr = {
759                let mut receiver = self.stderr_receiver.lock().await;
760                receiver.drain_lossy()
761            };
762
763            match flow {
764                ControlFlow::Normal(mut r) => {
765                    if !drained_stderr.is_empty() {
766                        if !r.err.is_empty() && !r.err.ends_with('\n') {
767                            r.err.push('\n');
768                        }
769                        // Prepend pipeline stderr before the last stage's stderr
770                        let combined = format!("{}{}", drained_stderr, r.err);
771                        r.err = combined;
772                    }
773                    on_output(&r);
774                    // Carry the last statement's structured output for MCP TOON encoding.
775                    // Must be done here (not in accumulate_result) because accumulate_result
776                    // is also used in loops where per-iteration output would be wrong.
777                    let last_output = r.output.clone();
778                    accumulate_result(&mut result, &r);
779                    result.output = last_output;
780                }
781                ControlFlow::Exit { code } => {
782                    if !drained_stderr.is_empty() {
783                        result.err.push_str(&drained_stderr);
784                    }
785                    result.code = code;
786                    return Ok(result);
787                }
788                ControlFlow::Return { mut value } => {
789                    if !drained_stderr.is_empty() {
790                        value.err = format!("{}{}", drained_stderr, value.err);
791                    }
792                    on_output(&value);
793                    result = value;
794                }
795                ControlFlow::Break { result: mut r, .. } | ControlFlow::Continue { result: mut r, .. } => {
796                    if !drained_stderr.is_empty() {
797                        r.err = format!("{}{}", drained_stderr, r.err);
798                    }
799                    on_output(&r);
800                    result = r;
801                }
802            }
803        }
804
805        Ok(result)
806    }
807
808    /// Execute a single statement, returning control flow information.
809    fn execute_stmt_flow<'a>(
810        &'a self,
811        stmt: &'a Stmt,
812    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
813        use tracing::Instrument;
814        let span = tracing::debug_span!("execute_stmt_flow", stmt_type = %stmt.kind_name());
815        Box::pin(async move {
816        match stmt {
817            Stmt::Assignment(assign) => {
818                // Use async evaluator to support command substitution
819                let value = self.eval_expr_async(&assign.value).await
820                    .context("failed to evaluate assignment")?;
821                let mut scope = self.scope.write().await;
822                if assign.local {
823                    // local: set in innermost (current function) frame
824                    scope.set(&assign.name, value.clone());
825                } else {
826                    // non-local: update existing or create in root frame
827                    scope.set_global(&assign.name, value.clone());
828                }
829                drop(scope);
830
831                // Assignments don't produce output (like sh)
832                Ok(ControlFlow::ok(ExecResult::success("")))
833            }
834            Stmt::Command(cmd) => {
835                // Route single commands through execute_pipeline for a unified path.
836                // This ensures all commands go through the dispatcher chain.
837                let pipeline = crate::ast::Pipeline {
838                    commands: vec![cmd.clone()],
839                    background: false,
840                };
841                let result = self.execute_pipeline(&pipeline).await?;
842                self.update_last_result(&result).await;
843
844                // Check for error exit mode (set -e)
845                if !result.ok() {
846                    let scope = self.scope.read().await;
847                    if scope.error_exit_enabled() {
848                        return Ok(ControlFlow::exit_code(result.code));
849                    }
850                }
851
852                Ok(ControlFlow::ok(result))
853            }
854            Stmt::Pipeline(pipeline) => {
855                let result = self.execute_pipeline(pipeline).await?;
856                self.update_last_result(&result).await;
857
858                // Check for error exit mode (set -e)
859                if !result.ok() {
860                    let scope = self.scope.read().await;
861                    if scope.error_exit_enabled() {
862                        return Ok(ControlFlow::exit_code(result.code));
863                    }
864                }
865
866                Ok(ControlFlow::ok(result))
867            }
868            Stmt::If(if_stmt) => {
869                // Use async evaluator to support command substitution in conditions
870                let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
871
872                let branch = if is_truthy(&cond_value) {
873                    &if_stmt.then_branch
874                } else {
875                    if_stmt.else_branch.as_deref().unwrap_or(&[])
876                };
877
878                let mut result = ExecResult::success("");
879                for stmt in branch {
880                    let flow = self.execute_stmt_flow(stmt).await?;
881                    match flow {
882                        ControlFlow::Normal(r) => {
883                            accumulate_result(&mut result, &r);
884                            self.drain_stderr_into(&mut result).await;
885                        }
886                        other => {
887                            self.drain_stderr_into(&mut result).await;
888                            return Ok(other);
889                        }
890                    }
891                }
892                Ok(ControlFlow::ok(result))
893            }
894            Stmt::For(for_loop) => {
895                // Evaluate all items and collect values for iteration
896                // Use async evaluator to support command substitution like $(seq 1 5)
897                let mut items: Vec<Value> = Vec::new();
898                for item_expr in &for_loop.items {
899                    // Glob expansion in for-loop items: `for f in *.txt`
900                    if let Expr::GlobPattern(pattern) = item_expr {
901                        let glob_enabled = {
902                            let scope = self.scope.read().await;
903                            scope.glob_enabled()
904                        };
905                        if glob_enabled {
906                            let (paths, cwd) = {
907                                let ctx = self.exec_ctx.read().await;
908                                let paths = ctx.expand_glob(pattern).await
909                                    .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
910                                let cwd = ctx.resolve_path(".");
911                                (paths, cwd)
912                            };
913                            if paths.is_empty() {
914                                return Err(anyhow::anyhow!("no matches: {}", pattern));
915                            }
916                            for path in paths {
917                                let display = if !pattern.starts_with('/') {
918                                    path.strip_prefix(&cwd)
919                                        .unwrap_or(&path)
920                                        .to_string_lossy().into_owned()
921                                } else {
922                                    path.to_string_lossy().into_owned()
923                                };
924                                items.push(Value::String(display));
925                            }
926                            continue;
927                        }
928                    }
929                    let item = self.eval_expr_async(item_expr).await?;
930                    // NO implicit word splitting - arrays iterate, strings stay whole
931                    match &item {
932                        // JSON arrays iterate over elements
933                        Value::Json(serde_json::Value::Array(arr)) => {
934                            for elem in arr {
935                                items.push(json_to_value(elem.clone()));
936                            }
937                        }
938                        // Strings are ONE value - no splitting!
939                        // Use $(split "$VAR") for explicit splitting
940                        Value::String(_) => {
941                            items.push(item);
942                        }
943                        // Other values as-is
944                        _ => items.push(item),
945                    }
946                }
947
948                let mut result = ExecResult::success("");
949                {
950                    let mut scope = self.scope.write().await;
951                    scope.push_frame();
952                }
953
954                'outer: for item in items {
955                    // Cancellation checkpoint per iteration
956                    if self.is_cancelled() {
957                        let mut scope = self.scope.write().await;
958                        scope.pop_frame();
959                        result.code = 130;
960                        return Ok(ControlFlow::ok(result));
961                    }
962                    {
963                        let mut scope = self.scope.write().await;
964                        scope.set(&for_loop.variable, item);
965                    }
966                    for stmt in &for_loop.body {
967                        let mut flow = self.execute_stmt_flow(stmt).await?;
968                        self.drain_stderr_into(&mut result).await;
969                        match &mut flow {
970                            ControlFlow::Normal(r) => {
971                                accumulate_result(&mut result, r);
972                                if !r.ok() {
973                                    let scope = self.scope.read().await;
974                                    if scope.error_exit_enabled() {
975                                        drop(scope);
976                                        let mut scope = self.scope.write().await;
977                                        scope.pop_frame();
978                                        return Ok(ControlFlow::exit_code(r.code));
979                                    }
980                                }
981                            }
982                            ControlFlow::Break { .. } => {
983                                if flow.decrement_level() {
984                                    break 'outer;
985                                }
986                                let mut scope = self.scope.write().await;
987                                scope.pop_frame();
988                                return Ok(flow);
989                            }
990                            ControlFlow::Continue { .. } => {
991                                if flow.decrement_level() {
992                                    continue 'outer;
993                                }
994                                let mut scope = self.scope.write().await;
995                                scope.pop_frame();
996                                return Ok(flow);
997                            }
998                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
999                                let mut scope = self.scope.write().await;
1000                                scope.pop_frame();
1001                                return Ok(flow);
1002                            }
1003                        }
1004                    }
1005                }
1006
1007                {
1008                    let mut scope = self.scope.write().await;
1009                    scope.pop_frame();
1010                }
1011                Ok(ControlFlow::ok(result))
1012            }
1013            Stmt::While(while_loop) => {
1014                let mut result = ExecResult::success("");
1015
1016                'outer: loop {
1017                    // Evaluate condition - use async to support command substitution
1018                    // Cancellation checkpoint per iteration
1019                    if self.is_cancelled() {
1020                        result.code = 130;
1021                        return Ok(ControlFlow::ok(result));
1022                    }
1023
1024                    let cond_value = self.eval_expr_async(&while_loop.condition).await?;
1025
1026                    if !is_truthy(&cond_value) {
1027                        break;
1028                    }
1029
1030                    // Execute body
1031                    for stmt in &while_loop.body {
1032                        let mut flow = self.execute_stmt_flow(stmt).await?;
1033                        self.drain_stderr_into(&mut result).await;
1034                        match &mut flow {
1035                            ControlFlow::Normal(r) => {
1036                                accumulate_result(&mut result, r);
1037                                if !r.ok() {
1038                                    let scope = self.scope.read().await;
1039                                    if scope.error_exit_enabled() {
1040                                        return Ok(ControlFlow::exit_code(r.code));
1041                                    }
1042                                }
1043                            }
1044                            ControlFlow::Break { .. } => {
1045                                if flow.decrement_level() {
1046                                    break 'outer;
1047                                }
1048                                return Ok(flow);
1049                            }
1050                            ControlFlow::Continue { .. } => {
1051                                if flow.decrement_level() {
1052                                    continue 'outer;
1053                                }
1054                                return Ok(flow);
1055                            }
1056                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
1057                                return Ok(flow);
1058                            }
1059                        }
1060                    }
1061                }
1062
1063                Ok(ControlFlow::ok(result))
1064            }
1065            Stmt::Case(case_stmt) => {
1066                // Evaluate the expression to match against
1067                let match_value = {
1068                    let value = self.eval_expr_async(&case_stmt.expr).await?;
1069                    value_to_string(&value)
1070                };
1071
1072                // Try each branch until we find a match
1073                for branch in &case_stmt.branches {
1074                    let matched = branch.patterns.iter().any(|pattern| {
1075                        glob_match(pattern, &match_value)
1076                    });
1077
1078                    if matched {
1079                        // Execute the branch body
1080                        let mut result = ExecResult::success("");
1081                        for stmt in &branch.body {
1082                            let flow = self.execute_stmt_flow(stmt).await?;
1083                            match flow {
1084                                ControlFlow::Normal(r) => {
1085                                    accumulate_result(&mut result, &r);
1086                                    self.drain_stderr_into(&mut result).await;
1087                                }
1088                                other => {
1089                                    self.drain_stderr_into(&mut result).await;
1090                                    return Ok(other);
1091                                }
1092                            }
1093                        }
1094                        return Ok(ControlFlow::ok(result));
1095                    }
1096                }
1097
1098                // No match - return success with empty output (like sh)
1099                Ok(ControlFlow::ok(ExecResult::success("")))
1100            }
1101            Stmt::Break(levels) => {
1102                Ok(ControlFlow::break_n(levels.unwrap_or(1)))
1103            }
1104            Stmt::Continue(levels) => {
1105                Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
1106            }
1107            Stmt::Return(expr) => {
1108                // return [N] - N becomes the exit code, NOT stdout
1109                // Shell semantics: return sets exit code, doesn't produce output
1110                let result = if let Some(e) = expr {
1111                    let val = self.eval_expr_async(e).await?;
1112                    // Convert value to exit code
1113                    let code = match val {
1114                        Value::Int(n) => n,
1115                        Value::Bool(b) => if b { 0 } else { 1 },
1116                        _ => 0,
1117                    };
1118                    ExecResult {
1119                        code,
1120                        out: String::new(),
1121                        err: String::new(),
1122                        data: None,
1123                        output: None,
1124                        did_spill: false,
1125                        original_code: None,
1126                    }
1127                } else {
1128                    ExecResult::success("")
1129                };
1130                Ok(ControlFlow::return_value(result))
1131            }
1132            Stmt::Exit(expr) => {
1133                let code = if let Some(e) = expr {
1134                    let val = self.eval_expr_async(e).await?;
1135                    match val {
1136                        Value::Int(n) => n,
1137                        _ => 0,
1138                    }
1139                } else {
1140                    0
1141                };
1142                Ok(ControlFlow::exit_code(code))
1143            }
1144            Stmt::ToolDef(tool_def) => {
1145                let mut user_tools = self.user_tools.write().await;
1146                user_tools.insert(tool_def.name.clone(), tool_def.clone());
1147                Ok(ControlFlow::ok(ExecResult::success("")))
1148            }
1149            Stmt::AndChain { left, right } => {
1150                // cmd1 && cmd2 - run cmd2 only if cmd1 succeeds (exit code 0)
1151                // Suppress errexit for the left side — && handles failure itself.
1152                {
1153                    let mut scope = self.scope.write().await;
1154                    scope.suppress_errexit();
1155                }
1156                let left_flow = self.execute_stmt_flow(left).await?;
1157                {
1158                    let mut scope = self.scope.write().await;
1159                    scope.unsuppress_errexit();
1160                }
1161                match left_flow {
1162                    ControlFlow::Normal(mut left_result) => {
1163                        self.drain_stderr_into(&mut left_result).await;
1164                        self.update_last_result(&left_result).await;
1165                        if left_result.ok() {
1166                            let right_flow = self.execute_stmt_flow(right).await?;
1167                            match right_flow {
1168                                ControlFlow::Normal(mut right_result) => {
1169                                    self.drain_stderr_into(&mut right_result).await;
1170                                    self.update_last_result(&right_result).await;
1171                                    let mut combined = left_result;
1172                                    accumulate_result(&mut combined, &right_result);
1173                                    Ok(ControlFlow::ok(combined))
1174                                }
1175                                other => Ok(other),
1176                            }
1177                        } else {
1178                            Ok(ControlFlow::ok(left_result))
1179                        }
1180                    }
1181                    _ => Ok(left_flow),
1182                }
1183            }
1184            Stmt::OrChain { left, right } => {
1185                // cmd1 || cmd2 - run cmd2 only if cmd1 fails (non-zero exit code)
1186                // Suppress errexit for the left side — || handles failure itself.
1187                {
1188                    let mut scope = self.scope.write().await;
1189                    scope.suppress_errexit();
1190                }
1191                let left_flow = self.execute_stmt_flow(left).await?;
1192                {
1193                    let mut scope = self.scope.write().await;
1194                    scope.unsuppress_errexit();
1195                }
1196                match left_flow {
1197                    ControlFlow::Normal(mut left_result) => {
1198                        self.drain_stderr_into(&mut left_result).await;
1199                        self.update_last_result(&left_result).await;
1200                        if !left_result.ok() {
1201                            let right_flow = self.execute_stmt_flow(right).await?;
1202                            match right_flow {
1203                                ControlFlow::Normal(mut right_result) => {
1204                                    self.drain_stderr_into(&mut right_result).await;
1205                                    self.update_last_result(&right_result).await;
1206                                    let mut combined = left_result;
1207                                    accumulate_result(&mut combined, &right_result);
1208                                    Ok(ControlFlow::ok(combined))
1209                                }
1210                                other => Ok(other),
1211                            }
1212                        } else {
1213                            Ok(ControlFlow::ok(left_result))
1214                        }
1215                    }
1216                    _ => Ok(left_flow), // Propagate non-normal flow
1217                }
1218            }
1219            Stmt::Test(test_expr) => {
1220                let is_true = self.eval_test_async(test_expr).await?;
1221                if is_true {
1222                    Ok(ControlFlow::ok(ExecResult::success("")))
1223                } else {
1224                    Ok(ControlFlow::ok(ExecResult::failure(1, "")))
1225                }
1226            }
1227            Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
1228        }
1229        }.instrument(span))
1230    }
1231
1232    /// Execute a pipeline.
1233    #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
1234    async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1235        if pipeline.commands.is_empty() {
1236            return Ok(ExecResult::success(""));
1237        }
1238
1239        // Handle background execution (`&` operator)
1240        if pipeline.background {
1241            return self.execute_background(pipeline).await;
1242        }
1243
1244        // All commands go through the runner with the Kernel as dispatcher.
1245        // This is the single execution path — no fast path for single commands.
1246        //
1247        // IMPORTANT: We snapshot exec_ctx into a local context and release the
1248        // lock before running. This prevents deadlocks when dispatch_command
1249        // is called from within the pipeline and recursively triggers another
1250        // pipeline (e.g., via user-defined tools).
1251        let mut ctx = {
1252            let ec = self.exec_ctx.read().await;
1253            let scope = self.scope.read().await;
1254            ExecContext {
1255                backend: ec.backend.clone(),
1256                scope: scope.clone(),
1257                cwd: ec.cwd.clone(),
1258                prev_cwd: ec.prev_cwd.clone(),
1259                stdin: None,
1260                stdin_data: None,
1261                pipe_stdin: None,
1262                pipe_stdout: None,
1263                stderr: ec.stderr.clone(),
1264                tool_schemas: ec.tool_schemas.clone(),
1265                tools: ec.tools.clone(),
1266                job_manager: ec.job_manager.clone(),
1267                pipeline_position: PipelinePosition::Only,
1268                interactive: self.interactive,
1269                aliases: ec.aliases.clone(),
1270                ignore_config: ec.ignore_config.clone(),
1271                output_limit: ec.output_limit.clone(),
1272                allow_external_commands: self.allow_external_commands,
1273                nonce_store: ec.nonce_store.clone(),
1274                #[cfg(unix)]
1275                terminal_state: ec.terminal_state.clone(),
1276            }
1277        }; // locks released
1278
1279        let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
1280
1281        // Post-hoc spill check (catches builtins and fast external commands)
1282        if ctx.output_limit.is_enabled() {
1283            let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
1284        }
1285
1286        // Signal spill with exit 3; agent reads the spill file directly
1287        // (use `set +o output-limit` before cat/head/tail to bypass the limit)
1288        if result.did_spill {
1289            result.original_code = Some(result.code);
1290            result.code = 3;
1291        }
1292
1293        // Sync changes back from context
1294        {
1295            let mut ec = self.exec_ctx.write().await;
1296            ec.cwd = ctx.cwd.clone();
1297            ec.prev_cwd = ctx.prev_cwd.clone();
1298            ec.aliases = ctx.aliases.clone();
1299            ec.ignore_config = ctx.ignore_config.clone();
1300            ec.output_limit = ctx.output_limit.clone();
1301        }
1302        {
1303            let mut scope = self.scope.write().await;
1304            *scope = ctx.scope.clone();
1305        }
1306
1307        Ok(result)
1308    }
1309
1310    /// Execute a pipeline in the background.
1311    ///
1312    /// The command is spawned as a tokio task, registered with the JobManager,
1313    /// and its output is captured via BoundedStreams. The job is observable via
1314    /// `/v/jobs/{id}/stdout`, `/v/jobs/{id}/stderr`, and `/v/jobs/{id}/status`.
1315    ///
1316    /// Returns immediately with a job ID like "[1]".
1317    #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
1318    async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1319        use tokio::sync::oneshot;
1320
1321        // Format the command for display in /v/jobs/{id}/command
1322        let command_str = self.format_pipeline(pipeline);
1323
1324        // Create bounded streams for output capture
1325        let stdout = Arc::new(BoundedStream::default_size());
1326        let stderr = Arc::new(BoundedStream::default_size());
1327
1328        // Create channel for result notification
1329        let (tx, rx) = oneshot::channel();
1330
1331        // Register with JobManager to get job ID and create VFS entries
1332        let job_id = self.jobs.register_with_streams(
1333            command_str.clone(),
1334            rx,
1335            stdout.clone(),
1336            stderr.clone(),
1337        ).await;
1338
1339        // Clone state needed for the spawned task
1340        let runner = self.runner.clone();
1341        let commands = pipeline.commands.clone();
1342        let backend = {
1343            let ctx = self.exec_ctx.read().await;
1344            ctx.backend.clone()
1345        };
1346        let scope = {
1347            let scope = self.scope.read().await;
1348            scope.clone()
1349        };
1350        let cwd = {
1351            let ctx = self.exec_ctx.read().await;
1352            ctx.cwd.clone()
1353        };
1354        let tools = self.tools.clone();
1355        let tool_schemas = self.tools.schemas();
1356        let allow_ext = self.allow_external_commands;
1357
1358        // Spawn the background task
1359        tokio::spawn(async move {
1360            // Create execution context for the background job
1361            // It inherits env vars and cwd from the parent context
1362            let mut bg_ctx = ExecContext::with_backend(backend);
1363            bg_ctx.scope = scope;
1364            bg_ctx.cwd = cwd;
1365            bg_ctx.set_tools(tools.clone());
1366            bg_ctx.set_tool_schemas(tool_schemas);
1367            bg_ctx.allow_external_commands = allow_ext;
1368
1369            // Use BackendDispatcher for background jobs (builtins only).
1370            // Full Kernel dispatch requires Arc<Kernel> — planned for a future phase.
1371            let dispatcher = crate::dispatch::BackendDispatcher::new(tools);
1372
1373            // Execute the pipeline
1374            let result = runner.run(&commands, &mut bg_ctx, &dispatcher).await;
1375
1376            // Write output to streams
1377            let text = result.text_out();
1378            if !text.is_empty() {
1379                stdout.write(text.as_bytes()).await;
1380            }
1381            if !result.err.is_empty() {
1382                stderr.write(result.err.as_bytes()).await;
1383            }
1384
1385            // Close streams
1386            stdout.close().await;
1387            stderr.close().await;
1388
1389            // Send result to JobManager (ignore error if receiver dropped)
1390            let _ = tx.send(result);
1391        });
1392
1393        Ok(ExecResult::success(format!("[{}]", job_id)))
1394    }
1395
1396    /// Format a pipeline as a command string for display.
1397    fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
1398        pipeline.commands
1399            .iter()
1400            .map(|cmd| {
1401                let mut parts = vec![cmd.name.clone()];
1402                for arg in &cmd.args {
1403                    match arg {
1404                        Arg::Positional(expr) => {
1405                            parts.push(self.format_expr(expr));
1406                        }
1407                        Arg::Named { key, value } => {
1408                            parts.push(format!("{}={}", key, self.format_expr(value)));
1409                        }
1410                        Arg::ShortFlag(name) => {
1411                            parts.push(format!("-{}", name));
1412                        }
1413                        Arg::LongFlag(name) => {
1414                            parts.push(format!("--{}", name));
1415                        }
1416                        Arg::DoubleDash => {
1417                            parts.push("--".to_string());
1418                        }
1419                    }
1420                }
1421                parts.join(" ")
1422            })
1423            .collect::<Vec<_>>()
1424            .join(" | ")
1425    }
1426
1427    /// Format an expression as a string for display.
1428    fn format_expr(&self, expr: &Expr) -> String {
1429        match expr {
1430            Expr::Literal(Value::String(s)) => {
1431                if s.contains(' ') || s.contains('"') {
1432                    format!("'{}'", s.replace('\'', "\\'"))
1433                } else {
1434                    s.clone()
1435                }
1436            }
1437            Expr::Literal(Value::Int(i)) => i.to_string(),
1438            Expr::Literal(Value::Float(f)) => f.to_string(),
1439            Expr::Literal(Value::Bool(b)) => b.to_string(),
1440            Expr::Literal(Value::Null) => "null".to_string(),
1441            Expr::VarRef(path) => {
1442                let name = path.segments.iter()
1443                    .map(|seg| match seg {
1444                        crate::ast::VarSegment::Field(f) => f.clone(),
1445                    })
1446                    .collect::<Vec<_>>()
1447                    .join(".");
1448                format!("${{{}}}", name)
1449            }
1450            Expr::Interpolated(_) => "\"...\"".to_string(),
1451            _ => "...".to_string(),
1452        }
1453    }
1454
1455    /// Execute a single command.
1456    async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
1457        self.execute_command_depth(name, args, 0).await
1458    }
1459
1460    #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
1461    async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
1462        // Special built-ins
1463        match name {
1464            "true" => return Ok(ExecResult::success("")),
1465            "false" => return Ok(ExecResult::failure(1, "")),
1466            "source" | "." => return self.execute_source(args).await,
1467            _ => {}
1468        }
1469
1470        // Alias expansion (with recursion limit)
1471        if alias_depth < 10 {
1472            let alias_value = {
1473                let ctx = self.exec_ctx.read().await;
1474                ctx.aliases.get(name).cloned()
1475            };
1476            if let Some(alias_val) = alias_value {
1477                // Split alias value into command + args
1478                let parts: Vec<&str> = alias_val.split_whitespace().collect();
1479                if let Some((alias_cmd, alias_args)) = parts.split_first() {
1480                    let mut new_args: Vec<Arg> = alias_args
1481                        .iter()
1482                        .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
1483                        .collect();
1484                    new_args.extend_from_slice(args);
1485                    return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
1486                }
1487            }
1488        }
1489
1490        // Handle /v/bin/ prefix — dispatch to builtins via virtual path
1491        if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
1492            return match self.tools.get(builtin_name) {
1493                Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
1494                None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
1495            };
1496        }
1497
1498        // Check user-defined tools first
1499        {
1500            let user_tools = self.user_tools.read().await;
1501            if let Some(tool_def) = user_tools.get(name) {
1502                let tool_def = tool_def.clone();
1503                drop(user_tools);
1504                return self.execute_user_tool(tool_def, args).await;
1505            }
1506        }
1507
1508        // Look up builtin tool
1509        let tool = match self.tools.get(name) {
1510            Some(t) => t,
1511            None => {
1512                // Try executing as .kai script from PATH
1513                if let Some(result) = self.try_execute_script(name, args).await? {
1514                    return Ok(result);
1515                }
1516                // Try executing as external command from PATH
1517                if let Some(result) = self.try_execute_external(name, args).await? {
1518                    return Ok(result);
1519                }
1520
1521                // Try backend-registered tools (embedder engines, MCP tools, etc.)
1522                // Look up tool schema for positional→named mapping.
1523                // Clone backend and drop read lock before awaiting (may involve network I/O).
1524                // Backend tools expect named JSON params, so enable positional mapping.
1525                let backend = self.exec_ctx.read().await.backend.clone();
1526                let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
1527                    let mut s = t.schema;
1528                    s.map_positionals = true;
1529                    s
1530                });
1531                let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
1532                let mut ctx = self.exec_ctx.write().await;
1533                {
1534                    let scope = self.scope.read().await;
1535                    ctx.scope = scope.clone();
1536                }
1537                let backend = ctx.backend.clone();
1538                match backend.call_tool(name, tool_args, &mut ctx).await {
1539                    Ok(tool_result) => {
1540                        let mut scope = self.scope.write().await;
1541                        *scope = ctx.scope.clone();
1542                        let mut exec = ExecResult::from_output(
1543                            tool_result.code as i64, tool_result.stdout, tool_result.stderr,
1544                        );
1545                        exec.output = tool_result.output;
1546                        return Ok(exec);
1547                    }
1548                    Err(BackendError::ToolNotFound(_)) => {
1549                        // Fall through to "command not found"
1550                    }
1551                    Err(e) => {
1552                        // Backend dispatch is last-resort lookup — if it fails
1553                        // for any reason, the command simply doesn't exist.
1554                        tracing::debug!("backend error for {name}: {e}");
1555                    }
1556                }
1557
1558                return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
1559            }
1560        };
1561
1562        // Build arguments (async to support command substitution, schema-aware for flag values)
1563        let schema = tool.schema();
1564        let mut tool_args = self.build_args_async(args, Some(&schema)).await?;
1565        let output_format = extract_output_format(&mut tool_args, Some(&schema));
1566
1567        // --help / -h: show help unless the tool's schema claims that flag
1568        let schema_claims = |flag: &str| -> bool {
1569            let bare = flag.trim_start_matches('-');
1570            schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
1571        };
1572        let wants_help =
1573            (tool_args.flags.contains("help") && !schema_claims("help"))
1574            || (tool_args.flags.contains("h") && !schema_claims("-h"));
1575        if wants_help {
1576            let help_topic = crate::help::HelpTopic::Tool(name.to_string());
1577            let ctx = self.exec_ctx.read().await;
1578            let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
1579            return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
1580        }
1581
1582        // Execute
1583        let mut ctx = self.exec_ctx.write().await;
1584        {
1585            let scope = self.scope.read().await;
1586            ctx.scope = scope.clone();
1587        }
1588
1589        let result = tool.execute(tool_args, &mut ctx).await;
1590
1591        // Sync scope changes back (e.g., from cd)
1592        {
1593            let mut scope = self.scope.write().await;
1594            *scope = ctx.scope.clone();
1595        }
1596
1597        let result = match output_format {
1598            Some(format) => apply_output_format(result, format),
1599            None => result,
1600        };
1601
1602        Ok(result)
1603    }
1604
1605    /// Build tool arguments from AST args.
1606    ///
1607    /// Uses async evaluation to support command substitution in arguments.
1608    async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
1609        let mut tool_args = ToolArgs::new();
1610        let param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
1611
1612        // Track which positional indices have been consumed as flag values
1613        let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1614        let mut past_double_dash = false;
1615
1616        // Find positional arg indices for flag value consumption
1617        let positional_indices: Vec<usize> = args.iter().enumerate()
1618            .filter_map(|(i, a)| matches!(a, Arg::Positional(_)).then_some(i))
1619            .collect();
1620
1621        let mut i = 0;
1622        while i < args.len() {
1623            match &args[i] {
1624                Arg::DoubleDash => {
1625                    past_double_dash = true;
1626                }
1627                Arg::Positional(expr) => {
1628                    if !consumed.contains(&i) {
1629                        // Glob expansion: bare glob patterns expand to matching files
1630                        if let Expr::GlobPattern(pattern) = expr {
1631                            let glob_enabled = {
1632                                let scope = self.scope.read().await;
1633                                scope.glob_enabled()
1634                            };
1635                            if glob_enabled {
1636                                let (paths, cwd) = {
1637                                    let ctx = self.exec_ctx.read().await;
1638                                    let paths = ctx.expand_glob(pattern).await
1639                                        .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
1640                                    let cwd = ctx.resolve_path(".");
1641                                    (paths, cwd)
1642                                };
1643                                if paths.is_empty() {
1644                                    return Err(anyhow::anyhow!("no matches: {}", pattern));
1645                                }
1646                                for path in paths {
1647                                    let display = if !pattern.starts_with('/') {
1648                                        path.strip_prefix(&cwd)
1649                                            .unwrap_or(&path)
1650                                            .to_string_lossy().into_owned()
1651                                    } else {
1652                                        path.to_string_lossy().into_owned()
1653                                    };
1654                                    tool_args.positional.push(Value::String(display));
1655                                }
1656                                i += 1;
1657                                continue;
1658                            }
1659                        }
1660                        let value = self.eval_expr_async(expr).await?;
1661                        let value = apply_tilde_expansion(value);
1662                        tool_args.positional.push(value);
1663                    }
1664                }
1665                Arg::Named { key, value } => {
1666                    let val = self.eval_expr_async(value).await?;
1667                    let val = apply_tilde_expansion(val);
1668                    tool_args.named.insert(key.clone(), val);
1669                }
1670                Arg::ShortFlag(name) => {
1671                    if past_double_dash {
1672                        tool_args.positional.push(Value::String(format!("-{name}")));
1673                    } else if name.len() == 1 {
1674                        let flag_name = name.as_str();
1675                        let lookup = param_lookup.get(flag_name);
1676                        let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1677
1678                        if is_bool {
1679                            tool_args.flags.insert(flag_name.to_string());
1680                        } else {
1681                            // Non-bool: consume next positional as value
1682                            let canonical = lookup.map(|(name, _)| *name).unwrap_or(flag_name);
1683                            let next_pos = positional_indices.iter()
1684                                .find(|idx| **idx > i && !consumed.contains(idx));
1685
1686                            if let Some(&pos_idx) = next_pos {
1687                                if let Arg::Positional(expr) = &args[pos_idx] {
1688                                    let value = self.eval_expr_async(expr).await?;
1689                                    let value = apply_tilde_expansion(value);
1690                                    tool_args.named.insert(canonical.to_string(), value);
1691                                    consumed.insert(pos_idx);
1692                                }
1693                            } else {
1694                                tool_args.flags.insert(flag_name.to_string());
1695                            }
1696                        }
1697                    } else if let Some(&(canonical, typ)) = param_lookup.get(name.as_str()) {
1698                        // Multi-char short flag matches a schema param (POSIX style: -name value)
1699                        if is_bool_type(typ) {
1700                            tool_args.flags.insert(canonical.to_string());
1701                        } else {
1702                            let next_pos = positional_indices.iter()
1703                                .find(|idx| **idx > i && !consumed.contains(idx));
1704                            if let Some(&pos_idx) = next_pos {
1705                                if let Arg::Positional(expr) = &args[pos_idx] {
1706                                    let value = self.eval_expr_async(expr).await?;
1707                                    let value = apply_tilde_expansion(value);
1708                                    tool_args.named.insert(canonical.to_string(), value);
1709                                    consumed.insert(pos_idx);
1710                                }
1711                            } else {
1712                                tool_args.flags.insert(name.clone());
1713                            }
1714                        }
1715                    } else {
1716                        // Multi-char combined flags like -la: always boolean
1717                        for c in name.chars() {
1718                            tool_args.flags.insert(c.to_string());
1719                        }
1720                    }
1721                }
1722                Arg::LongFlag(name) => {
1723                    if past_double_dash {
1724                        tool_args.positional.push(Value::String(format!("--{name}")));
1725                    } else {
1726                        let lookup = param_lookup.get(name.as_str());
1727                        let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1728
1729                        if is_bool {
1730                            tool_args.flags.insert(name.clone());
1731                        } else {
1732                            let canonical = lookup.map(|(name, _)| *name).unwrap_or(name.as_str());
1733                            let next_pos = positional_indices.iter()
1734                                .find(|idx| **idx > i && !consumed.contains(idx));
1735
1736                            if let Some(&pos_idx) = next_pos {
1737                                if let Arg::Positional(expr) = &args[pos_idx] {
1738                                    let value = self.eval_expr_async(expr).await?;
1739                                    let value = apply_tilde_expansion(value);
1740                                    tool_args.named.insert(canonical.to_string(), value);
1741                                    consumed.insert(pos_idx);
1742                                }
1743                            } else {
1744                                tool_args.flags.insert(name.clone());
1745                            }
1746                        }
1747                    }
1748                }
1749            }
1750            i += 1;
1751        }
1752
1753        // Map remaining positionals to unfilled non-bool schema params (in order).
1754        // This enables `drift_push "abc" "hello"` → named["target_ctx"] = "abc", named["content"] = "hello"
1755        // Positionals that appeared after `--` are never mapped (they're raw data).
1756        // Only for MCP/external tools (map_positionals=true). Builtins handle their own positionals.
1757        if let Some(schema) = schema.filter(|s| s.map_positionals) {
1758            let pre_dash_count = if past_double_dash {
1759                let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
1760                positional_indices.iter()
1761                    .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
1762                    .count()
1763            } else {
1764                tool_args.positional.len()
1765            };
1766
1767            let mut remaining = Vec::new();
1768            let mut positional_iter = tool_args.positional.drain(..).enumerate();
1769
1770            for param in &schema.params {
1771                if tool_args.named.contains_key(&param.name) || tool_args.flags.contains(&param.name) {
1772                    continue;
1773                }
1774                if is_bool_type(&param.param_type) {
1775                    continue;
1776                }
1777                loop {
1778                    match positional_iter.next() {
1779                        Some((idx, val)) if idx < pre_dash_count => {
1780                            tool_args.named.insert(param.name.clone(), val);
1781                            break;
1782                        }
1783                        Some((_, val)) => {
1784                            remaining.push(val);
1785                        }
1786                        None => break,
1787                    }
1788                }
1789            }
1790
1791            remaining.extend(positional_iter.map(|(_, v)| v));
1792            tool_args.positional = remaining;
1793        }
1794
1795        Ok(tool_args)
1796    }
1797
1798    /// Build arguments as flat string list for external commands.
1799    ///
1800    /// Unlike `build_args_async` which separates flags into a HashSet (for schema-aware builtins),
1801    /// this preserves the original flag format as strings for external commands:
1802    /// - `-l` stays as `-l`
1803    /// - `--verbose` stays as `--verbose`
1804    /// - `key=value` stays as `key=value`
1805    ///
1806    /// This is what external commands expect in their argv.
1807    async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
1808        let mut argv = Vec::new();
1809        for arg in args {
1810            match arg {
1811                Arg::Positional(expr) => {
1812                    // Glob expansion for external commands
1813                    if let Expr::GlobPattern(pattern) = expr {
1814                        let glob_enabled = {
1815                            let scope = self.scope.read().await;
1816                            scope.glob_enabled()
1817                        };
1818                        if glob_enabled {
1819                            let (paths, cwd) = {
1820                                let ctx = self.exec_ctx.read().await;
1821                                let paths = ctx.expand_glob(pattern).await
1822                                    .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
1823                                let cwd = ctx.resolve_path(".");
1824                                (paths, cwd)
1825                            };
1826                            if paths.is_empty() {
1827                                return Err(anyhow::anyhow!("no matches: {}", pattern));
1828                            }
1829                            for path in paths {
1830                                let display = if !pattern.starts_with('/') {
1831                                    path.strip_prefix(&cwd)
1832                                        .unwrap_or(&path)
1833                                        .to_string_lossy().into_owned()
1834                                } else {
1835                                    path.to_string_lossy().into_owned()
1836                                };
1837                                argv.push(display);
1838                            }
1839                            continue;
1840                        }
1841                    }
1842                    let value = self.eval_expr_async(expr).await?;
1843                    let value = apply_tilde_expansion(value);
1844                    argv.push(value_to_string(&value));
1845                }
1846                Arg::Named { key, value } => {
1847                    let val = self.eval_expr_async(value).await?;
1848                    let val = apply_tilde_expansion(val);
1849                    argv.push(format!("{}={}", key, value_to_string(&val)));
1850                }
1851                Arg::ShortFlag(name) => {
1852                    // Preserve original format: -l, -la (combined flags)
1853                    argv.push(format!("-{}", name));
1854                }
1855                Arg::LongFlag(name) => {
1856                    // Preserve original format: --verbose
1857                    argv.push(format!("--{}", name));
1858                }
1859                Arg::DoubleDash => {
1860                    // Preserve the -- marker
1861                    argv.push("--".to_string());
1862                }
1863            }
1864        }
1865        Ok(argv)
1866    }
1867
1868    /// Async expression evaluator that supports command substitution.
1869    ///
1870    /// This is used for contexts where expressions may contain `$(...)` command
1871    /// substitution. Unlike the sync `eval_expr`, this can execute pipelines.
1872    fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
1873        Box::pin(async move {
1874        match expr {
1875            Expr::Literal(value) => Ok(value.clone()),
1876            Expr::VarRef(path) => {
1877                let scope = self.scope.read().await;
1878                scope.resolve_path(path)
1879                    .ok_or_else(|| anyhow::anyhow!("undefined variable"))
1880            }
1881            Expr::Interpolated(parts) => {
1882                let mut result = String::new();
1883                for part in parts {
1884                    result.push_str(&self.eval_string_part_async(part).await?);
1885                }
1886                Ok(Value::String(result))
1887            }
1888            Expr::BinaryOp { left, op, right } => {
1889                match op {
1890                    BinaryOp::And => {
1891                        let left_val = self.eval_expr_async(left).await?;
1892                        if !is_truthy(&left_val) {
1893                            return Ok(left_val);
1894                        }
1895                        self.eval_expr_async(right).await
1896                    }
1897                    BinaryOp::Or => {
1898                        let left_val = self.eval_expr_async(left).await?;
1899                        if is_truthy(&left_val) {
1900                            return Ok(left_val);
1901                        }
1902                        self.eval_expr_async(right).await
1903                    }
1904                    _ => {
1905                        // Evaluate operands async (handles $(cmd)), then compare sync
1906                        let left_val = self.eval_expr_async(left).await?;
1907                        let right_val = self.eval_expr_async(right).await?;
1908                        let resolved = Expr::BinaryOp {
1909                            left: Box::new(Expr::Literal(left_val)),
1910                            op: *op,
1911                            right: Box::new(Expr::Literal(right_val)),
1912                        };
1913                        let mut scope = self.scope.write().await;
1914                        eval_expr(&resolved, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1915                    }
1916                }
1917            }
1918            Expr::CommandSubst(pipeline) => {
1919                // Snapshot scope+cwd before running — only output escapes,
1920                // not side effects like `cd` or variable assignments.
1921                let saved_scope = { self.scope.read().await.clone() };
1922                let saved_cwd = {
1923                    let ec = self.exec_ctx.read().await;
1924                    (ec.cwd.clone(), ec.prev_cwd.clone())
1925                };
1926
1927                // Capture result without `?` — restore state unconditionally
1928                let run_result = self.execute_pipeline(pipeline).await;
1929
1930                // Restore scope and cwd regardless of success/failure
1931                {
1932                    let mut scope = self.scope.write().await;
1933                    *scope = saved_scope;
1934                    if let Ok(ref r) = run_result {
1935                        scope.set_last_result(r.clone());
1936                    }
1937                }
1938                {
1939                    let mut ec = self.exec_ctx.write().await;
1940                    ec.cwd = saved_cwd.0;
1941                    ec.prev_cwd = saved_cwd.1;
1942                }
1943
1944                // Now propagate the error
1945                let result = run_result?;
1946
1947                // Prefer structured data (enables `for i in $(cmd)` iteration)
1948                if let Some(data) = &result.data {
1949                    Ok(data.clone())
1950                } else if let Some(ref output) = result.output {
1951                    // Flat non-text node lists (glob, ls, tree) → iterable array
1952                    if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
1953                        let items: Vec<serde_json::Value> = output.root.iter()
1954                            .map(|n| serde_json::Value::String(n.display_name().to_string()))
1955                            .collect();
1956                        Ok(Value::Json(serde_json::Value::Array(items)))
1957                    } else {
1958                        Ok(Value::String(result.text_out().trim_end().to_string()))
1959                    }
1960                } else {
1961                    // Otherwise return stdout as single string (NO implicit splitting)
1962                    Ok(Value::String(result.text_out().trim_end().to_string()))
1963                }
1964            }
1965            Expr::Test(test_expr) => {
1966                Ok(Value::Bool(self.eval_test_async(test_expr).await?))
1967            }
1968            Expr::Positional(n) => {
1969                let scope = self.scope.read().await;
1970                match scope.get_positional(*n) {
1971                    Some(s) => Ok(Value::String(s.to_string())),
1972                    None => Ok(Value::String(String::new())),
1973                }
1974            }
1975            Expr::AllArgs => {
1976                let scope = self.scope.read().await;
1977                Ok(Value::String(scope.all_args().join(" ")))
1978            }
1979            Expr::ArgCount => {
1980                let scope = self.scope.read().await;
1981                Ok(Value::Int(scope.arg_count() as i64))
1982            }
1983            Expr::VarLength(name) => {
1984                let scope = self.scope.read().await;
1985                match scope.get(name) {
1986                    Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
1987                    None => Ok(Value::Int(0)),
1988                }
1989            }
1990            Expr::VarWithDefault { name, default } => {
1991                let scope = self.scope.read().await;
1992                let use_default = match scope.get(name) {
1993                    Some(value) => value_to_string(value).is_empty(),
1994                    None => true,
1995                };
1996                drop(scope); // Release the lock before recursive evaluation
1997                if use_default {
1998                    // Evaluate the default parts (supports nested expansions)
1999                    self.eval_string_parts_async(default).await.map(Value::String)
2000                } else {
2001                    let scope = self.scope.read().await;
2002                    scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
2003                }
2004            }
2005            Expr::Arithmetic(expr_str) => {
2006                let scope = self.scope.read().await;
2007                crate::arithmetic::eval_arithmetic(expr_str, &scope)
2008                    .map(Value::Int)
2009                    .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
2010            }
2011            Expr::Command(cmd) => {
2012                // Execute command and return boolean based on exit code
2013                let result = self.execute_command(&cmd.name, &cmd.args).await?;
2014                Ok(Value::Bool(result.code == 0))
2015            }
2016            Expr::LastExitCode => {
2017                let scope = self.scope.read().await;
2018                Ok(Value::Int(scope.last_result().code))
2019            }
2020            Expr::CurrentPid => {
2021                let scope = self.scope.read().await;
2022                Ok(Value::Int(scope.pid() as i64))
2023            }
2024            Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
2025        }
2026        })
2027    }
2028
2029    /// Async helper to evaluate multiple StringParts into a single string.
2030    fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
2031        Box::pin(async move {
2032            let mut result = String::new();
2033            for part in parts {
2034                result.push_str(&self.eval_string_part_async(part).await?);
2035            }
2036            Ok(result)
2037        })
2038    }
2039
2040    /// Async helper to evaluate a StringPart.
2041    /// Evaluate a `[[ ]]` test expression asynchronously, routing file tests
2042    /// through the VFS backend instead of using raw `std::path`.
2043    fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
2044        Box::pin(async move {
2045            match test_expr {
2046                TestExpr::FileTest { op, path } => {
2047                    let path_value = self.eval_expr_async(path).await?;
2048                    let path_str = value_to_string(&path_value);
2049                    let backend = self.exec_ctx.read().await.backend.clone();
2050                    let entry = backend.stat(std::path::Path::new(&path_str)).await.ok();
2051                    Ok(match op {
2052                        FileTestOp::Exists => entry.is_some(),
2053                        FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
2054                        FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
2055                        FileTestOp::Readable => entry.is_some(),
2056                        FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
2057                            e.permissions.is_none_or(|p| p & 0o222 != 0)
2058                        }),
2059                        FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
2060                            e.permissions.is_some_and(|p| p & 0o111 != 0)
2061                        }),
2062                    })
2063                }
2064                TestExpr::StringTest { op, value } => {
2065                    let val = self.eval_expr_async(value).await?;
2066                    let s = value_to_string(&val);
2067                    Ok(match op {
2068                        crate::ast::StringTestOp::IsEmpty => s.is_empty(),
2069                        crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
2070                    })
2071                }
2072                TestExpr::Comparison { left, op, right } => {
2073                    // Evaluate operands async (handles $(cmd)), then compare sync
2074                    let left_val = self.eval_expr_async(left).await?;
2075                    let right_val = self.eval_expr_async(right).await?;
2076                    let resolved = TestExpr::Comparison {
2077                        left: Box::new(Expr::Literal(left_val)),
2078                        op: *op,
2079                        right: Box::new(Expr::Literal(right_val)),
2080                    };
2081                    let expr = Expr::Test(Box::new(resolved));
2082                    let mut scope = self.scope.write().await;
2083                    let value = eval_expr(&expr, &mut scope)
2084                        .map_err(|e| anyhow::anyhow!("{}", e))?;
2085                    Ok(value_to_bool(&value))
2086                }
2087                TestExpr::And { left, right } => {
2088                    if !self.eval_test_async(left).await? {
2089                        Ok(false)
2090                    } else {
2091                        self.eval_test_async(right).await
2092                    }
2093                }
2094                TestExpr::Or { left, right } => {
2095                    if self.eval_test_async(left).await? {
2096                        Ok(true)
2097                    } else {
2098                        self.eval_test_async(right).await
2099                    }
2100                }
2101                TestExpr::Not { expr } => {
2102                    Ok(!self.eval_test_async(expr).await?)
2103                }
2104            }
2105        })
2106    }
2107
2108    fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
2109        Box::pin(async move {
2110            match part {
2111                StringPart::Literal(s) => Ok(s.clone()),
2112                StringPart::Var(path) => {
2113                    let scope = self.scope.read().await;
2114                    match scope.resolve_path(path) {
2115                        Some(value) => Ok(value_to_string(&value)),
2116                        None => Ok(String::new()), // Unset vars expand to empty
2117                    }
2118                }
2119                StringPart::VarWithDefault { name, default } => {
2120                    let scope = self.scope.read().await;
2121                    let use_default = match scope.get(name) {
2122                        Some(value) => value_to_string(value).is_empty(),
2123                        None => true,
2124                    };
2125                    drop(scope); // Release lock before recursive evaluation
2126                    if use_default {
2127                        // Evaluate the default parts (supports nested expansions)
2128                        self.eval_string_parts_async(default).await
2129                    } else {
2130                        let scope = self.scope.read().await;
2131                        Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
2132                    }
2133                }
2134            StringPart::VarLength(name) => {
2135                let scope = self.scope.read().await;
2136                match scope.get(name) {
2137                    Some(value) => Ok(value_to_string(value).len().to_string()),
2138                    None => Ok("0".to_string()),
2139                }
2140            }
2141            StringPart::Positional(n) => {
2142                let scope = self.scope.read().await;
2143                match scope.get_positional(*n) {
2144                    Some(s) => Ok(s.to_string()),
2145                    None => Ok(String::new()),
2146                }
2147            }
2148            StringPart::AllArgs => {
2149                let scope = self.scope.read().await;
2150                Ok(scope.all_args().join(" "))
2151            }
2152            StringPart::ArgCount => {
2153                let scope = self.scope.read().await;
2154                Ok(scope.arg_count().to_string())
2155            }
2156            StringPart::Arithmetic(expr) => {
2157                let scope = self.scope.read().await;
2158                match crate::arithmetic::eval_arithmetic(expr, &scope) {
2159                    Ok(value) => Ok(value.to_string()),
2160                    Err(_) => Ok(String::new()),
2161                }
2162            }
2163            StringPart::CommandSubst(pipeline) => {
2164                // Snapshot scope+cwd — command substitution in strings must
2165                // not leak side effects (e.g., `"dir: $(cd /; pwd)"` must not change cwd).
2166                let saved_scope = { self.scope.read().await.clone() };
2167                let saved_cwd = {
2168                    let ec = self.exec_ctx.read().await;
2169                    (ec.cwd.clone(), ec.prev_cwd.clone())
2170                };
2171
2172                // Capture result without `?` — restore state unconditionally
2173                let run_result = self.execute_pipeline(pipeline).await;
2174
2175                // Restore scope and cwd regardless of success/failure
2176                {
2177                    let mut scope = self.scope.write().await;
2178                    *scope = saved_scope;
2179                    if let Ok(ref r) = run_result {
2180                        scope.set_last_result(r.clone());
2181                    }
2182                }
2183                {
2184                    let mut ec = self.exec_ctx.write().await;
2185                    ec.cwd = saved_cwd.0;
2186                    ec.prev_cwd = saved_cwd.1;
2187                }
2188
2189                // Now propagate the error
2190                let result = run_result?;
2191
2192                Ok(result.text_out().trim_end_matches('\n').to_string())
2193            }
2194            StringPart::LastExitCode => {
2195                let scope = self.scope.read().await;
2196                Ok(scope.last_result().code.to_string())
2197            }
2198            StringPart::CurrentPid => {
2199                let scope = self.scope.read().await;
2200                Ok(scope.pid().to_string())
2201            }
2202        }
2203        })
2204    }
2205
2206    /// Update the last result in scope.
2207    async fn update_last_result(&self, result: &ExecResult) {
2208        let mut scope = self.scope.write().await;
2209        scope.set_last_result(result.clone());
2210    }
2211
2212    /// Drain accumulated pipeline stderr into a result.
2213    ///
2214    /// Called after each sub-statement inside control structures (`if`, `for`,
2215    /// `while`, `case`, `&&`, `||`) so that stderr appears incrementally rather
2216    /// than batching until the entire structure finishes.
2217    async fn drain_stderr_into(&self, result: &mut ExecResult) {
2218        let drained = {
2219            let mut receiver = self.stderr_receiver.lock().await;
2220            receiver.drain_lossy()
2221        };
2222        if !drained.is_empty() {
2223            if !result.err.is_empty() && !result.err.ends_with('\n') {
2224                result.err.push('\n');
2225            }
2226            result.err.push_str(&drained);
2227        }
2228    }
2229
2230    /// Execute a user-defined function with local variable scoping.
2231    ///
2232    /// Functions push a new scope frame for local variables. Variables declared
2233    /// with `local` are scoped to the function; other assignments modify outer
2234    /// scopes (or create in root if new).
2235    async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
2236        // 1. Build function args from AST args (async to support command substitution)
2237        let tool_args = self.build_args_async(args, None).await?;
2238
2239        // 2. Push a new scope frame for local variables
2240        {
2241            let mut scope = self.scope.write().await;
2242            scope.push_frame();
2243        }
2244
2245        // 3. Save current positional parameters and set new ones for this function
2246        let saved_positional = {
2247            let mut scope = self.scope.write().await;
2248            let saved = scope.save_positional();
2249
2250            // Set up new positional parameters ($0 = function name, $1, $2, ... = args)
2251            let positional_args: Vec<String> = tool_args.positional
2252                .iter()
2253                .map(value_to_string)
2254                .collect();
2255            scope.set_positional(&def.name, positional_args);
2256
2257            saved
2258        };
2259
2260        // 3. Execute body statements with control flow handling
2261        // Accumulate output across statements (like sh)
2262        let mut accumulated_out = String::new();
2263        let mut accumulated_err = String::new();
2264        let mut last_code = 0i64;
2265        let mut last_data: Option<Value> = None;
2266
2267        // Track execution error for propagation after cleanup
2268        let mut exec_error: Option<anyhow::Error> = None;
2269        let mut exit_code: Option<i64> = None;
2270
2271        for stmt in &def.body {
2272            match self.execute_stmt_flow(stmt).await {
2273                Ok(flow) => {
2274                    // Drain pipeline stderr after each sub-statement.
2275                    let drained = {
2276                        let mut receiver = self.stderr_receiver.lock().await;
2277                        receiver.drain_lossy()
2278                    };
2279                    if !drained.is_empty() {
2280                        accumulated_err.push_str(&drained);
2281                    }
2282
2283                    match flow {
2284                        ControlFlow::Normal(r) => {
2285                            accumulated_out.push_str(&r.out);
2286                            accumulated_err.push_str(&r.err);
2287                            last_code = r.code;
2288                            last_data = r.data;
2289                        }
2290                        ControlFlow::Return { value } => {
2291                            accumulated_out.push_str(&value.out);
2292                            accumulated_err.push_str(&value.err);
2293                            last_code = value.code;
2294                            last_data = value.data;
2295                            break;
2296                        }
2297                        ControlFlow::Exit { code } => {
2298                            exit_code = Some(code);
2299                            break;
2300                        }
2301                        ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2302                            accumulated_out.push_str(&r.out);
2303                            accumulated_err.push_str(&r.err);
2304                            last_code = r.code;
2305                            last_data = r.data;
2306                        }
2307                    }
2308                }
2309                Err(e) => {
2310                    exec_error = Some(e);
2311                    break;
2312                }
2313            }
2314        }
2315
2316        // 4. Pop scope frame and restore original positional parameters (unconditionally)
2317        {
2318            let mut scope = self.scope.write().await;
2319            scope.pop_frame();
2320            scope.set_positional(saved_positional.0, saved_positional.1);
2321        }
2322
2323        // 5. Propagate error or exit after cleanup
2324        if let Some(e) = exec_error {
2325            return Err(e);
2326        }
2327        if let Some(code) = exit_code {
2328            return Ok(ExecResult {
2329                code,
2330                out: accumulated_out,
2331                err: accumulated_err,
2332                data: last_data,
2333                output: None,
2334                did_spill: false,
2335                original_code: None,
2336            });
2337        }
2338
2339        Ok(ExecResult {
2340            code: last_code,
2341            out: accumulated_out,
2342            err: accumulated_err,
2343            data: last_data,
2344            output: None,
2345            did_spill: false,
2346            original_code: None,
2347        })
2348    }
2349
2350    /// Execute the `source` / `.` command to include and run a script.
2351    ///
2352    /// Unlike regular tool execution, `source` executes in the CURRENT scope,
2353    /// allowing the sourced script to set variables and modify shell state.
2354    async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
2355        // Get the file path from the first positional argument
2356        let tool_args = self.build_args_async(args, None).await?;
2357        let path = match tool_args.positional.first() {
2358            Some(Value::String(s)) => s.clone(),
2359            Some(v) => value_to_string(v),
2360            None => {
2361                return Ok(ExecResult::failure(1, "source: missing filename"));
2362            }
2363        };
2364
2365        // Resolve path relative to cwd
2366        let full_path = {
2367            let ctx = self.exec_ctx.read().await;
2368            if path.starts_with('/') {
2369                std::path::PathBuf::from(&path)
2370            } else {
2371                ctx.cwd.join(&path)
2372            }
2373        };
2374
2375        // Read file content via backend
2376        let content = {
2377            let ctx = self.exec_ctx.read().await;
2378            match ctx.backend.read(&full_path, None).await {
2379                Ok(bytes) => {
2380                    String::from_utf8(bytes).map_err(|e| {
2381                        anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
2382                    })?
2383                }
2384                Err(e) => {
2385                    return Ok(ExecResult::failure(
2386                        1,
2387                        format!("source: {}: {}", path, e),
2388                    ));
2389                }
2390            }
2391        };
2392
2393        // Parse the content
2394        let program = match crate::parser::parse(&content) {
2395            Ok(p) => p,
2396            Err(errors) => {
2397                let msg = errors
2398                    .iter()
2399                    .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
2400                    .collect::<Vec<_>>()
2401                    .join("\n");
2402                return Ok(ExecResult::failure(1, format!("source: {}", msg)));
2403            }
2404        };
2405
2406        // Execute each statement in the CURRENT scope (not isolated)
2407        let mut result = ExecResult::success("");
2408        for stmt in program.statements {
2409            if matches!(stmt, crate::ast::Stmt::Empty) {
2410                continue;
2411            }
2412
2413            match self.execute_stmt_flow(&stmt).await {
2414                Ok(flow) => {
2415                    self.drain_stderr_into(&mut result).await;
2416                    match flow {
2417                        ControlFlow::Normal(r) => {
2418                            result = r.clone();
2419                            self.update_last_result(&r).await;
2420                        }
2421                        ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
2422                            return Err(anyhow::anyhow!(
2423                                "source: {}: unexpected break/continue outside loop",
2424                                path
2425                            ));
2426                        }
2427                        ControlFlow::Return { value } => {
2428                            return Ok(value);
2429                        }
2430                        ControlFlow::Exit { code } => {
2431                            result.code = code;
2432                            return Ok(result);
2433                        }
2434                    }
2435                }
2436                Err(e) => {
2437                    return Err(e.context(format!("source: {}", path)));
2438                }
2439            }
2440        }
2441
2442        Ok(result)
2443    }
2444
2445    /// Try to execute a script from PATH directories.
2446    ///
2447    /// Searches PATH for `{name}.kai` files and executes them in isolated scope
2448    /// (like user-defined tools). Returns None if no script is found.
2449    async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2450        // Get PATH from scope (default to "/bin")
2451        let path_value = {
2452            let scope = self.scope.read().await;
2453            scope
2454                .get("PATH")
2455                .map(value_to_string)
2456                .unwrap_or_else(|| "/bin".to_string())
2457        };
2458
2459        // Search PATH directories for script
2460        for dir in path_value.split(':') {
2461            if dir.is_empty() {
2462                continue;
2463            }
2464
2465            // Build script path: {dir}/{name}.kai
2466            let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
2467
2468            // Check if script exists
2469            let exists = {
2470                let ctx = self.exec_ctx.read().await;
2471                ctx.backend.exists(&script_path).await
2472            };
2473
2474            if !exists {
2475                continue;
2476            }
2477
2478            // Read script content
2479            let content = {
2480                let ctx = self.exec_ctx.read().await;
2481                match ctx.backend.read(&script_path, None).await {
2482                    Ok(bytes) => match String::from_utf8(bytes) {
2483                        Ok(s) => s,
2484                        Err(e) => {
2485                            return Ok(Some(ExecResult::failure(
2486                                1,
2487                                format!("{}: invalid UTF-8: {}", script_path.display(), e),
2488                            )));
2489                        }
2490                    },
2491                    Err(e) => {
2492                        return Ok(Some(ExecResult::failure(
2493                            1,
2494                            format!("{}: {}", script_path.display(), e),
2495                        )));
2496                    }
2497                }
2498            };
2499
2500            // Parse the script
2501            let program = match crate::parser::parse(&content) {
2502                Ok(p) => p,
2503                Err(errors) => {
2504                    let msg = errors
2505                        .iter()
2506                        .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
2507                        .collect::<Vec<_>>()
2508                        .join("\n");
2509                    return Ok(Some(ExecResult::failure(1, msg)));
2510                }
2511            };
2512
2513            // Build tool_args from args (async for command substitution support)
2514            let tool_args = self.build_args_async(args, None).await?;
2515
2516            // Create isolated scope (like user tools)
2517            let mut isolated_scope = Scope::new();
2518
2519            // Set up positional parameters ($0 = script name, $1, $2, ... = args)
2520            let positional_args: Vec<String> = tool_args.positional
2521                .iter()
2522                .map(value_to_string)
2523                .collect();
2524            isolated_scope.set_positional(name, positional_args);
2525
2526            // Save current scope and swap with isolated scope
2527            let original_scope = {
2528                let mut scope = self.scope.write().await;
2529                std::mem::replace(&mut *scope, isolated_scope)
2530            };
2531
2532            // Execute script statements — track outcome for cleanup
2533            let mut result = ExecResult::success("");
2534            let mut exec_error: Option<anyhow::Error> = None;
2535            let mut exit_code: Option<i64> = None;
2536
2537            for stmt in program.statements {
2538                if matches!(stmt, crate::ast::Stmt::Empty) {
2539                    continue;
2540                }
2541
2542                match self.execute_stmt_flow(&stmt).await {
2543                    Ok(flow) => {
2544                        match flow {
2545                            ControlFlow::Normal(r) => result = r,
2546                            ControlFlow::Return { value } => {
2547                                result = value;
2548                                break;
2549                            }
2550                            ControlFlow::Exit { code } => {
2551                                exit_code = Some(code);
2552                                break;
2553                            }
2554                            ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2555                                result = r;
2556                            }
2557                        }
2558                    }
2559                    Err(e) => {
2560                        exec_error = Some(e);
2561                        break;
2562                    }
2563                }
2564            }
2565
2566            // Restore original scope unconditionally
2567            {
2568                let mut scope = self.scope.write().await;
2569                *scope = original_scope;
2570            }
2571
2572            // Propagate error or exit after cleanup
2573            if let Some(e) = exec_error {
2574                return Err(e.context(format!("script: {}", script_path.display())));
2575            }
2576            if let Some(code) = exit_code {
2577                result.code = code;
2578                return Ok(Some(result));
2579            }
2580
2581            return Ok(Some(result));
2582        }
2583
2584        // No script found
2585        Ok(None)
2586    }
2587
2588    /// Try to execute an external command from PATH.
2589    ///
2590    /// This is the fallback when no builtin or user-defined tool matches.
2591    /// External commands receive a clean argv (flags preserved in their original format).
2592    ///
2593    /// # Requirements
2594    /// - Command must be found in PATH
2595    /// - Current working directory must be on a real filesystem (not virtual like /v)
2596    ///
2597    /// # Returns
2598    /// - `Ok(Some(result))` if command was found and executed
2599    /// - `Ok(None)` if command was not found in PATH
2600    /// - `Err` on execution errors
2601    #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
2602    async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2603        if !self.allow_external_commands {
2604            return Ok(None);
2605        }
2606
2607        // Get real working directory for relative path resolution and child cwd.
2608        // If the CWD is virtual (no real filesystem path), skip external command
2609        // execution entirely — return None so the dispatch can fall through to
2610        // backend-registered tools.
2611        let real_cwd = {
2612            let ctx = self.exec_ctx.read().await;
2613            match ctx.backend.resolve_real_path(&ctx.cwd) {
2614                Some(p) => p,
2615                None => return Ok(None),
2616            }
2617        };
2618
2619        let executable = if name.contains('/') {
2620            // Resolve relative paths (./script, ../bin/tool) against the shell's cwd
2621            let resolved = if std::path::Path::new(name).is_absolute() {
2622                std::path::PathBuf::from(name)
2623            } else {
2624                real_cwd.join(name)
2625            };
2626            if !resolved.exists() {
2627                return Ok(Some(ExecResult::failure(
2628                    127,
2629                    format!("{}: No such file or directory", name),
2630                )));
2631            }
2632            if !resolved.is_file() {
2633                return Ok(Some(ExecResult::failure(
2634                    126,
2635                    format!("{}: Is a directory", name),
2636                )));
2637            }
2638            #[cfg(unix)]
2639            {
2640                use std::os::unix::fs::PermissionsExt;
2641                let mode = std::fs::metadata(&resolved)
2642                    .map(|m| m.permissions().mode())
2643                    .unwrap_or(0);
2644                if mode & 0o111 == 0 {
2645                    return Ok(Some(ExecResult::failure(
2646                        126,
2647                        format!("{}: Permission denied", name),
2648                    )));
2649                }
2650            }
2651            resolved.to_string_lossy().into_owned()
2652        } else {
2653            // Get PATH from scope or environment
2654            let path_var = {
2655                let scope = self.scope.read().await;
2656                scope
2657                    .get("PATH")
2658                    .map(value_to_string)
2659                    .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
2660            };
2661
2662            // Resolve command in PATH
2663            match resolve_in_path(name, &path_var) {
2664                Some(path) => path,
2665                None => return Ok(None), // Not found - let caller handle error
2666            }
2667        };
2668
2669        tracing::debug!(executable = %executable, "resolved external command");
2670
2671        // Build flat argv (preserves flag format)
2672        let argv = self.build_args_flat(args).await?;
2673
2674        // Get stdin if available
2675        let stdin_data = {
2676            let mut ctx = self.exec_ctx.write().await;
2677            ctx.take_stdin()
2678        };
2679
2680        // Build and spawn the command
2681        use tokio::process::Command;
2682
2683        let mut cmd = Command::new(&executable);
2684        cmd.args(&argv);
2685        cmd.current_dir(&real_cwd);
2686
2687        // Handle stdin
2688        cmd.stdin(if stdin_data.is_some() {
2689            std::process::Stdio::piped()
2690        } else if self.interactive {
2691            std::process::Stdio::inherit()
2692        } else {
2693            std::process::Stdio::null()
2694        });
2695
2696        // In interactive mode, standalone or last-in-pipeline commands inherit
2697        // the terminal's stdout/stderr so output streams in real-time.
2698        // First/middle commands must capture stdout for the pipe — same as bash.
2699        let pipeline_position = {
2700            let ctx = self.exec_ctx.read().await;
2701            ctx.pipeline_position
2702        };
2703        let inherit_output = self.interactive
2704            && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
2705
2706        if inherit_output {
2707            cmd.stdout(std::process::Stdio::inherit());
2708            cmd.stderr(std::process::Stdio::inherit());
2709        } else {
2710            cmd.stdout(std::process::Stdio::piped());
2711            cmd.stderr(std::process::Stdio::piped());
2712        }
2713
2714        // On Unix with job control, put child in its own process group
2715        // and restore default signal handlers (shell ignores SIGTSTP etc.
2716        // but children should respond to them normally).
2717        #[cfg(unix)]
2718        if self.terminal_state.is_some() && inherit_output {
2719            // SAFETY: setpgid and sigaction(SIG_DFL) are async-signal-safe per POSIX
2720            #[allow(unsafe_code)]
2721            unsafe {
2722                cmd.pre_exec(|| {
2723                    // Own process group
2724                    nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
2725                        .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
2726                    // Restore default signal handlers for job control signals
2727                    use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
2728                    let mut sa: nix::libc::sigaction = std::mem::zeroed();
2729                    sa.sa_sigaction = SIG_DFL;
2730                    sigaction(SIGTSTP, &sa, std::ptr::null_mut());
2731                    sigaction(SIGTTOU, &sa, std::ptr::null_mut());
2732                    sigaction(SIGTTIN, &sa, std::ptr::null_mut());
2733                    sigaction(SIGINT, &sa, std::ptr::null_mut());
2734                    Ok(())
2735                });
2736            }
2737        }
2738
2739        // Spawn the process
2740        let mut child = match cmd.spawn() {
2741            Ok(child) => child,
2742            Err(e) => {
2743                return Ok(Some(ExecResult::failure(
2744                    127,
2745                    format!("{}: {}", name, e),
2746                )));
2747            }
2748        };
2749
2750        // Write stdin if present
2751        if let Some(data) = stdin_data
2752            && let Some(mut stdin) = child.stdin.take()
2753        {
2754            use tokio::io::AsyncWriteExt;
2755            if let Err(e) = stdin.write_all(data.as_bytes()).await {
2756                return Ok(Some(ExecResult::failure(
2757                    1,
2758                    format!("{}: failed to write stdin: {}", name, e),
2759                )));
2760            }
2761            // Drop stdin to signal EOF
2762        }
2763
2764        if inherit_output {
2765            // Job control path: use waitpid with WUNTRACED for Ctrl-Z support
2766            #[cfg(unix)]
2767            if let Some(ref term) = self.terminal_state {
2768                let child_id = child.id().unwrap_or(0);
2769                let pid = nix::unistd::Pid::from_raw(child_id as i32);
2770                let pgid = pid; // child is its own pgid leader
2771
2772                // Give the terminal to the child's process group
2773                if let Err(e) = term.give_terminal_to(pgid) {
2774                    tracing::warn!("failed to give terminal to child: {}", e);
2775                }
2776
2777                let term_clone = term.clone();
2778                let cmd_name = name.to_string();
2779                let cmd_display = format!("{} {}", name, argv.join(" "));
2780                let jobs = self.jobs.clone();
2781
2782                let code = tokio::task::block_in_place(move || {
2783                    let result = term_clone.wait_for_foreground(pid);
2784
2785                    // Always reclaim the terminal
2786                    if let Err(e) = term_clone.reclaim_terminal() {
2787                        tracing::warn!("failed to reclaim terminal: {}", e);
2788                    }
2789
2790                    match result {
2791                        crate::terminal::WaitResult::Exited(code) => code as i64,
2792                        crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
2793                        crate::terminal::WaitResult::Stopped(_sig) => {
2794                            // Register as a stopped job
2795                            let rt = tokio::runtime::Handle::current();
2796                            let job_id = rt.block_on(jobs.register_stopped(
2797                                cmd_display,
2798                                child_id,
2799                                child_id, // pgid = pid for group leader
2800                            ));
2801                            eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
2802                            148 // 128 + SIGTSTP(20) on most systems, but we use a fixed value
2803                        }
2804                    }
2805                });
2806
2807                return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
2808            }
2809
2810            // Non-job-control path: simple wait
2811            let status = match child.wait().await {
2812                Ok(s) => s,
2813                Err(e) => {
2814                    return Ok(Some(ExecResult::failure(
2815                        1,
2816                        format!("{}: failed to wait: {}", name, e),
2817                    )));
2818                }
2819            };
2820
2821            let code = status.code().unwrap_or_else(|| {
2822                #[cfg(unix)]
2823                {
2824                    use std::os::unix::process::ExitStatusExt;
2825                    128 + status.signal().unwrap_or(0)
2826                }
2827                #[cfg(not(unix))]
2828                {
2829                    -1
2830                }
2831            }) as i64;
2832
2833            // stdout/stderr already went to the terminal
2834            Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
2835        } else {
2836            // Capture output via bounded streams
2837            let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2838            let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2839
2840            let stdout_pipe = child.stdout.take();
2841            let stderr_pipe = child.stderr.take();
2842
2843            let stdout_clone = stdout_stream.clone();
2844            let stderr_clone = stderr_stream.clone();
2845
2846            let stdout_task = stdout_pipe.map(|pipe| {
2847                tokio::spawn(async move {
2848                    drain_to_stream(pipe, stdout_clone).await;
2849                })
2850            });
2851
2852            let stderr_task = stderr_pipe.map(|pipe| {
2853                tokio::spawn(async move {
2854                    drain_to_stream(pipe, stderr_clone).await;
2855                })
2856            });
2857
2858            let status = match child.wait().await {
2859                Ok(s) => s,
2860                Err(e) => {
2861                    return Ok(Some(ExecResult::failure(
2862                        1,
2863                        format!("{}: failed to wait: {}", name, e),
2864                    )));
2865                }
2866            };
2867
2868            if let Some(task) = stdout_task {
2869                // Ignore join error — the drain task logs its own errors
2870                let _ = task.await;
2871            }
2872            if let Some(task) = stderr_task {
2873                let _ = task.await;
2874            }
2875
2876            let code = status.code().unwrap_or_else(|| {
2877                #[cfg(unix)]
2878                {
2879                    use std::os::unix::process::ExitStatusExt;
2880                    128 + status.signal().unwrap_or(0)
2881                }
2882                #[cfg(not(unix))]
2883                {
2884                    -1
2885                }
2886            }) as i64;
2887
2888            let stdout = stdout_stream.read_string().await;
2889            let stderr = stderr_stream.read_string().await;
2890
2891            Ok(Some(ExecResult::from_output(code, stdout, stderr)))
2892        }
2893    }
2894
2895    // --- Variable Access ---
2896
2897    /// Get a variable value.
2898    pub async fn get_var(&self, name: &str) -> Option<Value> {
2899        let scope = self.scope.read().await;
2900        scope.get(name).cloned()
2901    }
2902
2903    /// Check if error-exit mode is enabled (for testing).
2904    #[cfg(test)]
2905    pub async fn error_exit_enabled(&self) -> bool {
2906        let scope = self.scope.read().await;
2907        scope.error_exit_enabled()
2908    }
2909
2910    /// Set a variable value.
2911    pub async fn set_var(&self, name: &str, value: Value) {
2912        let mut scope = self.scope.write().await;
2913        scope.set(name.to_string(), value);
2914    }
2915
2916    /// Set positional parameters ($0 script name and $1-$9 args).
2917    pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
2918        let mut scope = self.scope.write().await;
2919        scope.set_positional(script_name, args);
2920    }
2921
2922    /// List all variables.
2923    pub async fn list_vars(&self) -> Vec<(String, Value)> {
2924        let scope = self.scope.read().await;
2925        scope.all()
2926    }
2927
2928    // --- CWD ---
2929
2930    /// Get current working directory.
2931    pub async fn cwd(&self) -> PathBuf {
2932        self.exec_ctx.read().await.cwd.clone()
2933    }
2934
2935    /// Set current working directory.
2936    pub async fn set_cwd(&self, path: PathBuf) {
2937        let mut ctx = self.exec_ctx.write().await;
2938        ctx.set_cwd(path);
2939    }
2940
2941    // --- Last Result ---
2942
2943    /// Get the last result ($?).
2944    pub async fn last_result(&self) -> ExecResult {
2945        let scope = self.scope.read().await;
2946        scope.last_result().clone()
2947    }
2948
2949    // --- Tools ---
2950
2951    /// Check if a user-defined function exists.
2952    pub async fn has_function(&self, name: &str) -> bool {
2953        self.user_tools.read().await.contains_key(name)
2954    }
2955
2956    /// Get available tool schemas.
2957    pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
2958        self.tools.schemas()
2959    }
2960
2961    // --- Jobs ---
2962
2963    /// Get job manager.
2964    pub fn jobs(&self) -> Arc<JobManager> {
2965        self.jobs.clone()
2966    }
2967
2968    // --- VFS ---
2969
2970    /// Get VFS router.
2971    pub fn vfs(&self) -> Arc<VfsRouter> {
2972        self.vfs.clone()
2973    }
2974
2975    // --- State ---
2976
2977    /// Reset kernel to initial state.
2978    ///
2979    /// Clears in-memory variables and resets cwd to root.
2980    /// History is not cleared (it persists across resets).
2981    pub async fn reset(&self) -> Result<()> {
2982        {
2983            let mut scope = self.scope.write().await;
2984            *scope = Scope::new();
2985        }
2986        {
2987            let mut ctx = self.exec_ctx.write().await;
2988            ctx.cwd = PathBuf::from("/");
2989        }
2990        Ok(())
2991    }
2992
2993    /// Shutdown the kernel.
2994    pub async fn shutdown(self) -> Result<()> {
2995        // Wait for all background jobs
2996        self.jobs.wait_all().await;
2997        Ok(())
2998    }
2999
3000    /// Dispatch a single command using the full resolution chain.
3001    ///
3002    /// This is the core of `CommandDispatcher` — it syncs state between the
3003    /// passed-in `ExecContext` and kernel-internal state (scope, exec_ctx),
3004    /// then delegates to `execute_command` for the actual dispatch.
3005    ///
3006    /// State flow:
3007    /// 1. ctx → self: sync scope, cwd, stdin so internal methods see current state
3008    /// 2. execute_command: full dispatch chain (user tools, builtins, scripts, external, backend)
3009    /// 3. self → ctx: sync scope, cwd changes back so the pipeline runner sees them
3010    async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
3011        // 1. Sync ctx → self internals
3012        {
3013            let mut scope = self.scope.write().await;
3014            *scope = ctx.scope.clone();
3015        }
3016        {
3017            let mut ec = self.exec_ctx.write().await;
3018            ec.cwd = ctx.cwd.clone();
3019            ec.prev_cwd = ctx.prev_cwd.clone();
3020            ec.stdin = ctx.stdin.take();
3021            ec.stdin_data = ctx.stdin_data.take();
3022            ec.aliases = ctx.aliases.clone();
3023            ec.ignore_config = ctx.ignore_config.clone();
3024            ec.output_limit = ctx.output_limit.clone();
3025            ec.pipeline_position = ctx.pipeline_position;
3026        }
3027
3028        // 2. Execute via the full dispatch chain
3029        let result = self.execute_command(&cmd.name, &cmd.args).await?;
3030
3031        // 3. Sync self → ctx
3032        {
3033            let scope = self.scope.read().await;
3034            ctx.scope = scope.clone();
3035        }
3036        {
3037            let ec = self.exec_ctx.read().await;
3038            ctx.cwd = ec.cwd.clone();
3039            ctx.prev_cwd = ec.prev_cwd.clone();
3040            ctx.aliases = ec.aliases.clone();
3041            ctx.ignore_config = ec.ignore_config.clone();
3042            ctx.output_limit = ec.output_limit.clone();
3043        }
3044
3045        Ok(result)
3046    }
3047}
3048
3049#[async_trait]
3050impl CommandDispatcher for Kernel {
3051    /// Dispatch a command through the Kernel's full resolution chain.
3052    ///
3053    /// This is the single path for all command execution when called from
3054    /// the pipeline runner. It provides the full dispatch chain:
3055    /// user tools → builtins → .kai scripts → external commands → backend tools.
3056    async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
3057        self.dispatch_command(cmd, ctx).await
3058    }
3059}
3060
3061/// Accumulate output from one result into another.
3062///
3063/// This appends stdout and stderr (with newlines as separators) and updates
3064/// the exit code to match the new result. Used to preserve output from
3065/// multiple statements, loop iterations, and command chains.
3066fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
3067    // Materialize lazy OutputData into .out before accumulating.
3068    // Without this, the first command's output stays in .output while
3069    // the second's text gets appended to .out, losing the first.
3070    if accumulated.out.is_empty() {
3071        if let Some(ref output) = accumulated.output {
3072            accumulated.out = output.to_canonical_string();
3073            accumulated.output = None;
3074        }
3075    }
3076    let new_text = new.text_out();
3077    if !accumulated.out.is_empty() && !new_text.is_empty() && !accumulated.out.ends_with('\n') {
3078        accumulated.out.push('\n');
3079    }
3080    accumulated.out.push_str(&new_text);
3081    if !accumulated.err.is_empty() && !new.err.is_empty() && !accumulated.err.ends_with('\n') {
3082        accumulated.err.push('\n');
3083    }
3084    accumulated.err.push_str(&new.err);
3085    accumulated.code = new.code;
3086    accumulated.data = new.data.clone();
3087}
3088
3089/// Check if a value is truthy.
3090fn is_truthy(value: &Value) -> bool {
3091    match value {
3092        Value::Null => false,
3093        Value::Bool(b) => *b,
3094        Value::Int(i) => *i != 0,
3095        Value::Float(f) => *f != 0.0,
3096        Value::String(s) => !s.is_empty(),
3097        Value::Json(json) => match json {
3098            serde_json::Value::Null => false,
3099            serde_json::Value::Array(arr) => !arr.is_empty(),
3100            serde_json::Value::Object(obj) => !obj.is_empty(),
3101            serde_json::Value::Bool(b) => *b,
3102            serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
3103            serde_json::Value::String(s) => !s.is_empty(),
3104        },
3105        Value::Blob(_) => true, // Blob references are always truthy
3106    }
3107}
3108
3109/// Apply tilde expansion to a value.
3110///
3111/// Only string values starting with `~` are expanded.
3112fn apply_tilde_expansion(value: Value) -> Value {
3113    match value {
3114        Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s)),
3115        _ => value,
3116    }
3117}
3118
3119#[cfg(test)]
3120mod tests {
3121    use super::*;
3122
3123    #[tokio::test]
3124    async fn test_kernel_transient() {
3125        let kernel = Kernel::transient().expect("failed to create kernel");
3126        assert_eq!(kernel.name(), "transient");
3127    }
3128
3129    #[tokio::test]
3130    async fn test_kernel_execute_echo() {
3131        let kernel = Kernel::transient().expect("failed to create kernel");
3132        let result = kernel.execute("echo hello").await.expect("execution failed");
3133        assert!(result.ok());
3134        assert_eq!(result.out.trim(), "hello");
3135    }
3136
3137    #[tokio::test]
3138    async fn test_multiple_statements_accumulate_output() {
3139        let kernel = Kernel::transient().expect("failed to create kernel");
3140        let result = kernel
3141            .execute("echo one\necho two\necho three")
3142            .await
3143            .expect("execution failed");
3144        assert!(result.ok());
3145        // Should have all three outputs separated by newlines
3146        assert!(result.out.contains("one"), "missing 'one': {}", result.out);
3147        assert!(result.out.contains("two"), "missing 'two': {}", result.out);
3148        assert!(result.out.contains("three"), "missing 'three': {}", result.out);
3149    }
3150
3151    #[tokio::test]
3152    async fn test_and_chain_accumulates_output() {
3153        let kernel = Kernel::transient().expect("failed to create kernel");
3154        let result = kernel
3155            .execute("echo first && echo second")
3156            .await
3157            .expect("execution failed");
3158        assert!(result.ok());
3159        assert!(result.out.contains("first"), "missing 'first': {}", result.out);
3160        assert!(result.out.contains("second"), "missing 'second': {}", result.out);
3161    }
3162
3163    #[tokio::test]
3164    async fn test_for_loop_accumulates_output() {
3165        let kernel = Kernel::transient().expect("failed to create kernel");
3166        let result = kernel
3167            .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
3168            .await
3169            .expect("execution failed");
3170        assert!(result.ok());
3171        assert!(result.out.contains("item: a"), "missing 'item: a': {}", result.out);
3172        assert!(result.out.contains("item: b"), "missing 'item: b': {}", result.out);
3173        assert!(result.out.contains("item: c"), "missing 'item: c': {}", result.out);
3174    }
3175
3176    #[tokio::test]
3177    async fn test_while_loop_accumulates_output() {
3178        let kernel = Kernel::transient().expect("failed to create kernel");
3179        let result = kernel
3180            .execute(r#"
3181                N=3
3182                while [[ ${N} -gt 0 ]]; do
3183                    echo "N=${N}"
3184                    N=$((N - 1))
3185                done
3186            "#)
3187            .await
3188            .expect("execution failed");
3189        assert!(result.ok());
3190        assert!(result.out.contains("N=3"), "missing 'N=3': {}", result.out);
3191        assert!(result.out.contains("N=2"), "missing 'N=2': {}", result.out);
3192        assert!(result.out.contains("N=1"), "missing 'N=1': {}", result.out);
3193    }
3194
3195    #[tokio::test]
3196    async fn test_kernel_set_var() {
3197        let kernel = Kernel::transient().expect("failed to create kernel");
3198
3199        kernel.execute("X=42").await.expect("set failed");
3200
3201        let value = kernel.get_var("X").await;
3202        assert_eq!(value, Some(Value::Int(42)));
3203    }
3204
3205    #[tokio::test]
3206    async fn test_kernel_var_expansion() {
3207        let kernel = Kernel::transient().expect("failed to create kernel");
3208
3209        kernel.execute("NAME=\"world\"").await.expect("set failed");
3210        let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
3211
3212        assert!(result.ok());
3213        assert_eq!(result.out.trim(), "hello world");
3214    }
3215
3216    #[tokio::test]
3217    async fn test_kernel_last_result() {
3218        let kernel = Kernel::transient().expect("failed to create kernel");
3219
3220        kernel.execute("echo test").await.expect("echo failed");
3221
3222        let last = kernel.last_result().await;
3223        assert!(last.ok());
3224        assert_eq!(last.out.trim(), "test");
3225    }
3226
3227    #[tokio::test]
3228    async fn test_kernel_tool_not_found() {
3229        let kernel = Kernel::transient().expect("failed to create kernel");
3230
3231        let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
3232        assert!(!result.ok());
3233        assert_eq!(result.code, 127);
3234        assert!(result.err.contains("command not found"));
3235    }
3236
3237    #[tokio::test]
3238    async fn test_external_command_true() {
3239        // Use REPL config for passthrough filesystem access
3240        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
3241
3242        // /bin/true should be available on any Unix system
3243        let result = kernel.execute("true").await.expect("execution failed");
3244        // This should use the builtin true, which returns 0
3245        assert!(result.ok(), "true should succeed: {:?}", result);
3246    }
3247
3248    #[tokio::test]
3249    async fn test_external_command_basic() {
3250        // Use REPL config for passthrough filesystem access
3251        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
3252
3253        // Test with /bin/echo which is external
3254        // Note: kaish has a builtin echo, so this will use the builtin
3255        // Let's test with a command that's not a builtin
3256        // Actually, let's just test that PATH resolution works by checking the PATH var
3257        let path_var = std::env::var("PATH").unwrap_or_default();
3258        eprintln!("System PATH: {}", path_var);
3259
3260        // Set PATH in kernel to ensure it's available
3261        kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
3262
3263        // Now try an external command like /usr/bin/env
3264        // But env is also a builtin... let's try uname
3265        let result = kernel.execute("uname").await.expect("execution failed");
3266        eprintln!("uname result: {:?}", result);
3267        // uname should succeed if external commands work
3268        assert!(result.ok() || result.code == 127, "uname: {:?}", result);
3269    }
3270
3271    #[tokio::test]
3272    async fn test_kernel_reset() {
3273        let kernel = Kernel::transient().expect("failed to create kernel");
3274
3275        kernel.execute("X=1").await.expect("set failed");
3276        assert!(kernel.get_var("X").await.is_some());
3277
3278        kernel.reset().await.expect("reset failed");
3279        assert!(kernel.get_var("X").await.is_none());
3280    }
3281
3282    #[tokio::test]
3283    async fn test_kernel_cwd() {
3284        let kernel = Kernel::transient().expect("failed to create kernel");
3285
3286        // Transient kernel uses sandboxed mode with cwd=$HOME
3287        let cwd = kernel.cwd().await;
3288        let home = std::env::var("HOME")
3289            .map(PathBuf::from)
3290            .unwrap_or_else(|_| PathBuf::from("/"));
3291        assert_eq!(cwd, home);
3292
3293        kernel.set_cwd(PathBuf::from("/tmp")).await;
3294        assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
3295    }
3296
3297    #[tokio::test]
3298    async fn test_kernel_list_vars() {
3299        let kernel = Kernel::transient().expect("failed to create kernel");
3300
3301        kernel.execute("A=1").await.ok();
3302        kernel.execute("B=2").await.ok();
3303
3304        let vars = kernel.list_vars().await;
3305        assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
3306        assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
3307    }
3308
3309    #[tokio::test]
3310    async fn test_is_truthy() {
3311        assert!(!is_truthy(&Value::Null));
3312        assert!(!is_truthy(&Value::Bool(false)));
3313        assert!(is_truthy(&Value::Bool(true)));
3314        assert!(!is_truthy(&Value::Int(0)));
3315        assert!(is_truthy(&Value::Int(1)));
3316        assert!(!is_truthy(&Value::String("".into())));
3317        assert!(is_truthy(&Value::String("x".into())));
3318    }
3319
3320    #[tokio::test]
3321    async fn test_jq_in_pipeline() {
3322        let kernel = Kernel::transient().expect("failed to create kernel");
3323        // kaish uses double quotes only; escape inner quotes
3324        let result = kernel
3325            .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
3326            .await
3327            .expect("execution failed");
3328        assert!(result.ok(), "jq pipeline failed: {}", result.err);
3329        assert_eq!(result.out.trim(), "Alice");
3330    }
3331
3332    #[tokio::test]
3333    async fn test_user_defined_tool() {
3334        let kernel = Kernel::transient().expect("failed to create kernel");
3335
3336        // Define a function
3337        kernel
3338            .execute(r#"greet() { echo "Hello, $1!" }"#)
3339            .await
3340            .expect("function definition failed");
3341
3342        // Call the function
3343        let result = kernel
3344            .execute(r#"greet "World""#)
3345            .await
3346            .expect("function call failed");
3347
3348        assert!(result.ok(), "greet failed: {}", result.err);
3349        assert_eq!(result.out.trim(), "Hello, World!");
3350    }
3351
3352    #[tokio::test]
3353    async fn test_user_tool_positional_args() {
3354        let kernel = Kernel::transient().expect("failed to create kernel");
3355
3356        // Define a function with positional param
3357        kernel
3358            .execute(r#"greet() { echo "Hi $1" }"#)
3359            .await
3360            .expect("function definition failed");
3361
3362        // Call with positional argument
3363        let result = kernel
3364            .execute(r#"greet "Amy""#)
3365            .await
3366            .expect("function call failed");
3367
3368        assert!(result.ok(), "greet failed: {}", result.err);
3369        assert_eq!(result.out.trim(), "Hi Amy");
3370    }
3371
3372    #[tokio::test]
3373    async fn test_function_shared_scope() {
3374        let kernel = Kernel::transient().expect("failed to create kernel");
3375
3376        // Set a variable in parent scope
3377        kernel
3378            .execute(r#"SECRET="hidden""#)
3379            .await
3380            .expect("set failed");
3381
3382        // Define a function that accesses and modifies parent variable
3383        kernel
3384            .execute(r#"access_parent() {
3385                echo "${SECRET}"
3386                SECRET="modified"
3387            }"#)
3388            .await
3389            .expect("function definition failed");
3390
3391        // Call the function - it SHOULD see SECRET (shared scope like sh)
3392        let result = kernel.execute("access_parent").await.expect("function call failed");
3393
3394        // Function should have access to parent scope
3395        assert!(
3396            result.out.contains("hidden"),
3397            "Function should access parent scope, got: {}",
3398            result.out
3399        );
3400
3401        // Function should have modified the parent variable
3402        let secret = kernel.get_var("SECRET").await;
3403        assert_eq!(
3404            secret,
3405            Some(Value::String("modified".into())),
3406            "Function should modify parent scope"
3407        );
3408    }
3409
3410    #[tokio::test]
3411    async fn test_exec_builtin() {
3412        let kernel = Kernel::transient().expect("failed to create kernel");
3413        // argv is now a space-separated string or JSON array string
3414        let result = kernel
3415            .execute(r#"exec command="/bin/echo" argv="hello world""#)
3416            .await
3417            .expect("exec failed");
3418
3419        assert!(result.ok(), "exec failed: {}", result.err);
3420        assert_eq!(result.out.trim(), "hello world");
3421    }
3422
3423    #[tokio::test]
3424    async fn test_while_false_never_runs() {
3425        let kernel = Kernel::transient().expect("failed to create kernel");
3426
3427        // A while loop with false condition should never run
3428        let result = kernel
3429            .execute(r#"
3430                while false; do
3431                    echo "should not run"
3432                done
3433            "#)
3434            .await
3435            .expect("while false failed");
3436
3437        assert!(result.ok());
3438        assert!(result.out.is_empty(), "while false should not execute body: {}", result.out);
3439    }
3440
3441    #[tokio::test]
3442    async fn test_while_string_comparison() {
3443        let kernel = Kernel::transient().expect("failed to create kernel");
3444
3445        // Set a flag
3446        kernel.execute(r#"FLAG="go""#).await.expect("set failed");
3447
3448        // Use string comparison as condition (shell-compatible [[ ]] syntax)
3449        // Note: Put echo last so we can check the output
3450        let result = kernel
3451            .execute(r#"
3452                while [[ ${FLAG} == "go" ]]; do
3453                    FLAG="stop"
3454                    echo "running"
3455                done
3456            "#)
3457            .await
3458            .expect("while with string cmp failed");
3459
3460        assert!(result.ok());
3461        assert!(result.out.contains("running"), "should have run once: {}", result.out);
3462
3463        // Verify flag was changed
3464        let flag = kernel.get_var("FLAG").await;
3465        assert_eq!(flag, Some(Value::String("stop".into())));
3466    }
3467
3468    #[tokio::test]
3469    async fn test_while_numeric_comparison() {
3470        let kernel = Kernel::transient().expect("failed to create kernel");
3471
3472        // Test > comparison (shell-compatible [[ ]] with -gt)
3473        kernel.execute("N=5").await.expect("set failed");
3474
3475        // Note: Put echo last so we can check the output
3476        let result = kernel
3477            .execute(r#"
3478                while [[ ${N} -gt 3 ]]; do
3479                    N=3
3480                    echo "N was greater"
3481                done
3482            "#)
3483            .await
3484            .expect("while with > failed");
3485
3486        assert!(result.ok());
3487        assert!(result.out.contains("N was greater"), "should have run once: {}", result.out);
3488    }
3489
3490    #[tokio::test]
3491    async fn test_break_in_while_loop() {
3492        let kernel = Kernel::transient().expect("failed to create kernel");
3493
3494        let result = kernel
3495            .execute(r#"
3496                I=0
3497                while true; do
3498                    I=1
3499                    echo "before break"
3500                    break
3501                    echo "after break"
3502                done
3503            "#)
3504            .await
3505            .expect("while with break failed");
3506
3507        assert!(result.ok());
3508        assert!(result.out.contains("before break"), "should see before break: {}", result.out);
3509        assert!(!result.out.contains("after break"), "should not see after break: {}", result.out);
3510
3511        // Verify we exited the loop
3512        let i = kernel.get_var("I").await;
3513        assert_eq!(i, Some(Value::Int(1)));
3514    }
3515
3516    #[tokio::test]
3517    async fn test_continue_in_while_loop() {
3518        let kernel = Kernel::transient().expect("failed to create kernel");
3519
3520        // Test continue in a while loop where variables persist
3521        // We use string state transition: "start" -> "middle" -> "end"
3522        // continue on "middle" should skip to next iteration
3523        // Shell-compatible: use [[ ]] for comparisons
3524        let result = kernel
3525            .execute(r#"
3526                STATE="start"
3527                AFTER_CONTINUE="no"
3528                while [[ ${STATE} != "done" ]]; do
3529                    if [[ ${STATE} == "start" ]]; then
3530                        STATE="middle"
3531                        continue
3532                        AFTER_CONTINUE="yes"
3533                    fi
3534                    if [[ ${STATE} == "middle" ]]; then
3535                        STATE="done"
3536                    fi
3537                done
3538            "#)
3539            .await
3540            .expect("while with continue failed");
3541
3542        assert!(result.ok());
3543
3544        // STATE should be "done" (we completed the loop)
3545        let state = kernel.get_var("STATE").await;
3546        assert_eq!(state, Some(Value::String("done".into())));
3547
3548        // AFTER_CONTINUE should still be "no" (continue skipped the assignment)
3549        let after = kernel.get_var("AFTER_CONTINUE").await;
3550        assert_eq!(after, Some(Value::String("no".into())));
3551    }
3552
3553    #[tokio::test]
3554    async fn test_break_with_level() {
3555        let kernel = Kernel::transient().expect("failed to create kernel");
3556
3557        // Nested loop with break 2 to exit both loops
3558        // We verify by checking OUTER value:
3559        // - If break 2 works, OUTER stays at 1 (set before for loop)
3560        // - If break 2 fails, OUTER becomes 2 (set after for loop)
3561        let result = kernel
3562            .execute(r#"
3563                OUTER=0
3564                while true; do
3565                    OUTER=1
3566                    for X in "1 2"; do
3567                        break 2
3568                    done
3569                    OUTER=2
3570                done
3571            "#)
3572            .await
3573            .expect("nested break failed");
3574
3575        assert!(result.ok());
3576
3577        // OUTER should be 1 (set before for loop), not 2 (would be set after for loop)
3578        let outer = kernel.get_var("OUTER").await;
3579        assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
3580    }
3581
3582    #[tokio::test]
3583    async fn test_return_from_tool() {
3584        let kernel = Kernel::transient().expect("failed to create kernel");
3585
3586        // Define a function that returns early
3587        kernel
3588            .execute(r#"early_return() {
3589                if [[ $1 == 1 ]]; then
3590                    return 42
3591                fi
3592                echo "not returned"
3593            }"#)
3594            .await
3595            .expect("function definition failed");
3596
3597        // Call with arg=1 should return with exit code 42
3598        // (POSIX shell behavior: return N sets exit code, doesn't output N)
3599        let result = kernel
3600            .execute("early_return 1")
3601            .await
3602            .expect("function call failed");
3603
3604        // Exit code should be 42 (non-zero, so not ok())
3605        assert_eq!(result.code, 42);
3606        // Output should be empty (we returned before echo)
3607        assert!(result.out.is_empty());
3608    }
3609
3610    #[tokio::test]
3611    async fn test_return_without_value() {
3612        let kernel = Kernel::transient().expect("failed to create kernel");
3613
3614        // Define a function that returns without a value
3615        kernel
3616            .execute(r#"early_exit() {
3617                if [[ $1 == "stop" ]]; then
3618                    return
3619                fi
3620                echo "continued"
3621            }"#)
3622            .await
3623            .expect("function definition failed");
3624
3625        // Call with arg="stop" should return early
3626        let result = kernel
3627            .execute(r#"early_exit "stop""#)
3628            .await
3629            .expect("function call failed");
3630
3631        assert!(result.ok());
3632        assert!(result.out.is_empty() || result.out.trim().is_empty());
3633    }
3634
3635    #[tokio::test]
3636    async fn test_exit_stops_execution() {
3637        let kernel = Kernel::transient().expect("failed to create kernel");
3638
3639        // exit should stop further execution
3640        kernel
3641            .execute(r#"
3642                BEFORE="yes"
3643                exit 0
3644                AFTER="yes"
3645            "#)
3646            .await
3647            .expect("execution failed");
3648
3649        // BEFORE should be set, AFTER should not
3650        let before = kernel.get_var("BEFORE").await;
3651        assert_eq!(before, Some(Value::String("yes".into())));
3652
3653        let after = kernel.get_var("AFTER").await;
3654        assert!(after.is_none(), "AFTER should not be set after exit");
3655    }
3656
3657    #[tokio::test]
3658    async fn test_exit_with_code() {
3659        let kernel = Kernel::transient().expect("failed to create kernel");
3660
3661        // exit with code should propagate the exit code
3662        let result = kernel
3663            .execute("exit 42")
3664            .await
3665            .expect("exit failed");
3666
3667        assert_eq!(result.code, 42);
3668        assert!(result.out.is_empty(), "exit should not produce stdout");
3669    }
3670
3671    #[tokio::test]
3672    async fn test_set_e_stops_on_failure() {
3673        let kernel = Kernel::transient().expect("failed to create kernel");
3674
3675        // Enable error-exit mode
3676        kernel.execute("set -e").await.expect("set -e failed");
3677
3678        // Run a sequence where the middle command fails
3679        kernel
3680            .execute(r#"
3681                STEP1="done"
3682                false
3683                STEP2="done"
3684            "#)
3685            .await
3686            .expect("execution failed");
3687
3688        // STEP1 should be set, but STEP2 should NOT be set (exit on false)
3689        let step1 = kernel.get_var("STEP1").await;
3690        assert_eq!(step1, Some(Value::String("done".into())));
3691
3692        let step2 = kernel.get_var("STEP2").await;
3693        assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
3694    }
3695
3696    #[tokio::test]
3697    async fn test_set_plus_e_disables_error_exit() {
3698        let kernel = Kernel::transient().expect("failed to create kernel");
3699
3700        // Enable then disable error-exit mode
3701        kernel.execute("set -e").await.expect("set -e failed");
3702        kernel.execute("set +e").await.expect("set +e failed");
3703
3704        // Now failure should NOT stop execution
3705        kernel
3706            .execute(r#"
3707                STEP1="done"
3708                false
3709                STEP2="done"
3710            "#)
3711            .await
3712            .expect("execution failed");
3713
3714        // Both should be set since +e disables error exit
3715        let step1 = kernel.get_var("STEP1").await;
3716        assert_eq!(step1, Some(Value::String("done".into())));
3717
3718        let step2 = kernel.get_var("STEP2").await;
3719        assert_eq!(step2, Some(Value::String("done".into())));
3720    }
3721
3722    #[tokio::test]
3723    async fn test_set_ignores_unknown_options() {
3724        let kernel = Kernel::transient().expect("failed to create kernel");
3725
3726        // Bash idiom: set -euo pipefail (we support -e, ignore the rest)
3727        let result = kernel
3728            .execute("set -e -u -o pipefail")
3729            .await
3730            .expect("set with unknown options failed");
3731
3732        assert!(result.ok(), "set should succeed with unknown options");
3733
3734        // -e should still be enabled
3735        kernel
3736            .execute(r#"
3737                BEFORE="yes"
3738                false
3739                AFTER="yes"
3740            "#)
3741            .await
3742            .ok();
3743
3744        let after = kernel.get_var("AFTER").await;
3745        assert!(after.is_none(), "-e should be enabled despite unknown options");
3746    }
3747
3748    #[tokio::test]
3749    async fn test_set_no_args_shows_settings() {
3750        let kernel = Kernel::transient().expect("failed to create kernel");
3751
3752        // Enable -e
3753        kernel.execute("set -e").await.expect("set -e failed");
3754
3755        // Call set with no args to see settings
3756        let result = kernel.execute("set").await.expect("set failed");
3757
3758        assert!(result.ok());
3759        assert!(result.out.contains("set -e"), "should show -e is enabled: {}", result.out);
3760    }
3761
3762    #[tokio::test]
3763    async fn test_set_e_in_pipeline() {
3764        let kernel = Kernel::transient().expect("failed to create kernel");
3765
3766        kernel.execute("set -e").await.expect("set -e failed");
3767
3768        // Pipeline failure should trigger exit
3769        kernel
3770            .execute(r#"
3771                BEFORE="yes"
3772                false | cat
3773                AFTER="yes"
3774            "#)
3775            .await
3776            .ok();
3777
3778        let before = kernel.get_var("BEFORE").await;
3779        assert_eq!(before, Some(Value::String("yes".into())));
3780
3781        // AFTER should not be set if pipeline failure triggers exit
3782        // Note: The exit code of a pipeline is the exit code of the last command
3783        // So `false | cat` returns 0 (cat succeeds). This is bash-compatible behavior.
3784        // To test pipeline failure, we need the last command to fail.
3785    }
3786
3787    #[tokio::test]
3788    async fn test_set_e_with_and_chain() {
3789        let kernel = Kernel::transient().expect("failed to create kernel");
3790
3791        kernel.execute("set -e").await.expect("set -e failed");
3792
3793        // Commands in && chain should not trigger -e on the first failure
3794        // because && explicitly handles the error
3795        kernel
3796            .execute(r#"
3797                RESULT="initial"
3798                false && RESULT="chained"
3799                RESULT="continued"
3800            "#)
3801            .await
3802            .ok();
3803
3804        // In bash, commands in && don't trigger -e. The chain handles the failure.
3805        // Our implementation may differ - let's verify current behavior.
3806        let result = kernel.get_var("RESULT").await;
3807        // If we follow bash semantics, RESULT should be "continued"
3808        // If we trigger -e on the false, RESULT stays "initial"
3809        assert!(result.is_some(), "RESULT should be set");
3810    }
3811
3812    #[tokio::test]
3813    async fn test_set_e_exits_in_for_loop() {
3814        let kernel = Kernel::transient().expect("failed to create kernel");
3815
3816        kernel.execute("set -e").await.expect("set -e failed");
3817
3818        kernel
3819            .execute(r#"
3820                REACHED="no"
3821                for x in 1 2 3; do
3822                    false
3823                    REACHED="yes"
3824                done
3825            "#)
3826            .await
3827            .ok();
3828
3829        // With set -e, false should trigger exit; REACHED should remain "no"
3830        let reached = kernel.get_var("REACHED").await;
3831        assert_eq!(reached, Some(Value::String("no".into())),
3832            "set -e should exit on failure in for loop body");
3833    }
3834
3835    #[tokio::test]
3836    async fn test_for_loop_continues_without_set_e() {
3837        let kernel = Kernel::transient().expect("failed to create kernel");
3838
3839        // Without set -e, for loop should continue normally
3840        kernel
3841            .execute(r#"
3842                COUNT=0
3843                for x in 1 2 3; do
3844                    false
3845                    COUNT=$((COUNT + 1))
3846                done
3847            "#)
3848            .await
3849            .ok();
3850
3851        let count = kernel.get_var("COUNT").await;
3852        // Arithmetic produces Int values; accept either Int or String representation
3853        let count_val = match &count {
3854            Some(Value::Int(n)) => *n,
3855            Some(Value::String(s)) => s.parse().unwrap_or(-1),
3856            _ => -1,
3857        };
3858        assert_eq!(count_val, 3,
3859            "without set -e, loop should complete all iterations (got {:?})", count);
3860    }
3861
3862    // ═══════════════════════════════════════════════════════════════════════════
3863    // Source Tests
3864    // ═══════════════════════════════════════════════════════════════════════════
3865
3866    #[tokio::test]
3867    async fn test_source_sets_variables() {
3868        let kernel = Kernel::transient().expect("failed to create kernel");
3869
3870        // Write a script to the VFS
3871        kernel
3872            .execute(r#"write "/test.kai" 'FOO="bar"'"#)
3873            .await
3874            .expect("write failed");
3875
3876        // Source the script
3877        let result = kernel
3878            .execute(r#"source "/test.kai""#)
3879            .await
3880            .expect("source failed");
3881
3882        assert!(result.ok(), "source should succeed");
3883
3884        // Variable should be set in current scope
3885        let foo = kernel.get_var("FOO").await;
3886        assert_eq!(foo, Some(Value::String("bar".into())));
3887    }
3888
3889    #[tokio::test]
3890    async fn test_source_with_dot_alias() {
3891        let kernel = Kernel::transient().expect("failed to create kernel");
3892
3893        // Write a script to the VFS
3894        kernel
3895            .execute(r#"write "/vars.kai" 'X=42'"#)
3896            .await
3897            .expect("write failed");
3898
3899        // Source using . alias
3900        let result = kernel
3901            .execute(r#". "/vars.kai""#)
3902            .await
3903            .expect(". failed");
3904
3905        assert!(result.ok(), ". should succeed");
3906
3907        // Variable should be set in current scope
3908        let x = kernel.get_var("X").await;
3909        assert_eq!(x, Some(Value::Int(42)));
3910    }
3911
3912    #[tokio::test]
3913    async fn test_source_not_found() {
3914        let kernel = Kernel::transient().expect("failed to create kernel");
3915
3916        // Try to source a non-existent file
3917        let result = kernel
3918            .execute(r#"source "/nonexistent.kai""#)
3919            .await
3920            .expect("source should not fail with error");
3921
3922        assert!(!result.ok(), "source of non-existent file should fail");
3923        assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
3924    }
3925
3926    #[tokio::test]
3927    async fn test_source_missing_filename() {
3928        let kernel = Kernel::transient().expect("failed to create kernel");
3929
3930        // Call source with no arguments
3931        let result = kernel
3932            .execute("source")
3933            .await
3934            .expect("source should not fail with error");
3935
3936        assert!(!result.ok(), "source without filename should fail");
3937        assert!(result.err.contains("missing filename"), "error should mention missing filename");
3938    }
3939
3940    #[tokio::test]
3941    async fn test_source_executes_multiple_statements() {
3942        let kernel = Kernel::transient().expect("failed to create kernel");
3943
3944        // Write a script with multiple statements
3945        kernel
3946            .execute(r#"write "/multi.kai" 'A=1
3947B=2
3948C=3'"#)
3949            .await
3950            .expect("write failed");
3951
3952        // Source it
3953        kernel
3954            .execute(r#"source "/multi.kai""#)
3955            .await
3956            .expect("source failed");
3957
3958        // All variables should be set
3959        assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
3960        assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
3961        assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
3962    }
3963
3964    #[tokio::test]
3965    async fn test_source_can_define_functions() {
3966        let kernel = Kernel::transient().expect("failed to create kernel");
3967
3968        // Write a script that defines a function
3969        kernel
3970            .execute(r#"write "/functions.kai" 'greet() {
3971    echo "Hello, $1!"
3972}'"#)
3973            .await
3974            .expect("write failed");
3975
3976        // Source it
3977        kernel
3978            .execute(r#"source "/functions.kai""#)
3979            .await
3980            .expect("source failed");
3981
3982        // Use the defined function
3983        let result = kernel
3984            .execute(r#"greet "World""#)
3985            .await
3986            .expect("greet failed");
3987
3988        assert!(result.ok());
3989        assert!(result.out.contains("Hello, World!"));
3990    }
3991
3992    #[tokio::test]
3993    async fn test_source_inherits_error_exit() {
3994        let kernel = Kernel::transient().expect("failed to create kernel");
3995
3996        // Enable error exit
3997        kernel.execute("set -e").await.expect("set -e failed");
3998
3999        // Write a script that has a failure
4000        kernel
4001            .execute(r#"write "/fail.kai" 'BEFORE="yes"
4002false
4003AFTER="yes"'"#)
4004            .await
4005            .expect("write failed");
4006
4007        // Source it (should exit on false due to set -e)
4008        kernel
4009            .execute(r#"source "/fail.kai""#)
4010            .await
4011            .ok();
4012
4013        // BEFORE should be set, AFTER should NOT be set due to error exit
4014        let before = kernel.get_var("BEFORE").await;
4015        assert_eq!(before, Some(Value::String("yes".into())));
4016
4017        // Note: This test depends on whether error exit is checked within source
4018        // Currently our implementation checks per-statement in the main kernel
4019    }
4020
4021    // ═══════════════════════════════════════════════════════════════════════════
4022    // set -e with && / || chains
4023    // ═══════════════════════════════════════════════════════════════════════════
4024
4025    #[tokio::test]
4026    async fn test_set_e_and_chain_left_fails() {
4027        // set -e; false && echo hi; REACHED=1 → REACHED should be set
4028        let kernel = Kernel::transient().expect("failed to create kernel");
4029        kernel.execute("set -e").await.expect("set -e failed");
4030
4031        kernel
4032            .execute("false && echo hi; REACHED=1")
4033            .await
4034            .expect("execution failed");
4035
4036        let reached = kernel.get_var("REACHED").await;
4037        assert_eq!(
4038            reached,
4039            Some(Value::Int(1)),
4040            "set -e should not trigger on left side of &&"
4041        );
4042    }
4043
4044    #[tokio::test]
4045    async fn test_set_e_and_chain_right_fails() {
4046        // set -e; true && false; REACHED=1 → REACHED should NOT be set
4047        let kernel = Kernel::transient().expect("failed to create kernel");
4048        kernel.execute("set -e").await.expect("set -e failed");
4049
4050        kernel
4051            .execute("true && false; REACHED=1")
4052            .await
4053            .expect("execution failed");
4054
4055        let reached = kernel.get_var("REACHED").await;
4056        assert!(
4057            reached.is_none(),
4058            "set -e should trigger when right side of && fails"
4059        );
4060    }
4061
4062    #[tokio::test]
4063    async fn test_set_e_or_chain_recovers() {
4064        // set -e; false || echo recovered; REACHED=1 → REACHED should be set
4065        let kernel = Kernel::transient().expect("failed to create kernel");
4066        kernel.execute("set -e").await.expect("set -e failed");
4067
4068        kernel
4069            .execute("false || echo recovered; REACHED=1")
4070            .await
4071            .expect("execution failed");
4072
4073        let reached = kernel.get_var("REACHED").await;
4074        assert_eq!(
4075            reached,
4076            Some(Value::Int(1)),
4077            "set -e should not trigger when || recovers the failure"
4078        );
4079    }
4080
4081    #[tokio::test]
4082    async fn test_set_e_or_chain_both_fail() {
4083        // set -e; false || false; REACHED=1 → REACHED should NOT be set
4084        let kernel = Kernel::transient().expect("failed to create kernel");
4085        kernel.execute("set -e").await.expect("set -e failed");
4086
4087        kernel
4088            .execute("false || false; REACHED=1")
4089            .await
4090            .expect("execution failed");
4091
4092        let reached = kernel.get_var("REACHED").await;
4093        assert!(
4094            reached.is_none(),
4095            "set -e should trigger when || chain ultimately fails"
4096        );
4097    }
4098
4099    // ═══════════════════════════════════════════════════════════════════════════
4100    // Cancellation Tests
4101    // ═══════════════════════════════════════════════════════════════════════════
4102
4103    /// Helper: schedule a cancel after a delay from a background thread.
4104    /// Uses std::thread because cancel() is sync and Kernel is not Send.
4105    fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
4106        let k = Arc::clone(kernel);
4107        std::thread::spawn(move || {
4108            std::thread::sleep(delay);
4109            k.cancel();
4110        });
4111    }
4112
4113    #[tokio::test]
4114    async fn test_cancel_interrupts_for_loop() {
4115        let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4116
4117        // Schedule cancel after a short delay from a background OS thread
4118        schedule_cancel(&kernel, std::time::Duration::from_millis(10));
4119
4120        let result = kernel
4121            .execute("for i in $(seq 1 100000); do X=$i; done")
4122            .await
4123            .expect("execute failed");
4124
4125        assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
4126
4127        // The loop variable should be set to something < 100000
4128        let x = kernel.get_var("X").await;
4129        if let Some(Value::Int(n)) = x {
4130            assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
4131        }
4132    }
4133
4134    #[tokio::test]
4135    async fn test_cancel_interrupts_while_loop() {
4136        let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4137        kernel.execute("COUNT=0").await.expect("init failed");
4138
4139        schedule_cancel(&kernel, std::time::Duration::from_millis(10));
4140
4141        let result = kernel
4142            .execute("while true; do COUNT=$((COUNT + 1)); done")
4143            .await
4144            .expect("execute failed");
4145
4146        assert_eq!(result.code, 130);
4147
4148        let count = kernel.get_var("COUNT").await;
4149        if let Some(Value::Int(n)) = count {
4150            assert!(n > 0, "loop should have run at least once");
4151        }
4152    }
4153
4154    #[tokio::test]
4155    async fn test_reset_after_cancel() {
4156        // After cancellation, the next execute() should work normally
4157        let kernel = Kernel::transient().expect("failed to create kernel");
4158        kernel.cancel(); // cancel with nothing running
4159
4160        let result = kernel.execute("echo hello").await.expect("execute failed");
4161        assert!(result.ok(), "execute after cancel should succeed");
4162        assert_eq!(result.out.trim(), "hello");
4163    }
4164
4165    #[tokio::test]
4166    async fn test_cancel_interrupts_statement_sequence() {
4167        let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4168
4169        // Schedule cancel after the first statement runs but before sleep finishes
4170        schedule_cancel(&kernel, std::time::Duration::from_millis(50));
4171
4172        let result = kernel
4173            .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
4174            .await
4175            .expect("execute failed");
4176
4177        assert_eq!(result.code, 130);
4178
4179        // STEP should be 1 (set before sleep), not 2 or 3
4180        let step = kernel.get_var("STEP").await;
4181        assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
4182    }
4183
4184    // ═══════════════════════════════════════════════════════════════════════════
4185    // Case Statement Tests
4186    // ═══════════════════════════════════════════════════════════════════════════
4187
4188    #[tokio::test]
4189    async fn test_case_simple_match() {
4190        let kernel = Kernel::transient().expect("failed to create kernel");
4191
4192        let result = kernel
4193            .execute(r#"
4194                case "hello" in
4195                    hello) echo "matched hello" ;;
4196                    world) echo "matched world" ;;
4197                esac
4198            "#)
4199            .await
4200            .expect("case failed");
4201
4202        assert!(result.ok());
4203        assert_eq!(result.out.trim(), "matched hello");
4204    }
4205
4206    #[tokio::test]
4207    async fn test_case_wildcard_match() {
4208        let kernel = Kernel::transient().expect("failed to create kernel");
4209
4210        let result = kernel
4211            .execute(r#"
4212                case "main.rs" in
4213                    *.py) echo "Python" ;;
4214                    *.rs) echo "Rust" ;;
4215                    *) echo "Unknown" ;;
4216                esac
4217            "#)
4218            .await
4219            .expect("case failed");
4220
4221        assert!(result.ok());
4222        assert_eq!(result.out.trim(), "Rust");
4223    }
4224
4225    #[tokio::test]
4226    async fn test_case_default_match() {
4227        let kernel = Kernel::transient().expect("failed to create kernel");
4228
4229        let result = kernel
4230            .execute(r#"
4231                case "unknown.xyz" in
4232                    *.py) echo "Python" ;;
4233                    *.rs) echo "Rust" ;;
4234                    *) echo "Default" ;;
4235                esac
4236            "#)
4237            .await
4238            .expect("case failed");
4239
4240        assert!(result.ok());
4241        assert_eq!(result.out.trim(), "Default");
4242    }
4243
4244    #[tokio::test]
4245    async fn test_case_no_match() {
4246        let kernel = Kernel::transient().expect("failed to create kernel");
4247
4248        // Case with no default branch and no match
4249        let result = kernel
4250            .execute(r#"
4251                case "nope" in
4252                    "yes") echo "yes" ;;
4253                    "no") echo "no" ;;
4254                esac
4255            "#)
4256            .await
4257            .expect("case failed");
4258
4259        assert!(result.ok());
4260        assert!(result.out.is_empty(), "no match should produce empty output");
4261    }
4262
4263    #[tokio::test]
4264    async fn test_case_with_variable() {
4265        let kernel = Kernel::transient().expect("failed to create kernel");
4266
4267        kernel.execute(r#"LANG="rust""#).await.expect("set failed");
4268
4269        let result = kernel
4270            .execute(r#"
4271                case ${LANG} in
4272                    python) echo "snake" ;;
4273                    rust) echo "crab" ;;
4274                    go) echo "gopher" ;;
4275                esac
4276            "#)
4277            .await
4278            .expect("case failed");
4279
4280        assert!(result.ok());
4281        assert_eq!(result.out.trim(), "crab");
4282    }
4283
4284    #[tokio::test]
4285    async fn test_case_multiple_patterns() {
4286        let kernel = Kernel::transient().expect("failed to create kernel");
4287
4288        let result = kernel
4289            .execute(r#"
4290                case "yes" in
4291                    "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
4292                    "n"|"no"|"N"|"NO") echo "negative" ;;
4293                esac
4294            "#)
4295            .await
4296            .expect("case failed");
4297
4298        assert!(result.ok());
4299        assert_eq!(result.out.trim(), "affirmative");
4300    }
4301
4302    #[tokio::test]
4303    async fn test_case_glob_question_mark() {
4304        let kernel = Kernel::transient().expect("failed to create kernel");
4305
4306        let result = kernel
4307            .execute(r#"
4308                case "test1" in
4309                    test?) echo "matched test?" ;;
4310                    *) echo "default" ;;
4311                esac
4312            "#)
4313            .await
4314            .expect("case failed");
4315
4316        assert!(result.ok());
4317        assert_eq!(result.out.trim(), "matched test?");
4318    }
4319
4320    #[tokio::test]
4321    async fn test_case_char_class() {
4322        let kernel = Kernel::transient().expect("failed to create kernel");
4323
4324        let result = kernel
4325            .execute(r#"
4326                case "Yes" in
4327                    [Yy]*) echo "yes-like" ;;
4328                    [Nn]*) echo "no-like" ;;
4329                esac
4330            "#)
4331            .await
4332            .expect("case failed");
4333
4334        assert!(result.ok());
4335        assert_eq!(result.out.trim(), "yes-like");
4336    }
4337
4338    // ═══════════════════════════════════════════════════════════════════════════
4339    // Cat Stdin Tests
4340    // ═══════════════════════════════════════════════════════════════════════════
4341
4342    #[tokio::test]
4343    async fn test_cat_from_pipeline() {
4344        let kernel = Kernel::transient().expect("failed to create kernel");
4345
4346        let result = kernel
4347            .execute(r#"echo "piped text" | cat"#)
4348            .await
4349            .expect("cat pipeline failed");
4350
4351        assert!(result.ok(), "cat failed: {}", result.err);
4352        assert_eq!(result.out.trim(), "piped text");
4353    }
4354
4355    #[tokio::test]
4356    async fn test_cat_from_pipeline_multiline() {
4357        let kernel = Kernel::transient().expect("failed to create kernel");
4358
4359        let result = kernel
4360            .execute(r#"echo "line1\nline2" | cat -n"#)
4361            .await
4362            .expect("cat pipeline failed");
4363
4364        assert!(result.ok(), "cat failed: {}", result.err);
4365        assert!(result.out.contains("1\t"), "output: {}", result.out);
4366    }
4367
4368    // ═══════════════════════════════════════════════════════════════════════════
4369    // Heredoc Tests
4370    // ═══════════════════════════════════════════════════════════════════════════
4371
4372    #[tokio::test]
4373    async fn test_heredoc_basic() {
4374        let kernel = Kernel::transient().expect("failed to create kernel");
4375
4376        let result = kernel
4377            .execute("cat <<EOF\nhello\nEOF")
4378            .await
4379            .expect("heredoc failed");
4380
4381        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
4382        assert_eq!(result.out.trim(), "hello");
4383    }
4384
4385    #[tokio::test]
4386    async fn test_arithmetic_in_string() {
4387        let kernel = Kernel::transient().expect("failed to create kernel");
4388
4389        let result = kernel
4390            .execute(r#"echo "result: $((1 + 2))""#)
4391            .await
4392            .expect("arithmetic in string failed");
4393
4394        assert!(result.ok(), "echo failed: {}", result.err);
4395        assert_eq!(result.out.trim(), "result: 3");
4396    }
4397
4398    #[tokio::test]
4399    async fn test_heredoc_multiline() {
4400        let kernel = Kernel::transient().expect("failed to create kernel");
4401
4402        let result = kernel
4403            .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
4404            .await
4405            .expect("heredoc failed");
4406
4407        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
4408        assert!(result.out.contains("line1"), "output: {}", result.out);
4409        assert!(result.out.contains("line2"), "output: {}", result.out);
4410        assert!(result.out.contains("line3"), "output: {}", result.out);
4411    }
4412
4413    #[tokio::test]
4414    async fn test_heredoc_variable_expansion() {
4415        // Bug N: unquoted heredoc should expand variables
4416        let kernel = Kernel::transient().expect("failed to create kernel");
4417
4418        kernel.execute("GREETING=hello").await.expect("set var");
4419
4420        let result = kernel
4421            .execute("cat <<EOF\n$GREETING world\nEOF")
4422            .await
4423            .expect("heredoc expansion failed");
4424
4425        assert!(result.ok(), "heredoc expansion failed: {}", result.err);
4426        assert_eq!(result.out.trim(), "hello world");
4427    }
4428
4429    #[tokio::test]
4430    async fn test_heredoc_quoted_no_expansion() {
4431        // Bug N: quoted heredoc (<<'EOF') should NOT expand variables
4432        let kernel = Kernel::transient().expect("failed to create kernel");
4433
4434        kernel.execute("GREETING=hello").await.expect("set var");
4435
4436        let result = kernel
4437            .execute("cat <<'EOF'\n$GREETING world\nEOF")
4438            .await
4439            .expect("quoted heredoc failed");
4440
4441        assert!(result.ok(), "quoted heredoc failed: {}", result.err);
4442        assert_eq!(result.out.trim(), "$GREETING world");
4443    }
4444
4445    #[tokio::test]
4446    async fn test_heredoc_default_value_expansion() {
4447        // Bug N: ${VAR:-default} should expand in unquoted heredocs
4448        let kernel = Kernel::transient().expect("failed to create kernel");
4449
4450        let result = kernel
4451            .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
4452            .await
4453            .expect("heredoc default expansion failed");
4454
4455        assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
4456        assert_eq!(result.out.trim(), "fallback");
4457    }
4458
4459    // ═══════════════════════════════════════════════════════════════════════════
4460    // Read Builtin Tests
4461    // ═══════════════════════════════════════════════════════════════════════════
4462
4463    #[tokio::test]
4464    async fn test_read_from_pipeline() {
4465        let kernel = Kernel::transient().expect("failed to create kernel");
4466
4467        // Pipe input to read
4468        let result = kernel
4469            .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
4470            .await
4471            .expect("read pipeline failed");
4472
4473        assert!(result.ok(), "read failed: {}", result.err);
4474        assert!(result.out.contains("Hello, Alice"), "output: {}", result.out);
4475    }
4476
4477    #[tokio::test]
4478    async fn test_read_multiple_vars_from_pipeline() {
4479        let kernel = Kernel::transient().expect("failed to create kernel");
4480
4481        let result = kernel
4482            .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
4483            .await
4484            .expect("read pipeline failed");
4485
4486        assert!(result.ok(), "read failed: {}", result.err);
4487        assert!(result.out.contains("John is 42"), "output: {}", result.out);
4488    }
4489
4490    // ═══════════════════════════════════════════════════════════════════════════
4491    // Shell-Style Function Tests
4492    // ═══════════════════════════════════════════════════════════════════════════
4493
4494    #[tokio::test]
4495    async fn test_posix_function_with_positional_params() {
4496        let kernel = Kernel::transient().expect("failed to create kernel");
4497
4498        // Define POSIX-style function
4499        kernel
4500            .execute(r#"greet() { echo "Hello, $1!" }"#)
4501            .await
4502            .expect("function definition failed");
4503
4504        // Call the function
4505        let result = kernel
4506            .execute(r#"greet "Amy""#)
4507            .await
4508            .expect("function call failed");
4509
4510        assert!(result.ok(), "greet failed: {}", result.err);
4511        assert_eq!(result.out.trim(), "Hello, Amy!");
4512    }
4513
4514    #[tokio::test]
4515    async fn test_posix_function_multiple_args() {
4516        let kernel = Kernel::transient().expect("failed to create kernel");
4517
4518        // Define function using $1 and $2
4519        kernel
4520            .execute(r#"add_greeting() { echo "$1 $2!" }"#)
4521            .await
4522            .expect("function definition failed");
4523
4524        // Call the function
4525        let result = kernel
4526            .execute(r#"add_greeting "Hello" "World""#)
4527            .await
4528            .expect("function call failed");
4529
4530        assert!(result.ok(), "function failed: {}", result.err);
4531        assert_eq!(result.out.trim(), "Hello World!");
4532    }
4533
4534    #[tokio::test]
4535    async fn test_bash_function_with_positional_params() {
4536        let kernel = Kernel::transient().expect("failed to create kernel");
4537
4538        // Define bash-style function (function keyword, no parens)
4539        kernel
4540            .execute(r#"function greet { echo "Hi $1" }"#)
4541            .await
4542            .expect("function definition failed");
4543
4544        // Call the function
4545        let result = kernel
4546            .execute(r#"greet "Bob""#)
4547            .await
4548            .expect("function call failed");
4549
4550        assert!(result.ok(), "greet failed: {}", result.err);
4551        assert_eq!(result.out.trim(), "Hi Bob");
4552    }
4553
4554    #[tokio::test]
4555    async fn test_shell_function_with_all_args() {
4556        let kernel = Kernel::transient().expect("failed to create kernel");
4557
4558        // Define function using $@ (all args)
4559        kernel
4560            .execute(r#"echo_all() { echo "args: $@" }"#)
4561            .await
4562            .expect("function definition failed");
4563
4564        // Call with multiple args
4565        let result = kernel
4566            .execute(r#"echo_all "a" "b" "c""#)
4567            .await
4568            .expect("function call failed");
4569
4570        assert!(result.ok(), "function failed: {}", result.err);
4571        assert_eq!(result.out.trim(), "args: a b c");
4572    }
4573
4574    #[tokio::test]
4575    async fn test_shell_function_with_arg_count() {
4576        let kernel = Kernel::transient().expect("failed to create kernel");
4577
4578        // Define function using $# (arg count)
4579        kernel
4580            .execute(r#"count_args() { echo "count: $#" }"#)
4581            .await
4582            .expect("function definition failed");
4583
4584        // Call with three args
4585        let result = kernel
4586            .execute(r#"count_args "x" "y" "z""#)
4587            .await
4588            .expect("function call failed");
4589
4590        assert!(result.ok(), "function failed: {}", result.err);
4591        assert_eq!(result.out.trim(), "count: 3");
4592    }
4593
4594    #[tokio::test]
4595    async fn test_shell_function_shared_scope() {
4596        let kernel = Kernel::transient().expect("failed to create kernel");
4597
4598        // Set a variable in parent scope
4599        kernel
4600            .execute(r#"PARENT_VAR="visible""#)
4601            .await
4602            .expect("set failed");
4603
4604        // Define shell function that reads and writes parent variable
4605        kernel
4606            .execute(r#"modify_parent() {
4607                echo "saw: ${PARENT_VAR}"
4608                PARENT_VAR="changed by function"
4609            }"#)
4610            .await
4611            .expect("function definition failed");
4612
4613        // Call the function - it SHOULD see PARENT_VAR (bash-compatible shared scope)
4614        let result = kernel.execute("modify_parent").await.expect("function failed");
4615
4616        assert!(
4617            result.out.contains("visible"),
4618            "Shell function should access parent scope, got: {}",
4619            result.out
4620        );
4621
4622        // Parent variable should be modified
4623        let var = kernel.get_var("PARENT_VAR").await;
4624        assert_eq!(
4625            var,
4626            Some(Value::String("changed by function".into())),
4627            "Shell function should modify parent scope"
4628        );
4629    }
4630
4631    // ═══════════════════════════════════════════════════════════════════════════
4632    // Script Execution via PATH Tests
4633    // ═══════════════════════════════════════════════════════════════════════════
4634
4635    #[tokio::test]
4636    async fn test_script_execution_from_path() {
4637        let kernel = Kernel::transient().expect("failed to create kernel");
4638
4639        // Create /bin directory and script
4640        kernel.execute(r#"mkdir "/bin""#).await.ok();
4641        kernel
4642            .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
4643            .await
4644            .expect("write script failed");
4645
4646        // Set PATH to /bin
4647        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4648
4649        // Call script by name (without .kai extension)
4650        let result = kernel
4651            .execute("hello")
4652            .await
4653            .expect("script execution failed");
4654
4655        assert!(result.ok(), "script failed: {}", result.err);
4656        assert_eq!(result.out.trim(), "Hello from script!");
4657    }
4658
4659    #[tokio::test]
4660    async fn test_script_with_args() {
4661        let kernel = Kernel::transient().expect("failed to create kernel");
4662
4663        // Create script that uses positional params
4664        kernel.execute(r#"mkdir "/bin""#).await.ok();
4665        kernel
4666            .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
4667            .await
4668            .expect("write script failed");
4669
4670        // Set PATH
4671        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4672
4673        // Call script with arg
4674        let result = kernel
4675            .execute(r#"greet "World""#)
4676            .await
4677            .expect("script execution failed");
4678
4679        assert!(result.ok(), "script failed: {}", result.err);
4680        assert_eq!(result.out.trim(), "Hello, World!");
4681    }
4682
4683    #[tokio::test]
4684    async fn test_script_not_found() {
4685        let kernel = Kernel::transient().expect("failed to create kernel");
4686
4687        // Set empty PATH
4688        kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
4689
4690        // Call non-existent script
4691        let result = kernel
4692            .execute("noscript")
4693            .await
4694            .expect("execution failed");
4695
4696        assert!(!result.ok(), "should fail with command not found");
4697        assert_eq!(result.code, 127);
4698        assert!(result.err.contains("command not found"));
4699    }
4700
4701    #[tokio::test]
4702    async fn test_script_path_search_order() {
4703        let kernel = Kernel::transient().expect("failed to create kernel");
4704
4705        // Create two directories with same-named script
4706        // Note: using "myscript" not "test" to avoid conflict with test builtin
4707        kernel.execute(r#"mkdir "/first""#).await.ok();
4708        kernel.execute(r#"mkdir "/second""#).await.ok();
4709        kernel
4710            .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
4711            .await
4712            .expect("write failed");
4713        kernel
4714            .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
4715            .await
4716            .expect("write failed");
4717
4718        // Set PATH with first before second
4719        kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
4720
4721        // Should find first one
4722        let result = kernel
4723            .execute("myscript")
4724            .await
4725            .expect("script execution failed");
4726
4727        assert!(result.ok(), "script failed: {}", result.err);
4728        assert_eq!(result.out.trim(), "from first");
4729    }
4730
4731    // ═══════════════════════════════════════════════════════════════════════════
4732    // Special Variable Tests ($?, $$, unset vars)
4733    // ═══════════════════════════════════════════════════════════════════════════
4734
4735    #[tokio::test]
4736    async fn test_last_exit_code_success() {
4737        let kernel = Kernel::transient().expect("failed to create kernel");
4738
4739        // true exits with 0
4740        let result = kernel.execute("true; echo $?").await.expect("execution failed");
4741        assert!(result.out.contains("0"), "expected 0, got: {}", result.out);
4742    }
4743
4744    #[tokio::test]
4745    async fn test_last_exit_code_failure() {
4746        let kernel = Kernel::transient().expect("failed to create kernel");
4747
4748        // false exits with 1
4749        let result = kernel.execute("false; echo $?").await.expect("execution failed");
4750        assert!(result.out.contains("1"), "expected 1, got: {}", result.out);
4751    }
4752
4753    #[tokio::test]
4754    async fn test_current_pid() {
4755        let kernel = Kernel::transient().expect("failed to create kernel");
4756
4757        let result = kernel.execute("echo $$").await.expect("execution failed");
4758        // PID should be a positive number
4759        let pid: u32 = result.out.trim().parse().expect("PID should be a number");
4760        assert!(pid > 0, "PID should be positive");
4761    }
4762
4763    #[tokio::test]
4764    async fn test_unset_variable_expands_to_empty() {
4765        let kernel = Kernel::transient().expect("failed to create kernel");
4766
4767        // Unset variable in interpolation should be empty
4768        let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
4769        assert_eq!(result.out.trim(), "prefix::suffix");
4770    }
4771
4772    #[tokio::test]
4773    async fn test_eq_ne_operators() {
4774        let kernel = Kernel::transient().expect("failed to create kernel");
4775
4776        // Test -eq operator
4777        let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
4778        assert_eq!(result.out.trim(), "eq works");
4779
4780        // Test -ne operator
4781        let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
4782        assert_eq!(result.out.trim(), "ne works");
4783
4784        // Test -eq with different values
4785        let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
4786        assert_eq!(result.out.trim(), "correct");
4787    }
4788
4789    #[tokio::test]
4790    async fn test_escaped_dollar_in_string() {
4791        let kernel = Kernel::transient().expect("failed to create kernel");
4792
4793        // \$ should produce literal $
4794        let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
4795        assert_eq!(result.out.trim(), "$100");
4796    }
4797
4798    #[tokio::test]
4799    async fn test_special_vars_in_interpolation() {
4800        let kernel = Kernel::transient().expect("failed to create kernel");
4801
4802        // Test $? in string interpolation
4803        let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
4804        assert_eq!(result.out.trim(), "exit: 0");
4805
4806        // Test $$ in string interpolation
4807        let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
4808        assert!(result.out.starts_with("pid: "), "unexpected output: {}", result.out);
4809        let pid_part = result.out.trim().strip_prefix("pid: ").unwrap();
4810        let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
4811    }
4812
4813    // ═══════════════════════════════════════════════════════════════════════════
4814    // Command Substitution Tests
4815    // ═══════════════════════════════════════════════════════════════════════════
4816
4817    #[tokio::test]
4818    async fn test_command_subst_assignment() {
4819        let kernel = Kernel::transient().expect("failed to create kernel");
4820
4821        // Command substitution in assignment
4822        let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
4823        assert_eq!(result.out.trim(), "hello");
4824    }
4825
4826    #[tokio::test]
4827    async fn test_command_subst_with_args() {
4828        let kernel = Kernel::transient().expect("failed to create kernel");
4829
4830        // Command substitution with string argument
4831        let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
4832        assert_eq!(result.out.trim(), "a b c");
4833    }
4834
4835    #[tokio::test]
4836    async fn test_command_subst_nested_vars() {
4837        let kernel = Kernel::transient().expect("failed to create kernel");
4838
4839        // Variables inside command substitution
4840        let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
4841        assert_eq!(result.out.trim(), "hello world");
4842    }
4843
4844    #[tokio::test]
4845    async fn test_background_job_basic() {
4846        use std::time::Duration;
4847
4848        let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
4849
4850        // Run a simple background command
4851        let result = kernel.execute("echo hello &").await.expect("execution failed");
4852        assert!(result.ok(), "background command should succeed: {}", result.err);
4853        assert!(result.out.contains("[1]"), "should return job ID: {}", result.out);
4854
4855        // Give the job time to complete
4856        tokio::time::sleep(Duration::from_millis(100)).await;
4857
4858        // Check job status
4859        let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
4860        assert!(status.ok(), "status should succeed: {}", status.err);
4861        assert!(
4862            status.out.contains("done:") || status.out.contains("running"),
4863            "should have valid status: {}",
4864            status.out
4865        );
4866
4867        // Check stdout
4868        let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
4869        assert!(stdout.ok());
4870        assert!(stdout.out.contains("hello"));
4871    }
4872
4873    #[tokio::test]
4874    async fn test_heredoc_piped_to_command() {
4875        // Bug 4: heredoc content should pipe through to next command
4876        let kernel = Kernel::transient().expect("kernel");
4877        let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
4878        assert!(result.ok(), "heredoc | cat failed: {}", result.err);
4879        assert_eq!(result.out.trim(), "hello world");
4880    }
4881
4882    #[tokio::test]
4883    async fn test_for_loop_glob_iterates() {
4884        // Bug 1: for F in $(glob ...) should iterate per file, not once
4885        let kernel = Kernel::transient().expect("kernel");
4886        let dir = format!("/tmp/kaish_test_glob_{}", std::process::id());
4887        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4888        kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4889        kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4890        let result = kernel.execute(&format!(r#"
4891            N=0
4892            for F in $(glob "{dir}/*.txt"); do
4893                N=$((N + 1))
4894            done
4895            echo $N
4896        "#)).await.unwrap();
4897        assert!(result.ok(), "for glob failed: {}", result.err);
4898        assert_eq!(result.out.trim(), "2", "Should iterate 2 files, got: {}", result.out);
4899        kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4900        kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4901    }
4902
4903    #[tokio::test]
4904    async fn test_bare_glob_expansion_echo() {
4905        let kernel = Kernel::transient().expect("kernel");
4906        let dir = format!("/tmp/kaish_test_bareglob_{}", std::process::id());
4907        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4908        kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4909        kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4910        kernel.execute(&format!("echo c > {dir}/c.rs")).await.unwrap();
4911        kernel.execute(&format!("cd {dir}")).await.unwrap();
4912        let result = kernel.execute("echo *.txt").await.unwrap();
4913        assert!(result.ok(), "echo *.txt failed: {}", result.err);
4914        let out = result.out.trim();
4915        // Should contain both .txt files (order may vary)
4916        assert!(out.contains("a.txt"), "missing a.txt in: {}", out);
4917        assert!(out.contains("b.txt"), "missing b.txt in: {}", out);
4918        assert!(!out.contains("c.rs"), "should not contain c.rs in: {}", out);
4919        // cleanup
4920        kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4921        kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4922        kernel.execute(&format!("rm {dir}/c.rs")).await.unwrap();
4923    }
4924
4925    #[tokio::test]
4926    async fn test_bare_glob_no_matches_errors() {
4927        let kernel = Kernel::transient().expect("kernel");
4928        let dir = format!("/tmp/kaish_test_bareglob_nomatch_{}", std::process::id());
4929        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4930        kernel.execute(&format!("cd {dir}")).await.unwrap();
4931        let result = kernel.execute("echo *.nonexistent").await;
4932        match &result {
4933            Ok(exec) => {
4934                // No-match glob should produce a non-zero exit code
4935                assert!(!exec.ok(), "expected failure, got success: out={}, err={}", exec.out, exec.err);
4936                assert!(exec.err.contains("no matches"), "error should say no matches: {}", exec.err);
4937            }
4938            Err(e) => {
4939                assert!(e.to_string().contains("no matches"), "error should say no matches: {}", e);
4940            }
4941        }
4942    }
4943
4944    #[tokio::test]
4945    async fn test_bare_glob_disabled_with_set() {
4946        let kernel = Kernel::transient().expect("kernel");
4947        let dir = format!("/tmp/kaish_test_bareglob_noglob_{}", std::process::id());
4948        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4949        kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4950        kernel.execute(&format!("cd {dir}")).await.unwrap();
4951        // Disable glob expansion
4952        kernel.execute("set +o glob").await.unwrap();
4953        let result = kernel.execute("echo *.txt").await.unwrap();
4954        // With glob disabled, *.txt should be passed as literal string
4955        assert!(result.ok(), "echo should succeed: {}", result.err);
4956        assert_eq!(result.out.trim(), "*.txt", "should be literal: {}", result.out);
4957        // cleanup
4958        kernel.execute("set -o glob").await.unwrap();
4959        kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4960    }
4961
4962    #[tokio::test]
4963    async fn test_bare_glob_quoted_not_expanded() {
4964        let kernel = Kernel::transient().expect("kernel");
4965        let dir = format!("/tmp/kaish_test_bareglob_quoted_{}", std::process::id());
4966        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4967        kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4968        kernel.execute(&format!("cd {dir}")).await.unwrap();
4969        // Quoted globs should NOT expand
4970        let result = kernel.execute("echo \"*.txt\"").await.unwrap();
4971        assert!(result.ok(), "echo should succeed: {}", result.err);
4972        assert_eq!(result.out.trim(), "*.txt", "quoted should be literal: {}", result.out);
4973        // cleanup
4974        kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4975    }
4976
4977    #[tokio::test]
4978    async fn test_bare_glob_for_loop() {
4979        let kernel = Kernel::transient().expect("kernel");
4980        let dir = format!("/tmp/kaish_test_bareglob_forloop_{}", std::process::id());
4981        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4982        kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4983        kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4984        kernel.execute(&format!("cd {dir}")).await.unwrap();
4985        let result = kernel.execute(r#"
4986            N=0
4987            for f in *.txt; do
4988                N=$((N + 1))
4989            done
4990            echo $N
4991        "#).await.unwrap();
4992        assert!(result.ok(), "for loop failed: {}", result.err);
4993        assert_eq!(result.out.trim(), "2", "should iterate 2 files: {}", result.out);
4994        // cleanup
4995        kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4996        kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4997    }
4998
4999    #[tokio::test]
5000    async fn test_glob_in_assignment_is_literal() {
5001        let kernel = Kernel::transient().expect("kernel");
5002        let result = kernel.execute("X=*.txt; echo $X").await.unwrap();
5003        assert!(result.ok());
5004        assert_eq!(result.out.trim(), "*.txt", "glob in assignment should be literal");
5005    }
5006
5007    #[tokio::test]
5008    async fn test_glob_in_test_expr_is_literal() {
5009        let kernel = Kernel::transient().expect("kernel");
5010        let result = kernel.execute(r#"
5011            if [[ *.txt == "*.txt" ]]; then
5012                echo "match"
5013            else
5014                echo "no"
5015            fi
5016        "#).await.unwrap();
5017        assert!(result.ok());
5018        assert_eq!(result.out.trim(), "match", "glob in test expr should be literal");
5019    }
5020
5021    #[tokio::test]
5022    async fn test_command_subst_echo_not_iterable() {
5023        // Regression guard: $(echo "a b c") must remain a single string
5024        let kernel = Kernel::transient().expect("kernel");
5025        let result = kernel.execute(r#"
5026            N=0
5027            for X in $(echo "a b c"); do N=$((N + 1)); done
5028            echo $N
5029        "#).await.unwrap();
5030        assert!(result.ok());
5031        assert_eq!(result.out.trim(), "1", "echo should be one item: {}", result.out);
5032    }
5033
5034    // -- accumulate_result / newline tests --
5035
5036    #[test]
5037    fn test_accumulate_no_double_newlines() {
5038        // When output already ends with \n, accumulate should not add another
5039        let mut acc = ExecResult::success("line1\n");
5040        let new = ExecResult::success("line2\n");
5041        accumulate_result(&mut acc, &new);
5042        assert_eq!(acc.out, "line1\nline2\n");
5043        assert!(!acc.out.contains("\n\n"), "should not have double newlines: {:?}", acc.out);
5044    }
5045
5046    #[test]
5047    fn test_accumulate_adds_separator_when_needed() {
5048        // When output does NOT end with \n, accumulate adds one
5049        let mut acc = ExecResult::success("line1");
5050        let new = ExecResult::success("line2");
5051        accumulate_result(&mut acc, &new);
5052        assert_eq!(acc.out, "line1\nline2");
5053    }
5054
5055    #[test]
5056    fn test_accumulate_empty_into_nonempty() {
5057        let mut acc = ExecResult::success("");
5058        let new = ExecResult::success("hello\n");
5059        accumulate_result(&mut acc, &new);
5060        assert_eq!(acc.out, "hello\n");
5061    }
5062
5063    #[test]
5064    fn test_accumulate_nonempty_into_empty() {
5065        let mut acc = ExecResult::success("hello\n");
5066        let new = ExecResult::success("");
5067        accumulate_result(&mut acc, &new);
5068        assert_eq!(acc.out, "hello\n");
5069    }
5070
5071    #[test]
5072    fn test_accumulate_stderr_no_double_newlines() {
5073        let mut acc = ExecResult::failure(1, "err1\n");
5074        let new = ExecResult::failure(1, "err2\n");
5075        accumulate_result(&mut acc, &new);
5076        assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
5077    }
5078
5079    #[tokio::test]
5080    async fn test_multiple_echo_no_blank_lines() {
5081        let kernel = Kernel::transient().expect("kernel");
5082        let result = kernel
5083            .execute("echo one\necho two\necho three")
5084            .await
5085            .expect("execution failed");
5086        assert!(result.ok());
5087        assert_eq!(result.out, "one\ntwo\nthree\n");
5088    }
5089
5090    #[tokio::test]
5091    async fn test_for_loop_no_blank_lines() {
5092        let kernel = Kernel::transient().expect("kernel");
5093        let result = kernel
5094            .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
5095            .await
5096            .expect("execution failed");
5097        assert!(result.ok());
5098        assert_eq!(result.out, "item: a\nitem: b\nitem: c\n");
5099    }
5100
5101    #[tokio::test]
5102    async fn test_for_command_subst_no_blank_lines() {
5103        let kernel = Kernel::transient().expect("kernel");
5104        let result = kernel
5105            .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
5106            .await
5107            .expect("execution failed");
5108        assert!(result.ok());
5109        assert_eq!(result.out, "n=1\nn=2\nn=3\n");
5110    }
5111
5112}