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                    let item = self.eval_expr_async(item_expr).await?;
900                    // NO implicit word splitting - arrays iterate, strings stay whole
901                    match &item {
902                        // JSON arrays iterate over elements
903                        Value::Json(serde_json::Value::Array(arr)) => {
904                            for elem in arr {
905                                items.push(json_to_value(elem.clone()));
906                            }
907                        }
908                        // Strings are ONE value - no splitting!
909                        // Use $(split "$VAR") for explicit splitting
910                        Value::String(_) => {
911                            items.push(item);
912                        }
913                        // Other values as-is
914                        _ => items.push(item),
915                    }
916                }
917
918                let mut result = ExecResult::success("");
919                {
920                    let mut scope = self.scope.write().await;
921                    scope.push_frame();
922                }
923
924                'outer: for item in items {
925                    // Cancellation checkpoint per iteration
926                    if self.is_cancelled() {
927                        let mut scope = self.scope.write().await;
928                        scope.pop_frame();
929                        result.code = 130;
930                        return Ok(ControlFlow::ok(result));
931                    }
932                    {
933                        let mut scope = self.scope.write().await;
934                        scope.set(&for_loop.variable, item);
935                    }
936                    for stmt in &for_loop.body {
937                        let mut flow = self.execute_stmt_flow(stmt).await?;
938                        self.drain_stderr_into(&mut result).await;
939                        match &mut flow {
940                            ControlFlow::Normal(r) => {
941                                accumulate_result(&mut result, r);
942                                if !r.ok() {
943                                    let scope = self.scope.read().await;
944                                    if scope.error_exit_enabled() {
945                                        drop(scope);
946                                        let mut scope = self.scope.write().await;
947                                        scope.pop_frame();
948                                        return Ok(ControlFlow::exit_code(r.code));
949                                    }
950                                }
951                            }
952                            ControlFlow::Break { .. } => {
953                                if flow.decrement_level() {
954                                    break 'outer;
955                                }
956                                let mut scope = self.scope.write().await;
957                                scope.pop_frame();
958                                return Ok(flow);
959                            }
960                            ControlFlow::Continue { .. } => {
961                                if flow.decrement_level() {
962                                    continue 'outer;
963                                }
964                                let mut scope = self.scope.write().await;
965                                scope.pop_frame();
966                                return Ok(flow);
967                            }
968                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
969                                let mut scope = self.scope.write().await;
970                                scope.pop_frame();
971                                return Ok(flow);
972                            }
973                        }
974                    }
975                }
976
977                {
978                    let mut scope = self.scope.write().await;
979                    scope.pop_frame();
980                }
981                Ok(ControlFlow::ok(result))
982            }
983            Stmt::While(while_loop) => {
984                let mut result = ExecResult::success("");
985
986                'outer: loop {
987                    // Evaluate condition - use async to support command substitution
988                    // Cancellation checkpoint per iteration
989                    if self.is_cancelled() {
990                        result.code = 130;
991                        return Ok(ControlFlow::ok(result));
992                    }
993
994                    let cond_value = self.eval_expr_async(&while_loop.condition).await?;
995
996                    if !is_truthy(&cond_value) {
997                        break;
998                    }
999
1000                    // Execute body
1001                    for stmt in &while_loop.body {
1002                        let mut flow = self.execute_stmt_flow(stmt).await?;
1003                        self.drain_stderr_into(&mut result).await;
1004                        match &mut flow {
1005                            ControlFlow::Normal(r) => {
1006                                accumulate_result(&mut result, r);
1007                                if !r.ok() {
1008                                    let scope = self.scope.read().await;
1009                                    if scope.error_exit_enabled() {
1010                                        return Ok(ControlFlow::exit_code(r.code));
1011                                    }
1012                                }
1013                            }
1014                            ControlFlow::Break { .. } => {
1015                                if flow.decrement_level() {
1016                                    break 'outer;
1017                                }
1018                                return Ok(flow);
1019                            }
1020                            ControlFlow::Continue { .. } => {
1021                                if flow.decrement_level() {
1022                                    continue 'outer;
1023                                }
1024                                return Ok(flow);
1025                            }
1026                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
1027                                return Ok(flow);
1028                            }
1029                        }
1030                    }
1031                }
1032
1033                Ok(ControlFlow::ok(result))
1034            }
1035            Stmt::Case(case_stmt) => {
1036                // Evaluate the expression to match against
1037                let match_value = {
1038                    let value = self.eval_expr_async(&case_stmt.expr).await?;
1039                    value_to_string(&value)
1040                };
1041
1042                // Try each branch until we find a match
1043                for branch in &case_stmt.branches {
1044                    let matched = branch.patterns.iter().any(|pattern| {
1045                        glob_match(pattern, &match_value)
1046                    });
1047
1048                    if matched {
1049                        // Execute the branch body
1050                        let mut result = ExecResult::success("");
1051                        for stmt in &branch.body {
1052                            let flow = self.execute_stmt_flow(stmt).await?;
1053                            match flow {
1054                                ControlFlow::Normal(r) => {
1055                                    accumulate_result(&mut result, &r);
1056                                    self.drain_stderr_into(&mut result).await;
1057                                }
1058                                other => {
1059                                    self.drain_stderr_into(&mut result).await;
1060                                    return Ok(other);
1061                                }
1062                            }
1063                        }
1064                        return Ok(ControlFlow::ok(result));
1065                    }
1066                }
1067
1068                // No match - return success with empty output (like sh)
1069                Ok(ControlFlow::ok(ExecResult::success("")))
1070            }
1071            Stmt::Break(levels) => {
1072                Ok(ControlFlow::break_n(levels.unwrap_or(1)))
1073            }
1074            Stmt::Continue(levels) => {
1075                Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
1076            }
1077            Stmt::Return(expr) => {
1078                // return [N] - N becomes the exit code, NOT stdout
1079                // Shell semantics: return sets exit code, doesn't produce output
1080                let result = if let Some(e) = expr {
1081                    let val = self.eval_expr_async(e).await?;
1082                    // Convert value to exit code
1083                    let code = match val {
1084                        Value::Int(n) => n,
1085                        Value::Bool(b) => if b { 0 } else { 1 },
1086                        _ => 0,
1087                    };
1088                    ExecResult {
1089                        code,
1090                        out: String::new(),
1091                        err: String::new(),
1092                        data: None,
1093                        output: None,
1094                        did_spill: false,
1095                        original_code: None,
1096                    }
1097                } else {
1098                    ExecResult::success("")
1099                };
1100                Ok(ControlFlow::return_value(result))
1101            }
1102            Stmt::Exit(expr) => {
1103                let code = if let Some(e) = expr {
1104                    let val = self.eval_expr_async(e).await?;
1105                    match val {
1106                        Value::Int(n) => n,
1107                        _ => 0,
1108                    }
1109                } else {
1110                    0
1111                };
1112                Ok(ControlFlow::exit_code(code))
1113            }
1114            Stmt::ToolDef(tool_def) => {
1115                let mut user_tools = self.user_tools.write().await;
1116                user_tools.insert(tool_def.name.clone(), tool_def.clone());
1117                Ok(ControlFlow::ok(ExecResult::success("")))
1118            }
1119            Stmt::AndChain { left, right } => {
1120                // cmd1 && cmd2 - run cmd2 only if cmd1 succeeds (exit code 0)
1121                // Suppress errexit for the left side — && handles failure itself.
1122                {
1123                    let mut scope = self.scope.write().await;
1124                    scope.suppress_errexit();
1125                }
1126                let left_flow = self.execute_stmt_flow(left).await?;
1127                {
1128                    let mut scope = self.scope.write().await;
1129                    scope.unsuppress_errexit();
1130                }
1131                match left_flow {
1132                    ControlFlow::Normal(mut left_result) => {
1133                        self.drain_stderr_into(&mut left_result).await;
1134                        self.update_last_result(&left_result).await;
1135                        if left_result.ok() {
1136                            let right_flow = self.execute_stmt_flow(right).await?;
1137                            match right_flow {
1138                                ControlFlow::Normal(mut right_result) => {
1139                                    self.drain_stderr_into(&mut right_result).await;
1140                                    self.update_last_result(&right_result).await;
1141                                    let mut combined = left_result;
1142                                    accumulate_result(&mut combined, &right_result);
1143                                    Ok(ControlFlow::ok(combined))
1144                                }
1145                                other => Ok(other),
1146                            }
1147                        } else {
1148                            Ok(ControlFlow::ok(left_result))
1149                        }
1150                    }
1151                    _ => Ok(left_flow),
1152                }
1153            }
1154            Stmt::OrChain { left, right } => {
1155                // cmd1 || cmd2 - run cmd2 only if cmd1 fails (non-zero exit code)
1156                // Suppress errexit for the left side — || handles failure itself.
1157                {
1158                    let mut scope = self.scope.write().await;
1159                    scope.suppress_errexit();
1160                }
1161                let left_flow = self.execute_stmt_flow(left).await?;
1162                {
1163                    let mut scope = self.scope.write().await;
1164                    scope.unsuppress_errexit();
1165                }
1166                match left_flow {
1167                    ControlFlow::Normal(mut left_result) => {
1168                        self.drain_stderr_into(&mut left_result).await;
1169                        self.update_last_result(&left_result).await;
1170                        if !left_result.ok() {
1171                            let right_flow = self.execute_stmt_flow(right).await?;
1172                            match right_flow {
1173                                ControlFlow::Normal(mut right_result) => {
1174                                    self.drain_stderr_into(&mut right_result).await;
1175                                    self.update_last_result(&right_result).await;
1176                                    let mut combined = left_result;
1177                                    accumulate_result(&mut combined, &right_result);
1178                                    Ok(ControlFlow::ok(combined))
1179                                }
1180                                other => Ok(other),
1181                            }
1182                        } else {
1183                            Ok(ControlFlow::ok(left_result))
1184                        }
1185                    }
1186                    _ => Ok(left_flow), // Propagate non-normal flow
1187                }
1188            }
1189            Stmt::Test(test_expr) => {
1190                let is_true = self.eval_test_async(test_expr).await?;
1191                if is_true {
1192                    Ok(ControlFlow::ok(ExecResult::success("")))
1193                } else {
1194                    Ok(ControlFlow::ok(ExecResult::failure(1, "")))
1195                }
1196            }
1197            Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
1198        }
1199        }.instrument(span))
1200    }
1201
1202    /// Execute a pipeline.
1203    #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
1204    async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1205        if pipeline.commands.is_empty() {
1206            return Ok(ExecResult::success(""));
1207        }
1208
1209        // Handle background execution (`&` operator)
1210        if pipeline.background {
1211            return self.execute_background(pipeline).await;
1212        }
1213
1214        // All commands go through the runner with the Kernel as dispatcher.
1215        // This is the single execution path — no fast path for single commands.
1216        //
1217        // IMPORTANT: We snapshot exec_ctx into a local context and release the
1218        // lock before running. This prevents deadlocks when dispatch_command
1219        // is called from within the pipeline and recursively triggers another
1220        // pipeline (e.g., via user-defined tools).
1221        let mut ctx = {
1222            let ec = self.exec_ctx.read().await;
1223            let scope = self.scope.read().await;
1224            ExecContext {
1225                backend: ec.backend.clone(),
1226                scope: scope.clone(),
1227                cwd: ec.cwd.clone(),
1228                prev_cwd: ec.prev_cwd.clone(),
1229                stdin: None,
1230                stdin_data: None,
1231                pipe_stdin: None,
1232                pipe_stdout: None,
1233                stderr: ec.stderr.clone(),
1234                tool_schemas: ec.tool_schemas.clone(),
1235                tools: ec.tools.clone(),
1236                job_manager: ec.job_manager.clone(),
1237                pipeline_position: PipelinePosition::Only,
1238                interactive: self.interactive,
1239                aliases: ec.aliases.clone(),
1240                ignore_config: ec.ignore_config.clone(),
1241                output_limit: ec.output_limit.clone(),
1242                allow_external_commands: self.allow_external_commands,
1243                nonce_store: ec.nonce_store.clone(),
1244                #[cfg(unix)]
1245                terminal_state: ec.terminal_state.clone(),
1246            }
1247        }; // locks released
1248
1249        let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
1250
1251        // Post-hoc spill check (catches builtins and fast external commands)
1252        if ctx.output_limit.is_enabled() {
1253            let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
1254        }
1255
1256        // Signal spill with exit 3; agent reads the spill file directly
1257        // (use `set +o output-limit` before cat/head/tail to bypass the limit)
1258        if result.did_spill {
1259            result.original_code = Some(result.code);
1260            result.code = 3;
1261        }
1262
1263        // Sync changes back from context
1264        {
1265            let mut ec = self.exec_ctx.write().await;
1266            ec.cwd = ctx.cwd.clone();
1267            ec.prev_cwd = ctx.prev_cwd.clone();
1268            ec.aliases = ctx.aliases.clone();
1269            ec.ignore_config = ctx.ignore_config.clone();
1270            ec.output_limit = ctx.output_limit.clone();
1271        }
1272        {
1273            let mut scope = self.scope.write().await;
1274            *scope = ctx.scope.clone();
1275        }
1276
1277        Ok(result)
1278    }
1279
1280    /// Execute a pipeline in the background.
1281    ///
1282    /// The command is spawned as a tokio task, registered with the JobManager,
1283    /// and its output is captured via BoundedStreams. The job is observable via
1284    /// `/v/jobs/{id}/stdout`, `/v/jobs/{id}/stderr`, and `/v/jobs/{id}/status`.
1285    ///
1286    /// Returns immediately with a job ID like "[1]".
1287    #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
1288    async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1289        use tokio::sync::oneshot;
1290
1291        // Format the command for display in /v/jobs/{id}/command
1292        let command_str = self.format_pipeline(pipeline);
1293
1294        // Create bounded streams for output capture
1295        let stdout = Arc::new(BoundedStream::default_size());
1296        let stderr = Arc::new(BoundedStream::default_size());
1297
1298        // Create channel for result notification
1299        let (tx, rx) = oneshot::channel();
1300
1301        // Register with JobManager to get job ID and create VFS entries
1302        let job_id = self.jobs.register_with_streams(
1303            command_str.clone(),
1304            rx,
1305            stdout.clone(),
1306            stderr.clone(),
1307        ).await;
1308
1309        // Clone state needed for the spawned task
1310        let runner = self.runner.clone();
1311        let commands = pipeline.commands.clone();
1312        let backend = {
1313            let ctx = self.exec_ctx.read().await;
1314            ctx.backend.clone()
1315        };
1316        let scope = {
1317            let scope = self.scope.read().await;
1318            scope.clone()
1319        };
1320        let cwd = {
1321            let ctx = self.exec_ctx.read().await;
1322            ctx.cwd.clone()
1323        };
1324        let tools = self.tools.clone();
1325        let tool_schemas = self.tools.schemas();
1326        let allow_ext = self.allow_external_commands;
1327
1328        // Spawn the background task
1329        tokio::spawn(async move {
1330            // Create execution context for the background job
1331            // It inherits env vars and cwd from the parent context
1332            let mut bg_ctx = ExecContext::with_backend(backend);
1333            bg_ctx.scope = scope;
1334            bg_ctx.cwd = cwd;
1335            bg_ctx.set_tools(tools.clone());
1336            bg_ctx.set_tool_schemas(tool_schemas);
1337            bg_ctx.allow_external_commands = allow_ext;
1338
1339            // Use BackendDispatcher for background jobs (builtins only).
1340            // Full Kernel dispatch requires Arc<Kernel> — planned for a future phase.
1341            let dispatcher = crate::dispatch::BackendDispatcher::new(tools);
1342
1343            // Execute the pipeline
1344            let result = runner.run(&commands, &mut bg_ctx, &dispatcher).await;
1345
1346            // Write output to streams
1347            let text = result.text_out();
1348            if !text.is_empty() {
1349                stdout.write(text.as_bytes()).await;
1350            }
1351            if !result.err.is_empty() {
1352                stderr.write(result.err.as_bytes()).await;
1353            }
1354
1355            // Close streams
1356            stdout.close().await;
1357            stderr.close().await;
1358
1359            // Send result to JobManager (ignore error if receiver dropped)
1360            let _ = tx.send(result);
1361        });
1362
1363        Ok(ExecResult::success(format!("[{}]", job_id)))
1364    }
1365
1366    /// Format a pipeline as a command string for display.
1367    fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
1368        pipeline.commands
1369            .iter()
1370            .map(|cmd| {
1371                let mut parts = vec![cmd.name.clone()];
1372                for arg in &cmd.args {
1373                    match arg {
1374                        Arg::Positional(expr) => {
1375                            parts.push(self.format_expr(expr));
1376                        }
1377                        Arg::Named { key, value } => {
1378                            parts.push(format!("{}={}", key, self.format_expr(value)));
1379                        }
1380                        Arg::ShortFlag(name) => {
1381                            parts.push(format!("-{}", name));
1382                        }
1383                        Arg::LongFlag(name) => {
1384                            parts.push(format!("--{}", name));
1385                        }
1386                        Arg::DoubleDash => {
1387                            parts.push("--".to_string());
1388                        }
1389                    }
1390                }
1391                parts.join(" ")
1392            })
1393            .collect::<Vec<_>>()
1394            .join(" | ")
1395    }
1396
1397    /// Format an expression as a string for display.
1398    fn format_expr(&self, expr: &Expr) -> String {
1399        match expr {
1400            Expr::Literal(Value::String(s)) => {
1401                if s.contains(' ') || s.contains('"') {
1402                    format!("'{}'", s.replace('\'', "\\'"))
1403                } else {
1404                    s.clone()
1405                }
1406            }
1407            Expr::Literal(Value::Int(i)) => i.to_string(),
1408            Expr::Literal(Value::Float(f)) => f.to_string(),
1409            Expr::Literal(Value::Bool(b)) => b.to_string(),
1410            Expr::Literal(Value::Null) => "null".to_string(),
1411            Expr::VarRef(path) => {
1412                let name = path.segments.iter()
1413                    .map(|seg| match seg {
1414                        crate::ast::VarSegment::Field(f) => f.clone(),
1415                    })
1416                    .collect::<Vec<_>>()
1417                    .join(".");
1418                format!("${{{}}}", name)
1419            }
1420            Expr::Interpolated(_) => "\"...\"".to_string(),
1421            _ => "...".to_string(),
1422        }
1423    }
1424
1425    /// Execute a single command.
1426    async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
1427        self.execute_command_depth(name, args, 0).await
1428    }
1429
1430    #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
1431    async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
1432        // Special built-ins
1433        match name {
1434            "true" => return Ok(ExecResult::success("")),
1435            "false" => return Ok(ExecResult::failure(1, "")),
1436            "source" | "." => return self.execute_source(args).await,
1437            _ => {}
1438        }
1439
1440        // Alias expansion (with recursion limit)
1441        if alias_depth < 10 {
1442            let alias_value = {
1443                let ctx = self.exec_ctx.read().await;
1444                ctx.aliases.get(name).cloned()
1445            };
1446            if let Some(alias_val) = alias_value {
1447                // Split alias value into command + args
1448                let parts: Vec<&str> = alias_val.split_whitespace().collect();
1449                if let Some((alias_cmd, alias_args)) = parts.split_first() {
1450                    let mut new_args: Vec<Arg> = alias_args
1451                        .iter()
1452                        .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
1453                        .collect();
1454                    new_args.extend_from_slice(args);
1455                    return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
1456                }
1457            }
1458        }
1459
1460        // Handle /v/bin/ prefix — dispatch to builtins via virtual path
1461        if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
1462            return match self.tools.get(builtin_name) {
1463                Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
1464                None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
1465            };
1466        }
1467
1468        // Check user-defined tools first
1469        {
1470            let user_tools = self.user_tools.read().await;
1471            if let Some(tool_def) = user_tools.get(name) {
1472                let tool_def = tool_def.clone();
1473                drop(user_tools);
1474                return self.execute_user_tool(tool_def, args).await;
1475            }
1476        }
1477
1478        // Look up builtin tool
1479        let tool = match self.tools.get(name) {
1480            Some(t) => t,
1481            None => {
1482                // Try executing as .kai script from PATH
1483                if let Some(result) = self.try_execute_script(name, args).await? {
1484                    return Ok(result);
1485                }
1486                // Try executing as external command from PATH
1487                if let Some(result) = self.try_execute_external(name, args).await? {
1488                    return Ok(result);
1489                }
1490
1491                // Try backend-registered tools (embedder engines, MCP tools, etc.)
1492                // Look up tool schema for positional→named mapping.
1493                // Clone backend and drop read lock before awaiting (may involve network I/O).
1494                // Backend tools expect named JSON params, so enable positional mapping.
1495                let backend = self.exec_ctx.read().await.backend.clone();
1496                let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
1497                    let mut s = t.schema;
1498                    s.map_positionals = true;
1499                    s
1500                });
1501                let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
1502                let mut ctx = self.exec_ctx.write().await;
1503                {
1504                    let scope = self.scope.read().await;
1505                    ctx.scope = scope.clone();
1506                }
1507                let backend = ctx.backend.clone();
1508                match backend.call_tool(name, tool_args, &mut ctx).await {
1509                    Ok(tool_result) => {
1510                        let mut scope = self.scope.write().await;
1511                        *scope = ctx.scope.clone();
1512                        let mut exec = ExecResult::from_output(
1513                            tool_result.code as i64, tool_result.stdout, tool_result.stderr,
1514                        );
1515                        exec.output = tool_result.output;
1516                        return Ok(exec);
1517                    }
1518                    Err(BackendError::ToolNotFound(_)) => {
1519                        // Fall through to "command not found"
1520                    }
1521                    Err(e) => {
1522                        // Backend dispatch is last-resort lookup — if it fails
1523                        // for any reason, the command simply doesn't exist.
1524                        tracing::debug!("backend error for {name}: {e}");
1525                    }
1526                }
1527
1528                return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
1529            }
1530        };
1531
1532        // Build arguments (async to support command substitution, schema-aware for flag values)
1533        let schema = tool.schema();
1534        let mut tool_args = self.build_args_async(args, Some(&schema)).await?;
1535        let output_format = extract_output_format(&mut tool_args, Some(&schema));
1536
1537        // --help / -h: show help unless the tool's schema claims that flag
1538        let schema_claims = |flag: &str| -> bool {
1539            let bare = flag.trim_start_matches('-');
1540            schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
1541        };
1542        let wants_help =
1543            (tool_args.flags.contains("help") && !schema_claims("help"))
1544            || (tool_args.flags.contains("h") && !schema_claims("-h"));
1545        if wants_help {
1546            let help_topic = crate::help::HelpTopic::Tool(name.to_string());
1547            let ctx = self.exec_ctx.read().await;
1548            let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
1549            return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
1550        }
1551
1552        // Execute
1553        let mut ctx = self.exec_ctx.write().await;
1554        {
1555            let scope = self.scope.read().await;
1556            ctx.scope = scope.clone();
1557        }
1558
1559        let result = tool.execute(tool_args, &mut ctx).await;
1560
1561        // Sync scope changes back (e.g., from cd)
1562        {
1563            let mut scope = self.scope.write().await;
1564            *scope = ctx.scope.clone();
1565        }
1566
1567        let result = match output_format {
1568            Some(format) => apply_output_format(result, format),
1569            None => result,
1570        };
1571
1572        Ok(result)
1573    }
1574
1575    /// Build tool arguments from AST args.
1576    ///
1577    /// Uses async evaluation to support command substitution in arguments.
1578    async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
1579        let mut tool_args = ToolArgs::new();
1580        let param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
1581
1582        // Track which positional indices have been consumed as flag values
1583        let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1584        let mut past_double_dash = false;
1585
1586        // Find positional arg indices for flag value consumption
1587        let positional_indices: Vec<usize> = args.iter().enumerate()
1588            .filter_map(|(i, a)| matches!(a, Arg::Positional(_)).then_some(i))
1589            .collect();
1590
1591        let mut i = 0;
1592        while i < args.len() {
1593            match &args[i] {
1594                Arg::DoubleDash => {
1595                    past_double_dash = true;
1596                }
1597                Arg::Positional(expr) => {
1598                    if !consumed.contains(&i) {
1599                        let value = self.eval_expr_async(expr).await?;
1600                        let value = apply_tilde_expansion(value);
1601                        tool_args.positional.push(value);
1602                    }
1603                }
1604                Arg::Named { key, value } => {
1605                    let val = self.eval_expr_async(value).await?;
1606                    let val = apply_tilde_expansion(val);
1607                    tool_args.named.insert(key.clone(), val);
1608                }
1609                Arg::ShortFlag(name) => {
1610                    if past_double_dash {
1611                        tool_args.positional.push(Value::String(format!("-{name}")));
1612                    } else if name.len() == 1 {
1613                        let flag_name = name.as_str();
1614                        let lookup = param_lookup.get(flag_name);
1615                        let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1616
1617                        if is_bool {
1618                            tool_args.flags.insert(flag_name.to_string());
1619                        } else {
1620                            // Non-bool: consume next positional as value
1621                            let canonical = lookup.map(|(name, _)| *name).unwrap_or(flag_name);
1622                            let next_pos = positional_indices.iter()
1623                                .find(|idx| **idx > i && !consumed.contains(idx));
1624
1625                            if let Some(&pos_idx) = next_pos {
1626                                if let Arg::Positional(expr) = &args[pos_idx] {
1627                                    let value = self.eval_expr_async(expr).await?;
1628                                    let value = apply_tilde_expansion(value);
1629                                    tool_args.named.insert(canonical.to_string(), value);
1630                                    consumed.insert(pos_idx);
1631                                }
1632                            } else {
1633                                tool_args.flags.insert(flag_name.to_string());
1634                            }
1635                        }
1636                    } else if let Some(&(canonical, typ)) = param_lookup.get(name.as_str()) {
1637                        // Multi-char short flag matches a schema param (POSIX style: -name value)
1638                        if is_bool_type(typ) {
1639                            tool_args.flags.insert(canonical.to_string());
1640                        } else {
1641                            let next_pos = positional_indices.iter()
1642                                .find(|idx| **idx > i && !consumed.contains(idx));
1643                            if let Some(&pos_idx) = next_pos {
1644                                if let Arg::Positional(expr) = &args[pos_idx] {
1645                                    let value = self.eval_expr_async(expr).await?;
1646                                    let value = apply_tilde_expansion(value);
1647                                    tool_args.named.insert(canonical.to_string(), value);
1648                                    consumed.insert(pos_idx);
1649                                }
1650                            } else {
1651                                tool_args.flags.insert(name.clone());
1652                            }
1653                        }
1654                    } else {
1655                        // Multi-char combined flags like -la: always boolean
1656                        for c in name.chars() {
1657                            tool_args.flags.insert(c.to_string());
1658                        }
1659                    }
1660                }
1661                Arg::LongFlag(name) => {
1662                    if past_double_dash {
1663                        tool_args.positional.push(Value::String(format!("--{name}")));
1664                    } else {
1665                        let lookup = param_lookup.get(name.as_str());
1666                        let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1667
1668                        if is_bool {
1669                            tool_args.flags.insert(name.clone());
1670                        } else {
1671                            let canonical = lookup.map(|(name, _)| *name).unwrap_or(name.as_str());
1672                            let next_pos = positional_indices.iter()
1673                                .find(|idx| **idx > i && !consumed.contains(idx));
1674
1675                            if let Some(&pos_idx) = next_pos {
1676                                if let Arg::Positional(expr) = &args[pos_idx] {
1677                                    let value = self.eval_expr_async(expr).await?;
1678                                    let value = apply_tilde_expansion(value);
1679                                    tool_args.named.insert(canonical.to_string(), value);
1680                                    consumed.insert(pos_idx);
1681                                }
1682                            } else {
1683                                tool_args.flags.insert(name.clone());
1684                            }
1685                        }
1686                    }
1687                }
1688            }
1689            i += 1;
1690        }
1691
1692        // Map remaining positionals to unfilled non-bool schema params (in order).
1693        // This enables `drift_push "abc" "hello"` → named["target_ctx"] = "abc", named["content"] = "hello"
1694        // Positionals that appeared after `--` are never mapped (they're raw data).
1695        // Only for MCP/external tools (map_positionals=true). Builtins handle their own positionals.
1696        if let Some(schema) = schema.filter(|s| s.map_positionals) {
1697            let pre_dash_count = if past_double_dash {
1698                let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
1699                positional_indices.iter()
1700                    .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
1701                    .count()
1702            } else {
1703                tool_args.positional.len()
1704            };
1705
1706            let mut remaining = Vec::new();
1707            let mut positional_iter = tool_args.positional.drain(..).enumerate();
1708
1709            for param in &schema.params {
1710                if tool_args.named.contains_key(&param.name) || tool_args.flags.contains(&param.name) {
1711                    continue;
1712                }
1713                if is_bool_type(&param.param_type) {
1714                    continue;
1715                }
1716                loop {
1717                    match positional_iter.next() {
1718                        Some((idx, val)) if idx < pre_dash_count => {
1719                            tool_args.named.insert(param.name.clone(), val);
1720                            break;
1721                        }
1722                        Some((_, val)) => {
1723                            remaining.push(val);
1724                        }
1725                        None => break,
1726                    }
1727                }
1728            }
1729
1730            remaining.extend(positional_iter.map(|(_, v)| v));
1731            tool_args.positional = remaining;
1732        }
1733
1734        Ok(tool_args)
1735    }
1736
1737    /// Build arguments as flat string list for external commands.
1738    ///
1739    /// Unlike `build_args_async` which separates flags into a HashSet (for schema-aware builtins),
1740    /// this preserves the original flag format as strings for external commands:
1741    /// - `-l` stays as `-l`
1742    /// - `--verbose` stays as `--verbose`
1743    /// - `key=value` stays as `key=value`
1744    ///
1745    /// This is what external commands expect in their argv.
1746    async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
1747        let mut argv = Vec::new();
1748        for arg in args {
1749            match arg {
1750                Arg::Positional(expr) => {
1751                    let value = self.eval_expr_async(expr).await?;
1752                    let value = apply_tilde_expansion(value);
1753                    argv.push(value_to_string(&value));
1754                }
1755                Arg::Named { key, value } => {
1756                    let val = self.eval_expr_async(value).await?;
1757                    let val = apply_tilde_expansion(val);
1758                    argv.push(format!("{}={}", key, value_to_string(&val)));
1759                }
1760                Arg::ShortFlag(name) => {
1761                    // Preserve original format: -l, -la (combined flags)
1762                    argv.push(format!("-{}", name));
1763                }
1764                Arg::LongFlag(name) => {
1765                    // Preserve original format: --verbose
1766                    argv.push(format!("--{}", name));
1767                }
1768                Arg::DoubleDash => {
1769                    // Preserve the -- marker
1770                    argv.push("--".to_string());
1771                }
1772            }
1773        }
1774        Ok(argv)
1775    }
1776
1777    /// Async expression evaluator that supports command substitution.
1778    ///
1779    /// This is used for contexts where expressions may contain `$(...)` command
1780    /// substitution. Unlike the sync `eval_expr`, this can execute pipelines.
1781    fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
1782        Box::pin(async move {
1783        match expr {
1784            Expr::Literal(value) => Ok(value.clone()),
1785            Expr::VarRef(path) => {
1786                let scope = self.scope.read().await;
1787                scope.resolve_path(path)
1788                    .ok_or_else(|| anyhow::anyhow!("undefined variable"))
1789            }
1790            Expr::Interpolated(parts) => {
1791                let mut result = String::new();
1792                for part in parts {
1793                    result.push_str(&self.eval_string_part_async(part).await?);
1794                }
1795                Ok(Value::String(result))
1796            }
1797            Expr::BinaryOp { left, op, right } => {
1798                match op {
1799                    BinaryOp::And => {
1800                        let left_val = self.eval_expr_async(left).await?;
1801                        if !is_truthy(&left_val) {
1802                            return Ok(left_val);
1803                        }
1804                        self.eval_expr_async(right).await
1805                    }
1806                    BinaryOp::Or => {
1807                        let left_val = self.eval_expr_async(left).await?;
1808                        if is_truthy(&left_val) {
1809                            return Ok(left_val);
1810                        }
1811                        self.eval_expr_async(right).await
1812                    }
1813                    _ => {
1814                        // Evaluate operands async (handles $(cmd)), then compare sync
1815                        let left_val = self.eval_expr_async(left).await?;
1816                        let right_val = self.eval_expr_async(right).await?;
1817                        let resolved = Expr::BinaryOp {
1818                            left: Box::new(Expr::Literal(left_val)),
1819                            op: *op,
1820                            right: Box::new(Expr::Literal(right_val)),
1821                        };
1822                        let mut scope = self.scope.write().await;
1823                        eval_expr(&resolved, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1824                    }
1825                }
1826            }
1827            Expr::CommandSubst(pipeline) => {
1828                // Snapshot scope+cwd before running — only output escapes,
1829                // not side effects like `cd` or variable assignments.
1830                let saved_scope = { self.scope.read().await.clone() };
1831                let saved_cwd = {
1832                    let ec = self.exec_ctx.read().await;
1833                    (ec.cwd.clone(), ec.prev_cwd.clone())
1834                };
1835
1836                // Capture result without `?` — restore state unconditionally
1837                let run_result = self.execute_pipeline(pipeline).await;
1838
1839                // Restore scope and cwd regardless of success/failure
1840                {
1841                    let mut scope = self.scope.write().await;
1842                    *scope = saved_scope;
1843                    if let Ok(ref r) = run_result {
1844                        scope.set_last_result(r.clone());
1845                    }
1846                }
1847                {
1848                    let mut ec = self.exec_ctx.write().await;
1849                    ec.cwd = saved_cwd.0;
1850                    ec.prev_cwd = saved_cwd.1;
1851                }
1852
1853                // Now propagate the error
1854                let result = run_result?;
1855
1856                // Prefer structured data (enables `for i in $(cmd)` iteration)
1857                if let Some(data) = &result.data {
1858                    Ok(data.clone())
1859                } else if let Some(ref output) = result.output {
1860                    // Flat non-text node lists (glob, ls, tree) → iterable array
1861                    if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
1862                        let items: Vec<serde_json::Value> = output.root.iter()
1863                            .map(|n| serde_json::Value::String(n.display_name().to_string()))
1864                            .collect();
1865                        Ok(Value::Json(serde_json::Value::Array(items)))
1866                    } else {
1867                        Ok(Value::String(result.text_out().trim_end().to_string()))
1868                    }
1869                } else {
1870                    // Otherwise return stdout as single string (NO implicit splitting)
1871                    Ok(Value::String(result.text_out().trim_end().to_string()))
1872                }
1873            }
1874            Expr::Test(test_expr) => {
1875                Ok(Value::Bool(self.eval_test_async(test_expr).await?))
1876            }
1877            Expr::Positional(n) => {
1878                let scope = self.scope.read().await;
1879                match scope.get_positional(*n) {
1880                    Some(s) => Ok(Value::String(s.to_string())),
1881                    None => Ok(Value::String(String::new())),
1882                }
1883            }
1884            Expr::AllArgs => {
1885                let scope = self.scope.read().await;
1886                Ok(Value::String(scope.all_args().join(" ")))
1887            }
1888            Expr::ArgCount => {
1889                let scope = self.scope.read().await;
1890                Ok(Value::Int(scope.arg_count() as i64))
1891            }
1892            Expr::VarLength(name) => {
1893                let scope = self.scope.read().await;
1894                match scope.get(name) {
1895                    Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
1896                    None => Ok(Value::Int(0)),
1897                }
1898            }
1899            Expr::VarWithDefault { name, default } => {
1900                let scope = self.scope.read().await;
1901                let use_default = match scope.get(name) {
1902                    Some(value) => value_to_string(value).is_empty(),
1903                    None => true,
1904                };
1905                drop(scope); // Release the lock before recursive evaluation
1906                if use_default {
1907                    // Evaluate the default parts (supports nested expansions)
1908                    self.eval_string_parts_async(default).await.map(Value::String)
1909                } else {
1910                    let scope = self.scope.read().await;
1911                    scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
1912                }
1913            }
1914            Expr::Arithmetic(expr_str) => {
1915                let scope = self.scope.read().await;
1916                crate::arithmetic::eval_arithmetic(expr_str, &scope)
1917                    .map(Value::Int)
1918                    .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
1919            }
1920            Expr::Command(cmd) => {
1921                // Execute command and return boolean based on exit code
1922                let result = self.execute_command(&cmd.name, &cmd.args).await?;
1923                Ok(Value::Bool(result.code == 0))
1924            }
1925            Expr::LastExitCode => {
1926                let scope = self.scope.read().await;
1927                Ok(Value::Int(scope.last_result().code))
1928            }
1929            Expr::CurrentPid => {
1930                let scope = self.scope.read().await;
1931                Ok(Value::Int(scope.pid() as i64))
1932            }
1933        }
1934        })
1935    }
1936
1937    /// Async helper to evaluate multiple StringParts into a single string.
1938    fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
1939        Box::pin(async move {
1940            let mut result = String::new();
1941            for part in parts {
1942                result.push_str(&self.eval_string_part_async(part).await?);
1943            }
1944            Ok(result)
1945        })
1946    }
1947
1948    /// Async helper to evaluate a StringPart.
1949    /// Evaluate a `[[ ]]` test expression asynchronously, routing file tests
1950    /// through the VFS backend instead of using raw `std::path`.
1951    fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
1952        Box::pin(async move {
1953            match test_expr {
1954                TestExpr::FileTest { op, path } => {
1955                    let path_value = self.eval_expr_async(path).await?;
1956                    let path_str = value_to_string(&path_value);
1957                    let backend = self.exec_ctx.read().await.backend.clone();
1958                    let entry = backend.stat(std::path::Path::new(&path_str)).await.ok();
1959                    Ok(match op {
1960                        FileTestOp::Exists => entry.is_some(),
1961                        FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
1962                        FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
1963                        FileTestOp::Readable => entry.is_some(),
1964                        FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
1965                            e.permissions.is_none_or(|p| p & 0o222 != 0)
1966                        }),
1967                        FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
1968                            e.permissions.is_some_and(|p| p & 0o111 != 0)
1969                        }),
1970                    })
1971                }
1972                TestExpr::StringTest { op, value } => {
1973                    let val = self.eval_expr_async(value).await?;
1974                    let s = value_to_string(&val);
1975                    Ok(match op {
1976                        crate::ast::StringTestOp::IsEmpty => s.is_empty(),
1977                        crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
1978                    })
1979                }
1980                TestExpr::Comparison { left, op, right } => {
1981                    // Evaluate operands async (handles $(cmd)), then compare sync
1982                    let left_val = self.eval_expr_async(left).await?;
1983                    let right_val = self.eval_expr_async(right).await?;
1984                    let resolved = TestExpr::Comparison {
1985                        left: Box::new(Expr::Literal(left_val)),
1986                        op: *op,
1987                        right: Box::new(Expr::Literal(right_val)),
1988                    };
1989                    let expr = Expr::Test(Box::new(resolved));
1990                    let mut scope = self.scope.write().await;
1991                    let value = eval_expr(&expr, &mut scope)
1992                        .map_err(|e| anyhow::anyhow!("{}", e))?;
1993                    Ok(value_to_bool(&value))
1994                }
1995                TestExpr::And { left, right } => {
1996                    if !self.eval_test_async(left).await? {
1997                        Ok(false)
1998                    } else {
1999                        self.eval_test_async(right).await
2000                    }
2001                }
2002                TestExpr::Or { left, right } => {
2003                    if self.eval_test_async(left).await? {
2004                        Ok(true)
2005                    } else {
2006                        self.eval_test_async(right).await
2007                    }
2008                }
2009                TestExpr::Not { expr } => {
2010                    Ok(!self.eval_test_async(expr).await?)
2011                }
2012            }
2013        })
2014    }
2015
2016    fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
2017        Box::pin(async move {
2018            match part {
2019                StringPart::Literal(s) => Ok(s.clone()),
2020                StringPart::Var(path) => {
2021                    let scope = self.scope.read().await;
2022                    match scope.resolve_path(path) {
2023                        Some(value) => Ok(value_to_string(&value)),
2024                        None => Ok(String::new()), // Unset vars expand to empty
2025                    }
2026                }
2027                StringPart::VarWithDefault { name, default } => {
2028                    let scope = self.scope.read().await;
2029                    let use_default = match scope.get(name) {
2030                        Some(value) => value_to_string(value).is_empty(),
2031                        None => true,
2032                    };
2033                    drop(scope); // Release lock before recursive evaluation
2034                    if use_default {
2035                        // Evaluate the default parts (supports nested expansions)
2036                        self.eval_string_parts_async(default).await
2037                    } else {
2038                        let scope = self.scope.read().await;
2039                        Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
2040                    }
2041                }
2042            StringPart::VarLength(name) => {
2043                let scope = self.scope.read().await;
2044                match scope.get(name) {
2045                    Some(value) => Ok(value_to_string(value).len().to_string()),
2046                    None => Ok("0".to_string()),
2047                }
2048            }
2049            StringPart::Positional(n) => {
2050                let scope = self.scope.read().await;
2051                match scope.get_positional(*n) {
2052                    Some(s) => Ok(s.to_string()),
2053                    None => Ok(String::new()),
2054                }
2055            }
2056            StringPart::AllArgs => {
2057                let scope = self.scope.read().await;
2058                Ok(scope.all_args().join(" "))
2059            }
2060            StringPart::ArgCount => {
2061                let scope = self.scope.read().await;
2062                Ok(scope.arg_count().to_string())
2063            }
2064            StringPart::Arithmetic(expr) => {
2065                let scope = self.scope.read().await;
2066                match crate::arithmetic::eval_arithmetic(expr, &scope) {
2067                    Ok(value) => Ok(value.to_string()),
2068                    Err(_) => Ok(String::new()),
2069                }
2070            }
2071            StringPart::CommandSubst(pipeline) => {
2072                // Snapshot scope+cwd — command substitution in strings must
2073                // not leak side effects (e.g., `"dir: $(cd /; pwd)"` must not change cwd).
2074                let saved_scope = { self.scope.read().await.clone() };
2075                let saved_cwd = {
2076                    let ec = self.exec_ctx.read().await;
2077                    (ec.cwd.clone(), ec.prev_cwd.clone())
2078                };
2079
2080                // Capture result without `?` — restore state unconditionally
2081                let run_result = self.execute_pipeline(pipeline).await;
2082
2083                // Restore scope and cwd regardless of success/failure
2084                {
2085                    let mut scope = self.scope.write().await;
2086                    *scope = saved_scope;
2087                    if let Ok(ref r) = run_result {
2088                        scope.set_last_result(r.clone());
2089                    }
2090                }
2091                {
2092                    let mut ec = self.exec_ctx.write().await;
2093                    ec.cwd = saved_cwd.0;
2094                    ec.prev_cwd = saved_cwd.1;
2095                }
2096
2097                // Now propagate the error
2098                let result = run_result?;
2099
2100                Ok(result.text_out().trim_end_matches('\n').to_string())
2101            }
2102            StringPart::LastExitCode => {
2103                let scope = self.scope.read().await;
2104                Ok(scope.last_result().code.to_string())
2105            }
2106            StringPart::CurrentPid => {
2107                let scope = self.scope.read().await;
2108                Ok(scope.pid().to_string())
2109            }
2110        }
2111        })
2112    }
2113
2114    /// Update the last result in scope.
2115    async fn update_last_result(&self, result: &ExecResult) {
2116        let mut scope = self.scope.write().await;
2117        scope.set_last_result(result.clone());
2118    }
2119
2120    /// Drain accumulated pipeline stderr into a result.
2121    ///
2122    /// Called after each sub-statement inside control structures (`if`, `for`,
2123    /// `while`, `case`, `&&`, `||`) so that stderr appears incrementally rather
2124    /// than batching until the entire structure finishes.
2125    async fn drain_stderr_into(&self, result: &mut ExecResult) {
2126        let drained = {
2127            let mut receiver = self.stderr_receiver.lock().await;
2128            receiver.drain_lossy()
2129        };
2130        if !drained.is_empty() {
2131            if !result.err.is_empty() && !result.err.ends_with('\n') {
2132                result.err.push('\n');
2133            }
2134            result.err.push_str(&drained);
2135        }
2136    }
2137
2138    /// Execute a user-defined function with local variable scoping.
2139    ///
2140    /// Functions push a new scope frame for local variables. Variables declared
2141    /// with `local` are scoped to the function; other assignments modify outer
2142    /// scopes (or create in root if new).
2143    async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
2144        // 1. Build function args from AST args (async to support command substitution)
2145        let tool_args = self.build_args_async(args, None).await?;
2146
2147        // 2. Push a new scope frame for local variables
2148        {
2149            let mut scope = self.scope.write().await;
2150            scope.push_frame();
2151        }
2152
2153        // 3. Save current positional parameters and set new ones for this function
2154        let saved_positional = {
2155            let mut scope = self.scope.write().await;
2156            let saved = scope.save_positional();
2157
2158            // Set up new positional parameters ($0 = function name, $1, $2, ... = args)
2159            let positional_args: Vec<String> = tool_args.positional
2160                .iter()
2161                .map(value_to_string)
2162                .collect();
2163            scope.set_positional(&def.name, positional_args);
2164
2165            saved
2166        };
2167
2168        // 3. Execute body statements with control flow handling
2169        // Accumulate output across statements (like sh)
2170        let mut accumulated_out = String::new();
2171        let mut accumulated_err = String::new();
2172        let mut last_code = 0i64;
2173        let mut last_data: Option<Value> = None;
2174
2175        // Track execution error for propagation after cleanup
2176        let mut exec_error: Option<anyhow::Error> = None;
2177        let mut exit_code: Option<i64> = None;
2178
2179        for stmt in &def.body {
2180            match self.execute_stmt_flow(stmt).await {
2181                Ok(flow) => {
2182                    // Drain pipeline stderr after each sub-statement.
2183                    let drained = {
2184                        let mut receiver = self.stderr_receiver.lock().await;
2185                        receiver.drain_lossy()
2186                    };
2187                    if !drained.is_empty() {
2188                        accumulated_err.push_str(&drained);
2189                    }
2190
2191                    match flow {
2192                        ControlFlow::Normal(r) => {
2193                            accumulated_out.push_str(&r.out);
2194                            accumulated_err.push_str(&r.err);
2195                            last_code = r.code;
2196                            last_data = r.data;
2197                        }
2198                        ControlFlow::Return { value } => {
2199                            accumulated_out.push_str(&value.out);
2200                            accumulated_err.push_str(&value.err);
2201                            last_code = value.code;
2202                            last_data = value.data;
2203                            break;
2204                        }
2205                        ControlFlow::Exit { code } => {
2206                            exit_code = Some(code);
2207                            break;
2208                        }
2209                        ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2210                            accumulated_out.push_str(&r.out);
2211                            accumulated_err.push_str(&r.err);
2212                            last_code = r.code;
2213                            last_data = r.data;
2214                        }
2215                    }
2216                }
2217                Err(e) => {
2218                    exec_error = Some(e);
2219                    break;
2220                }
2221            }
2222        }
2223
2224        // 4. Pop scope frame and restore original positional parameters (unconditionally)
2225        {
2226            let mut scope = self.scope.write().await;
2227            scope.pop_frame();
2228            scope.set_positional(saved_positional.0, saved_positional.1);
2229        }
2230
2231        // 5. Propagate error or exit after cleanup
2232        if let Some(e) = exec_error {
2233            return Err(e);
2234        }
2235        if let Some(code) = exit_code {
2236            return Ok(ExecResult {
2237                code,
2238                out: accumulated_out,
2239                err: accumulated_err,
2240                data: last_data,
2241                output: None,
2242                did_spill: false,
2243                original_code: None,
2244            });
2245        }
2246
2247        Ok(ExecResult {
2248            code: last_code,
2249            out: accumulated_out,
2250            err: accumulated_err,
2251            data: last_data,
2252            output: None,
2253            did_spill: false,
2254            original_code: None,
2255        })
2256    }
2257
2258    /// Execute the `source` / `.` command to include and run a script.
2259    ///
2260    /// Unlike regular tool execution, `source` executes in the CURRENT scope,
2261    /// allowing the sourced script to set variables and modify shell state.
2262    async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
2263        // Get the file path from the first positional argument
2264        let tool_args = self.build_args_async(args, None).await?;
2265        let path = match tool_args.positional.first() {
2266            Some(Value::String(s)) => s.clone(),
2267            Some(v) => value_to_string(v),
2268            None => {
2269                return Ok(ExecResult::failure(1, "source: missing filename"));
2270            }
2271        };
2272
2273        // Resolve path relative to cwd
2274        let full_path = {
2275            let ctx = self.exec_ctx.read().await;
2276            if path.starts_with('/') {
2277                std::path::PathBuf::from(&path)
2278            } else {
2279                ctx.cwd.join(&path)
2280            }
2281        };
2282
2283        // Read file content via backend
2284        let content = {
2285            let ctx = self.exec_ctx.read().await;
2286            match ctx.backend.read(&full_path, None).await {
2287                Ok(bytes) => {
2288                    String::from_utf8(bytes).map_err(|e| {
2289                        anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
2290                    })?
2291                }
2292                Err(e) => {
2293                    return Ok(ExecResult::failure(
2294                        1,
2295                        format!("source: {}: {}", path, e),
2296                    ));
2297                }
2298            }
2299        };
2300
2301        // Parse the content
2302        let program = match crate::parser::parse(&content) {
2303            Ok(p) => p,
2304            Err(errors) => {
2305                let msg = errors
2306                    .iter()
2307                    .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
2308                    .collect::<Vec<_>>()
2309                    .join("\n");
2310                return Ok(ExecResult::failure(1, format!("source: {}", msg)));
2311            }
2312        };
2313
2314        // Execute each statement in the CURRENT scope (not isolated)
2315        let mut result = ExecResult::success("");
2316        for stmt in program.statements {
2317            if matches!(stmt, crate::ast::Stmt::Empty) {
2318                continue;
2319            }
2320
2321            match self.execute_stmt_flow(&stmt).await {
2322                Ok(flow) => {
2323                    self.drain_stderr_into(&mut result).await;
2324                    match flow {
2325                        ControlFlow::Normal(r) => {
2326                            result = r.clone();
2327                            self.update_last_result(&r).await;
2328                        }
2329                        ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
2330                            return Err(anyhow::anyhow!(
2331                                "source: {}: unexpected break/continue outside loop",
2332                                path
2333                            ));
2334                        }
2335                        ControlFlow::Return { value } => {
2336                            return Ok(value);
2337                        }
2338                        ControlFlow::Exit { code } => {
2339                            result.code = code;
2340                            return Ok(result);
2341                        }
2342                    }
2343                }
2344                Err(e) => {
2345                    return Err(e.context(format!("source: {}", path)));
2346                }
2347            }
2348        }
2349
2350        Ok(result)
2351    }
2352
2353    /// Try to execute a script from PATH directories.
2354    ///
2355    /// Searches PATH for `{name}.kai` files and executes them in isolated scope
2356    /// (like user-defined tools). Returns None if no script is found.
2357    async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2358        // Get PATH from scope (default to "/bin")
2359        let path_value = {
2360            let scope = self.scope.read().await;
2361            scope
2362                .get("PATH")
2363                .map(value_to_string)
2364                .unwrap_or_else(|| "/bin".to_string())
2365        };
2366
2367        // Search PATH directories for script
2368        for dir in path_value.split(':') {
2369            if dir.is_empty() {
2370                continue;
2371            }
2372
2373            // Build script path: {dir}/{name}.kai
2374            let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
2375
2376            // Check if script exists
2377            let exists = {
2378                let ctx = self.exec_ctx.read().await;
2379                ctx.backend.exists(&script_path).await
2380            };
2381
2382            if !exists {
2383                continue;
2384            }
2385
2386            // Read script content
2387            let content = {
2388                let ctx = self.exec_ctx.read().await;
2389                match ctx.backend.read(&script_path, None).await {
2390                    Ok(bytes) => match String::from_utf8(bytes) {
2391                        Ok(s) => s,
2392                        Err(e) => {
2393                            return Ok(Some(ExecResult::failure(
2394                                1,
2395                                format!("{}: invalid UTF-8: {}", script_path.display(), e),
2396                            )));
2397                        }
2398                    },
2399                    Err(e) => {
2400                        return Ok(Some(ExecResult::failure(
2401                            1,
2402                            format!("{}: {}", script_path.display(), e),
2403                        )));
2404                    }
2405                }
2406            };
2407
2408            // Parse the script
2409            let program = match crate::parser::parse(&content) {
2410                Ok(p) => p,
2411                Err(errors) => {
2412                    let msg = errors
2413                        .iter()
2414                        .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
2415                        .collect::<Vec<_>>()
2416                        .join("\n");
2417                    return Ok(Some(ExecResult::failure(1, msg)));
2418                }
2419            };
2420
2421            // Build tool_args from args (async for command substitution support)
2422            let tool_args = self.build_args_async(args, None).await?;
2423
2424            // Create isolated scope (like user tools)
2425            let mut isolated_scope = Scope::new();
2426
2427            // Set up positional parameters ($0 = script name, $1, $2, ... = args)
2428            let positional_args: Vec<String> = tool_args.positional
2429                .iter()
2430                .map(value_to_string)
2431                .collect();
2432            isolated_scope.set_positional(name, positional_args);
2433
2434            // Save current scope and swap with isolated scope
2435            let original_scope = {
2436                let mut scope = self.scope.write().await;
2437                std::mem::replace(&mut *scope, isolated_scope)
2438            };
2439
2440            // Execute script statements — track outcome for cleanup
2441            let mut result = ExecResult::success("");
2442            let mut exec_error: Option<anyhow::Error> = None;
2443            let mut exit_code: Option<i64> = None;
2444
2445            for stmt in program.statements {
2446                if matches!(stmt, crate::ast::Stmt::Empty) {
2447                    continue;
2448                }
2449
2450                match self.execute_stmt_flow(&stmt).await {
2451                    Ok(flow) => {
2452                        match flow {
2453                            ControlFlow::Normal(r) => result = r,
2454                            ControlFlow::Return { value } => {
2455                                result = value;
2456                                break;
2457                            }
2458                            ControlFlow::Exit { code } => {
2459                                exit_code = Some(code);
2460                                break;
2461                            }
2462                            ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2463                                result = r;
2464                            }
2465                        }
2466                    }
2467                    Err(e) => {
2468                        exec_error = Some(e);
2469                        break;
2470                    }
2471                }
2472            }
2473
2474            // Restore original scope unconditionally
2475            {
2476                let mut scope = self.scope.write().await;
2477                *scope = original_scope;
2478            }
2479
2480            // Propagate error or exit after cleanup
2481            if let Some(e) = exec_error {
2482                return Err(e.context(format!("script: {}", script_path.display())));
2483            }
2484            if let Some(code) = exit_code {
2485                result.code = code;
2486                return Ok(Some(result));
2487            }
2488
2489            return Ok(Some(result));
2490        }
2491
2492        // No script found
2493        Ok(None)
2494    }
2495
2496    /// Try to execute an external command from PATH.
2497    ///
2498    /// This is the fallback when no builtin or user-defined tool matches.
2499    /// External commands receive a clean argv (flags preserved in their original format).
2500    ///
2501    /// # Requirements
2502    /// - Command must be found in PATH
2503    /// - Current working directory must be on a real filesystem (not virtual like /v)
2504    ///
2505    /// # Returns
2506    /// - `Ok(Some(result))` if command was found and executed
2507    /// - `Ok(None)` if command was not found in PATH
2508    /// - `Err` on execution errors
2509    #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
2510    async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2511        if !self.allow_external_commands {
2512            return Ok(None);
2513        }
2514
2515        // Get real working directory for relative path resolution and child cwd.
2516        // If the CWD is virtual (no real filesystem path), skip external command
2517        // execution entirely — return None so the dispatch can fall through to
2518        // backend-registered tools.
2519        let real_cwd = {
2520            let ctx = self.exec_ctx.read().await;
2521            match ctx.backend.resolve_real_path(&ctx.cwd) {
2522                Some(p) => p,
2523                None => return Ok(None),
2524            }
2525        };
2526
2527        let executable = if name.contains('/') {
2528            // Resolve relative paths (./script, ../bin/tool) against the shell's cwd
2529            let resolved = if std::path::Path::new(name).is_absolute() {
2530                std::path::PathBuf::from(name)
2531            } else {
2532                real_cwd.join(name)
2533            };
2534            if !resolved.exists() {
2535                return Ok(Some(ExecResult::failure(
2536                    127,
2537                    format!("{}: No such file or directory", name),
2538                )));
2539            }
2540            if !resolved.is_file() {
2541                return Ok(Some(ExecResult::failure(
2542                    126,
2543                    format!("{}: Is a directory", name),
2544                )));
2545            }
2546            #[cfg(unix)]
2547            {
2548                use std::os::unix::fs::PermissionsExt;
2549                let mode = std::fs::metadata(&resolved)
2550                    .map(|m| m.permissions().mode())
2551                    .unwrap_or(0);
2552                if mode & 0o111 == 0 {
2553                    return Ok(Some(ExecResult::failure(
2554                        126,
2555                        format!("{}: Permission denied", name),
2556                    )));
2557                }
2558            }
2559            resolved.to_string_lossy().into_owned()
2560        } else {
2561            // Get PATH from scope or environment
2562            let path_var = {
2563                let scope = self.scope.read().await;
2564                scope
2565                    .get("PATH")
2566                    .map(value_to_string)
2567                    .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
2568            };
2569
2570            // Resolve command in PATH
2571            match resolve_in_path(name, &path_var) {
2572                Some(path) => path,
2573                None => return Ok(None), // Not found - let caller handle error
2574            }
2575        };
2576
2577        tracing::debug!(executable = %executable, "resolved external command");
2578
2579        // Build flat argv (preserves flag format)
2580        let argv = self.build_args_flat(args).await?;
2581
2582        // Get stdin if available
2583        let stdin_data = {
2584            let mut ctx = self.exec_ctx.write().await;
2585            ctx.take_stdin()
2586        };
2587
2588        // Build and spawn the command
2589        use tokio::process::Command;
2590
2591        let mut cmd = Command::new(&executable);
2592        cmd.args(&argv);
2593        cmd.current_dir(&real_cwd);
2594
2595        // Handle stdin
2596        cmd.stdin(if stdin_data.is_some() {
2597            std::process::Stdio::piped()
2598        } else if self.interactive {
2599            std::process::Stdio::inherit()
2600        } else {
2601            std::process::Stdio::null()
2602        });
2603
2604        // In interactive mode, standalone or last-in-pipeline commands inherit
2605        // the terminal's stdout/stderr so output streams in real-time.
2606        // First/middle commands must capture stdout for the pipe — same as bash.
2607        let pipeline_position = {
2608            let ctx = self.exec_ctx.read().await;
2609            ctx.pipeline_position
2610        };
2611        let inherit_output = self.interactive
2612            && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
2613
2614        if inherit_output {
2615            cmd.stdout(std::process::Stdio::inherit());
2616            cmd.stderr(std::process::Stdio::inherit());
2617        } else {
2618            cmd.stdout(std::process::Stdio::piped());
2619            cmd.stderr(std::process::Stdio::piped());
2620        }
2621
2622        // On Unix with job control, put child in its own process group
2623        // and restore default signal handlers (shell ignores SIGTSTP etc.
2624        // but children should respond to them normally).
2625        #[cfg(unix)]
2626        if self.terminal_state.is_some() && inherit_output {
2627            // SAFETY: setpgid and sigaction(SIG_DFL) are async-signal-safe per POSIX
2628            #[allow(unsafe_code)]
2629            unsafe {
2630                cmd.pre_exec(|| {
2631                    // Own process group
2632                    nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
2633                        .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
2634                    // Restore default signal handlers for job control signals
2635                    use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
2636                    let mut sa: nix::libc::sigaction = std::mem::zeroed();
2637                    sa.sa_sigaction = SIG_DFL;
2638                    sigaction(SIGTSTP, &sa, std::ptr::null_mut());
2639                    sigaction(SIGTTOU, &sa, std::ptr::null_mut());
2640                    sigaction(SIGTTIN, &sa, std::ptr::null_mut());
2641                    sigaction(SIGINT, &sa, std::ptr::null_mut());
2642                    Ok(())
2643                });
2644            }
2645        }
2646
2647        // Spawn the process
2648        let mut child = match cmd.spawn() {
2649            Ok(child) => child,
2650            Err(e) => {
2651                return Ok(Some(ExecResult::failure(
2652                    127,
2653                    format!("{}: {}", name, e),
2654                )));
2655            }
2656        };
2657
2658        // Write stdin if present
2659        if let Some(data) = stdin_data
2660            && let Some(mut stdin) = child.stdin.take()
2661        {
2662            use tokio::io::AsyncWriteExt;
2663            if let Err(e) = stdin.write_all(data.as_bytes()).await {
2664                return Ok(Some(ExecResult::failure(
2665                    1,
2666                    format!("{}: failed to write stdin: {}", name, e),
2667                )));
2668            }
2669            // Drop stdin to signal EOF
2670        }
2671
2672        if inherit_output {
2673            // Job control path: use waitpid with WUNTRACED for Ctrl-Z support
2674            #[cfg(unix)]
2675            if let Some(ref term) = self.terminal_state {
2676                let child_id = child.id().unwrap_or(0);
2677                let pid = nix::unistd::Pid::from_raw(child_id as i32);
2678                let pgid = pid; // child is its own pgid leader
2679
2680                // Give the terminal to the child's process group
2681                if let Err(e) = term.give_terminal_to(pgid) {
2682                    tracing::warn!("failed to give terminal to child: {}", e);
2683                }
2684
2685                let term_clone = term.clone();
2686                let cmd_name = name.to_string();
2687                let cmd_display = format!("{} {}", name, argv.join(" "));
2688                let jobs = self.jobs.clone();
2689
2690                let code = tokio::task::block_in_place(move || {
2691                    let result = term_clone.wait_for_foreground(pid);
2692
2693                    // Always reclaim the terminal
2694                    if let Err(e) = term_clone.reclaim_terminal() {
2695                        tracing::warn!("failed to reclaim terminal: {}", e);
2696                    }
2697
2698                    match result {
2699                        crate::terminal::WaitResult::Exited(code) => code as i64,
2700                        crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
2701                        crate::terminal::WaitResult::Stopped(_sig) => {
2702                            // Register as a stopped job
2703                            let rt = tokio::runtime::Handle::current();
2704                            let job_id = rt.block_on(jobs.register_stopped(
2705                                cmd_display,
2706                                child_id,
2707                                child_id, // pgid = pid for group leader
2708                            ));
2709                            eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
2710                            148 // 128 + SIGTSTP(20) on most systems, but we use a fixed value
2711                        }
2712                    }
2713                });
2714
2715                return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
2716            }
2717
2718            // Non-job-control path: simple wait
2719            let status = match child.wait().await {
2720                Ok(s) => s,
2721                Err(e) => {
2722                    return Ok(Some(ExecResult::failure(
2723                        1,
2724                        format!("{}: failed to wait: {}", name, e),
2725                    )));
2726                }
2727            };
2728
2729            let code = status.code().unwrap_or_else(|| {
2730                #[cfg(unix)]
2731                {
2732                    use std::os::unix::process::ExitStatusExt;
2733                    128 + status.signal().unwrap_or(0)
2734                }
2735                #[cfg(not(unix))]
2736                {
2737                    -1
2738                }
2739            }) as i64;
2740
2741            // stdout/stderr already went to the terminal
2742            Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
2743        } else {
2744            // Capture output via bounded streams
2745            let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2746            let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2747
2748            let stdout_pipe = child.stdout.take();
2749            let stderr_pipe = child.stderr.take();
2750
2751            let stdout_clone = stdout_stream.clone();
2752            let stderr_clone = stderr_stream.clone();
2753
2754            let stdout_task = stdout_pipe.map(|pipe| {
2755                tokio::spawn(async move {
2756                    drain_to_stream(pipe, stdout_clone).await;
2757                })
2758            });
2759
2760            let stderr_task = stderr_pipe.map(|pipe| {
2761                tokio::spawn(async move {
2762                    drain_to_stream(pipe, stderr_clone).await;
2763                })
2764            });
2765
2766            let status = match child.wait().await {
2767                Ok(s) => s,
2768                Err(e) => {
2769                    return Ok(Some(ExecResult::failure(
2770                        1,
2771                        format!("{}: failed to wait: {}", name, e),
2772                    )));
2773                }
2774            };
2775
2776            if let Some(task) = stdout_task {
2777                // Ignore join error — the drain task logs its own errors
2778                let _ = task.await;
2779            }
2780            if let Some(task) = stderr_task {
2781                let _ = task.await;
2782            }
2783
2784            let code = status.code().unwrap_or_else(|| {
2785                #[cfg(unix)]
2786                {
2787                    use std::os::unix::process::ExitStatusExt;
2788                    128 + status.signal().unwrap_or(0)
2789                }
2790                #[cfg(not(unix))]
2791                {
2792                    -1
2793                }
2794            }) as i64;
2795
2796            let stdout = stdout_stream.read_string().await;
2797            let stderr = stderr_stream.read_string().await;
2798
2799            Ok(Some(ExecResult::from_output(code, stdout, stderr)))
2800        }
2801    }
2802
2803    // --- Variable Access ---
2804
2805    /// Get a variable value.
2806    pub async fn get_var(&self, name: &str) -> Option<Value> {
2807        let scope = self.scope.read().await;
2808        scope.get(name).cloned()
2809    }
2810
2811    /// Check if error-exit mode is enabled (for testing).
2812    #[cfg(test)]
2813    pub async fn error_exit_enabled(&self) -> bool {
2814        let scope = self.scope.read().await;
2815        scope.error_exit_enabled()
2816    }
2817
2818    /// Set a variable value.
2819    pub async fn set_var(&self, name: &str, value: Value) {
2820        let mut scope = self.scope.write().await;
2821        scope.set(name.to_string(), value);
2822    }
2823
2824    /// Set positional parameters ($0 script name and $1-$9 args).
2825    pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
2826        let mut scope = self.scope.write().await;
2827        scope.set_positional(script_name, args);
2828    }
2829
2830    /// List all variables.
2831    pub async fn list_vars(&self) -> Vec<(String, Value)> {
2832        let scope = self.scope.read().await;
2833        scope.all()
2834    }
2835
2836    // --- CWD ---
2837
2838    /// Get current working directory.
2839    pub async fn cwd(&self) -> PathBuf {
2840        self.exec_ctx.read().await.cwd.clone()
2841    }
2842
2843    /// Set current working directory.
2844    pub async fn set_cwd(&self, path: PathBuf) {
2845        let mut ctx = self.exec_ctx.write().await;
2846        ctx.set_cwd(path);
2847    }
2848
2849    // --- Last Result ---
2850
2851    /// Get the last result ($?).
2852    pub async fn last_result(&self) -> ExecResult {
2853        let scope = self.scope.read().await;
2854        scope.last_result().clone()
2855    }
2856
2857    // --- Tools ---
2858
2859    /// Check if a user-defined function exists.
2860    pub async fn has_function(&self, name: &str) -> bool {
2861        self.user_tools.read().await.contains_key(name)
2862    }
2863
2864    /// Get available tool schemas.
2865    pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
2866        self.tools.schemas()
2867    }
2868
2869    // --- Jobs ---
2870
2871    /// Get job manager.
2872    pub fn jobs(&self) -> Arc<JobManager> {
2873        self.jobs.clone()
2874    }
2875
2876    // --- VFS ---
2877
2878    /// Get VFS router.
2879    pub fn vfs(&self) -> Arc<VfsRouter> {
2880        self.vfs.clone()
2881    }
2882
2883    // --- State ---
2884
2885    /// Reset kernel to initial state.
2886    ///
2887    /// Clears in-memory variables and resets cwd to root.
2888    /// History is not cleared (it persists across resets).
2889    pub async fn reset(&self) -> Result<()> {
2890        {
2891            let mut scope = self.scope.write().await;
2892            *scope = Scope::new();
2893        }
2894        {
2895            let mut ctx = self.exec_ctx.write().await;
2896            ctx.cwd = PathBuf::from("/");
2897        }
2898        Ok(())
2899    }
2900
2901    /// Shutdown the kernel.
2902    pub async fn shutdown(self) -> Result<()> {
2903        // Wait for all background jobs
2904        self.jobs.wait_all().await;
2905        Ok(())
2906    }
2907
2908    /// Dispatch a single command using the full resolution chain.
2909    ///
2910    /// This is the core of `CommandDispatcher` — it syncs state between the
2911    /// passed-in `ExecContext` and kernel-internal state (scope, exec_ctx),
2912    /// then delegates to `execute_command` for the actual dispatch.
2913    ///
2914    /// State flow:
2915    /// 1. ctx → self: sync scope, cwd, stdin so internal methods see current state
2916    /// 2. execute_command: full dispatch chain (user tools, builtins, scripts, external, backend)
2917    /// 3. self → ctx: sync scope, cwd changes back so the pipeline runner sees them
2918    async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
2919        // 1. Sync ctx → self internals
2920        {
2921            let mut scope = self.scope.write().await;
2922            *scope = ctx.scope.clone();
2923        }
2924        {
2925            let mut ec = self.exec_ctx.write().await;
2926            ec.cwd = ctx.cwd.clone();
2927            ec.prev_cwd = ctx.prev_cwd.clone();
2928            ec.stdin = ctx.stdin.take();
2929            ec.stdin_data = ctx.stdin_data.take();
2930            ec.aliases = ctx.aliases.clone();
2931            ec.ignore_config = ctx.ignore_config.clone();
2932            ec.output_limit = ctx.output_limit.clone();
2933            ec.pipeline_position = ctx.pipeline_position;
2934        }
2935
2936        // 2. Execute via the full dispatch chain
2937        let result = self.execute_command(&cmd.name, &cmd.args).await?;
2938
2939        // 3. Sync self → ctx
2940        {
2941            let scope = self.scope.read().await;
2942            ctx.scope = scope.clone();
2943        }
2944        {
2945            let ec = self.exec_ctx.read().await;
2946            ctx.cwd = ec.cwd.clone();
2947            ctx.prev_cwd = ec.prev_cwd.clone();
2948            ctx.aliases = ec.aliases.clone();
2949            ctx.ignore_config = ec.ignore_config.clone();
2950            ctx.output_limit = ec.output_limit.clone();
2951        }
2952
2953        Ok(result)
2954    }
2955}
2956
2957#[async_trait]
2958impl CommandDispatcher for Kernel {
2959    /// Dispatch a command through the Kernel's full resolution chain.
2960    ///
2961    /// This is the single path for all command execution when called from
2962    /// the pipeline runner. It provides the full dispatch chain:
2963    /// user tools → builtins → .kai scripts → external commands → backend tools.
2964    async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
2965        self.dispatch_command(cmd, ctx).await
2966    }
2967}
2968
2969/// Accumulate output from one result into another.
2970///
2971/// This appends stdout and stderr (with newlines as separators) and updates
2972/// the exit code to match the new result. Used to preserve output from
2973/// multiple statements, loop iterations, and command chains.
2974fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
2975    // Materialize lazy OutputData into .out before accumulating.
2976    // Without this, the first command's output stays in .output while
2977    // the second's text gets appended to .out, losing the first.
2978    if accumulated.out.is_empty() {
2979        if let Some(ref output) = accumulated.output {
2980            accumulated.out = output.to_canonical_string();
2981            accumulated.output = None;
2982        }
2983    }
2984    let new_text = new.text_out();
2985    if !accumulated.out.is_empty() && !new_text.is_empty() && !accumulated.out.ends_with('\n') {
2986        accumulated.out.push('\n');
2987    }
2988    accumulated.out.push_str(&new_text);
2989    if !accumulated.err.is_empty() && !new.err.is_empty() && !accumulated.err.ends_with('\n') {
2990        accumulated.err.push('\n');
2991    }
2992    accumulated.err.push_str(&new.err);
2993    accumulated.code = new.code;
2994    accumulated.data = new.data.clone();
2995}
2996
2997/// Check if a value is truthy.
2998fn is_truthy(value: &Value) -> bool {
2999    match value {
3000        Value::Null => false,
3001        Value::Bool(b) => *b,
3002        Value::Int(i) => *i != 0,
3003        Value::Float(f) => *f != 0.0,
3004        Value::String(s) => !s.is_empty(),
3005        Value::Json(json) => match json {
3006            serde_json::Value::Null => false,
3007            serde_json::Value::Array(arr) => !arr.is_empty(),
3008            serde_json::Value::Object(obj) => !obj.is_empty(),
3009            serde_json::Value::Bool(b) => *b,
3010            serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
3011            serde_json::Value::String(s) => !s.is_empty(),
3012        },
3013        Value::Blob(_) => true, // Blob references are always truthy
3014    }
3015}
3016
3017/// Apply tilde expansion to a value.
3018///
3019/// Only string values starting with `~` are expanded.
3020fn apply_tilde_expansion(value: Value) -> Value {
3021    match value {
3022        Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s)),
3023        _ => value,
3024    }
3025}
3026
3027#[cfg(test)]
3028mod tests {
3029    use super::*;
3030
3031    #[tokio::test]
3032    async fn test_kernel_transient() {
3033        let kernel = Kernel::transient().expect("failed to create kernel");
3034        assert_eq!(kernel.name(), "transient");
3035    }
3036
3037    #[tokio::test]
3038    async fn test_kernel_execute_echo() {
3039        let kernel = Kernel::transient().expect("failed to create kernel");
3040        let result = kernel.execute("echo hello").await.expect("execution failed");
3041        assert!(result.ok());
3042        assert_eq!(result.out.trim(), "hello");
3043    }
3044
3045    #[tokio::test]
3046    async fn test_multiple_statements_accumulate_output() {
3047        let kernel = Kernel::transient().expect("failed to create kernel");
3048        let result = kernel
3049            .execute("echo one\necho two\necho three")
3050            .await
3051            .expect("execution failed");
3052        assert!(result.ok());
3053        // Should have all three outputs separated by newlines
3054        assert!(result.out.contains("one"), "missing 'one': {}", result.out);
3055        assert!(result.out.contains("two"), "missing 'two': {}", result.out);
3056        assert!(result.out.contains("three"), "missing 'three': {}", result.out);
3057    }
3058
3059    #[tokio::test]
3060    async fn test_and_chain_accumulates_output() {
3061        let kernel = Kernel::transient().expect("failed to create kernel");
3062        let result = kernel
3063            .execute("echo first && echo second")
3064            .await
3065            .expect("execution failed");
3066        assert!(result.ok());
3067        assert!(result.out.contains("first"), "missing 'first': {}", result.out);
3068        assert!(result.out.contains("second"), "missing 'second': {}", result.out);
3069    }
3070
3071    #[tokio::test]
3072    async fn test_for_loop_accumulates_output() {
3073        let kernel = Kernel::transient().expect("failed to create kernel");
3074        let result = kernel
3075            .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
3076            .await
3077            .expect("execution failed");
3078        assert!(result.ok());
3079        assert!(result.out.contains("item: a"), "missing 'item: a': {}", result.out);
3080        assert!(result.out.contains("item: b"), "missing 'item: b': {}", result.out);
3081        assert!(result.out.contains("item: c"), "missing 'item: c': {}", result.out);
3082    }
3083
3084    #[tokio::test]
3085    async fn test_while_loop_accumulates_output() {
3086        let kernel = Kernel::transient().expect("failed to create kernel");
3087        let result = kernel
3088            .execute(r#"
3089                N=3
3090                while [[ ${N} -gt 0 ]]; do
3091                    echo "N=${N}"
3092                    N=$((N - 1))
3093                done
3094            "#)
3095            .await
3096            .expect("execution failed");
3097        assert!(result.ok());
3098        assert!(result.out.contains("N=3"), "missing 'N=3': {}", result.out);
3099        assert!(result.out.contains("N=2"), "missing 'N=2': {}", result.out);
3100        assert!(result.out.contains("N=1"), "missing 'N=1': {}", result.out);
3101    }
3102
3103    #[tokio::test]
3104    async fn test_kernel_set_var() {
3105        let kernel = Kernel::transient().expect("failed to create kernel");
3106
3107        kernel.execute("X=42").await.expect("set failed");
3108
3109        let value = kernel.get_var("X").await;
3110        assert_eq!(value, Some(Value::Int(42)));
3111    }
3112
3113    #[tokio::test]
3114    async fn test_kernel_var_expansion() {
3115        let kernel = Kernel::transient().expect("failed to create kernel");
3116
3117        kernel.execute("NAME=\"world\"").await.expect("set failed");
3118        let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
3119
3120        assert!(result.ok());
3121        assert_eq!(result.out.trim(), "hello world");
3122    }
3123
3124    #[tokio::test]
3125    async fn test_kernel_last_result() {
3126        let kernel = Kernel::transient().expect("failed to create kernel");
3127
3128        kernel.execute("echo test").await.expect("echo failed");
3129
3130        let last = kernel.last_result().await;
3131        assert!(last.ok());
3132        assert_eq!(last.out.trim(), "test");
3133    }
3134
3135    #[tokio::test]
3136    async fn test_kernel_tool_not_found() {
3137        let kernel = Kernel::transient().expect("failed to create kernel");
3138
3139        let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
3140        assert!(!result.ok());
3141        assert_eq!(result.code, 127);
3142        assert!(result.err.contains("command not found"));
3143    }
3144
3145    #[tokio::test]
3146    async fn test_external_command_true() {
3147        // Use REPL config for passthrough filesystem access
3148        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
3149
3150        // /bin/true should be available on any Unix system
3151        let result = kernel.execute("true").await.expect("execution failed");
3152        // This should use the builtin true, which returns 0
3153        assert!(result.ok(), "true should succeed: {:?}", result);
3154    }
3155
3156    #[tokio::test]
3157    async fn test_external_command_basic() {
3158        // Use REPL config for passthrough filesystem access
3159        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
3160
3161        // Test with /bin/echo which is external
3162        // Note: kaish has a builtin echo, so this will use the builtin
3163        // Let's test with a command that's not a builtin
3164        // Actually, let's just test that PATH resolution works by checking the PATH var
3165        let path_var = std::env::var("PATH").unwrap_or_default();
3166        eprintln!("System PATH: {}", path_var);
3167
3168        // Set PATH in kernel to ensure it's available
3169        kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
3170
3171        // Now try an external command like /usr/bin/env
3172        // But env is also a builtin... let's try uname
3173        let result = kernel.execute("uname").await.expect("execution failed");
3174        eprintln!("uname result: {:?}", result);
3175        // uname should succeed if external commands work
3176        assert!(result.ok() || result.code == 127, "uname: {:?}", result);
3177    }
3178
3179    #[tokio::test]
3180    async fn test_kernel_reset() {
3181        let kernel = Kernel::transient().expect("failed to create kernel");
3182
3183        kernel.execute("X=1").await.expect("set failed");
3184        assert!(kernel.get_var("X").await.is_some());
3185
3186        kernel.reset().await.expect("reset failed");
3187        assert!(kernel.get_var("X").await.is_none());
3188    }
3189
3190    #[tokio::test]
3191    async fn test_kernel_cwd() {
3192        let kernel = Kernel::transient().expect("failed to create kernel");
3193
3194        // Transient kernel uses sandboxed mode with cwd=$HOME
3195        let cwd = kernel.cwd().await;
3196        let home = std::env::var("HOME")
3197            .map(PathBuf::from)
3198            .unwrap_or_else(|_| PathBuf::from("/"));
3199        assert_eq!(cwd, home);
3200
3201        kernel.set_cwd(PathBuf::from("/tmp")).await;
3202        assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
3203    }
3204
3205    #[tokio::test]
3206    async fn test_kernel_list_vars() {
3207        let kernel = Kernel::transient().expect("failed to create kernel");
3208
3209        kernel.execute("A=1").await.ok();
3210        kernel.execute("B=2").await.ok();
3211
3212        let vars = kernel.list_vars().await;
3213        assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
3214        assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
3215    }
3216
3217    #[tokio::test]
3218    async fn test_is_truthy() {
3219        assert!(!is_truthy(&Value::Null));
3220        assert!(!is_truthy(&Value::Bool(false)));
3221        assert!(is_truthy(&Value::Bool(true)));
3222        assert!(!is_truthy(&Value::Int(0)));
3223        assert!(is_truthy(&Value::Int(1)));
3224        assert!(!is_truthy(&Value::String("".into())));
3225        assert!(is_truthy(&Value::String("x".into())));
3226    }
3227
3228    #[tokio::test]
3229    async fn test_jq_in_pipeline() {
3230        let kernel = Kernel::transient().expect("failed to create kernel");
3231        // kaish uses double quotes only; escape inner quotes
3232        let result = kernel
3233            .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
3234            .await
3235            .expect("execution failed");
3236        assert!(result.ok(), "jq pipeline failed: {}", result.err);
3237        assert_eq!(result.out.trim(), "Alice");
3238    }
3239
3240    #[tokio::test]
3241    async fn test_user_defined_tool() {
3242        let kernel = Kernel::transient().expect("failed to create kernel");
3243
3244        // Define a function
3245        kernel
3246            .execute(r#"greet() { echo "Hello, $1!" }"#)
3247            .await
3248            .expect("function definition failed");
3249
3250        // Call the function
3251        let result = kernel
3252            .execute(r#"greet "World""#)
3253            .await
3254            .expect("function call failed");
3255
3256        assert!(result.ok(), "greet failed: {}", result.err);
3257        assert_eq!(result.out.trim(), "Hello, World!");
3258    }
3259
3260    #[tokio::test]
3261    async fn test_user_tool_positional_args() {
3262        let kernel = Kernel::transient().expect("failed to create kernel");
3263
3264        // Define a function with positional param
3265        kernel
3266            .execute(r#"greet() { echo "Hi $1" }"#)
3267            .await
3268            .expect("function definition failed");
3269
3270        // Call with positional argument
3271        let result = kernel
3272            .execute(r#"greet "Amy""#)
3273            .await
3274            .expect("function call failed");
3275
3276        assert!(result.ok(), "greet failed: {}", result.err);
3277        assert_eq!(result.out.trim(), "Hi Amy");
3278    }
3279
3280    #[tokio::test]
3281    async fn test_function_shared_scope() {
3282        let kernel = Kernel::transient().expect("failed to create kernel");
3283
3284        // Set a variable in parent scope
3285        kernel
3286            .execute(r#"SECRET="hidden""#)
3287            .await
3288            .expect("set failed");
3289
3290        // Define a function that accesses and modifies parent variable
3291        kernel
3292            .execute(r#"access_parent() {
3293                echo "${SECRET}"
3294                SECRET="modified"
3295            }"#)
3296            .await
3297            .expect("function definition failed");
3298
3299        // Call the function - it SHOULD see SECRET (shared scope like sh)
3300        let result = kernel.execute("access_parent").await.expect("function call failed");
3301
3302        // Function should have access to parent scope
3303        assert!(
3304            result.out.contains("hidden"),
3305            "Function should access parent scope, got: {}",
3306            result.out
3307        );
3308
3309        // Function should have modified the parent variable
3310        let secret = kernel.get_var("SECRET").await;
3311        assert_eq!(
3312            secret,
3313            Some(Value::String("modified".into())),
3314            "Function should modify parent scope"
3315        );
3316    }
3317
3318    #[tokio::test]
3319    async fn test_exec_builtin() {
3320        let kernel = Kernel::transient().expect("failed to create kernel");
3321        // argv is now a space-separated string or JSON array string
3322        let result = kernel
3323            .execute(r#"exec command="/bin/echo" argv="hello world""#)
3324            .await
3325            .expect("exec failed");
3326
3327        assert!(result.ok(), "exec failed: {}", result.err);
3328        assert_eq!(result.out.trim(), "hello world");
3329    }
3330
3331    #[tokio::test]
3332    async fn test_while_false_never_runs() {
3333        let kernel = Kernel::transient().expect("failed to create kernel");
3334
3335        // A while loop with false condition should never run
3336        let result = kernel
3337            .execute(r#"
3338                while false; do
3339                    echo "should not run"
3340                done
3341            "#)
3342            .await
3343            .expect("while false failed");
3344
3345        assert!(result.ok());
3346        assert!(result.out.is_empty(), "while false should not execute body: {}", result.out);
3347    }
3348
3349    #[tokio::test]
3350    async fn test_while_string_comparison() {
3351        let kernel = Kernel::transient().expect("failed to create kernel");
3352
3353        // Set a flag
3354        kernel.execute(r#"FLAG="go""#).await.expect("set failed");
3355
3356        // Use string comparison as condition (shell-compatible [[ ]] syntax)
3357        // Note: Put echo last so we can check the output
3358        let result = kernel
3359            .execute(r#"
3360                while [[ ${FLAG} == "go" ]]; do
3361                    FLAG="stop"
3362                    echo "running"
3363                done
3364            "#)
3365            .await
3366            .expect("while with string cmp failed");
3367
3368        assert!(result.ok());
3369        assert!(result.out.contains("running"), "should have run once: {}", result.out);
3370
3371        // Verify flag was changed
3372        let flag = kernel.get_var("FLAG").await;
3373        assert_eq!(flag, Some(Value::String("stop".into())));
3374    }
3375
3376    #[tokio::test]
3377    async fn test_while_numeric_comparison() {
3378        let kernel = Kernel::transient().expect("failed to create kernel");
3379
3380        // Test > comparison (shell-compatible [[ ]] with -gt)
3381        kernel.execute("N=5").await.expect("set failed");
3382
3383        // Note: Put echo last so we can check the output
3384        let result = kernel
3385            .execute(r#"
3386                while [[ ${N} -gt 3 ]]; do
3387                    N=3
3388                    echo "N was greater"
3389                done
3390            "#)
3391            .await
3392            .expect("while with > failed");
3393
3394        assert!(result.ok());
3395        assert!(result.out.contains("N was greater"), "should have run once: {}", result.out);
3396    }
3397
3398    #[tokio::test]
3399    async fn test_break_in_while_loop() {
3400        let kernel = Kernel::transient().expect("failed to create kernel");
3401
3402        let result = kernel
3403            .execute(r#"
3404                I=0
3405                while true; do
3406                    I=1
3407                    echo "before break"
3408                    break
3409                    echo "after break"
3410                done
3411            "#)
3412            .await
3413            .expect("while with break failed");
3414
3415        assert!(result.ok());
3416        assert!(result.out.contains("before break"), "should see before break: {}", result.out);
3417        assert!(!result.out.contains("after break"), "should not see after break: {}", result.out);
3418
3419        // Verify we exited the loop
3420        let i = kernel.get_var("I").await;
3421        assert_eq!(i, Some(Value::Int(1)));
3422    }
3423
3424    #[tokio::test]
3425    async fn test_continue_in_while_loop() {
3426        let kernel = Kernel::transient().expect("failed to create kernel");
3427
3428        // Test continue in a while loop where variables persist
3429        // We use string state transition: "start" -> "middle" -> "end"
3430        // continue on "middle" should skip to next iteration
3431        // Shell-compatible: use [[ ]] for comparisons
3432        let result = kernel
3433            .execute(r#"
3434                STATE="start"
3435                AFTER_CONTINUE="no"
3436                while [[ ${STATE} != "done" ]]; do
3437                    if [[ ${STATE} == "start" ]]; then
3438                        STATE="middle"
3439                        continue
3440                        AFTER_CONTINUE="yes"
3441                    fi
3442                    if [[ ${STATE} == "middle" ]]; then
3443                        STATE="done"
3444                    fi
3445                done
3446            "#)
3447            .await
3448            .expect("while with continue failed");
3449
3450        assert!(result.ok());
3451
3452        // STATE should be "done" (we completed the loop)
3453        let state = kernel.get_var("STATE").await;
3454        assert_eq!(state, Some(Value::String("done".into())));
3455
3456        // AFTER_CONTINUE should still be "no" (continue skipped the assignment)
3457        let after = kernel.get_var("AFTER_CONTINUE").await;
3458        assert_eq!(after, Some(Value::String("no".into())));
3459    }
3460
3461    #[tokio::test]
3462    async fn test_break_with_level() {
3463        let kernel = Kernel::transient().expect("failed to create kernel");
3464
3465        // Nested loop with break 2 to exit both loops
3466        // We verify by checking OUTER value:
3467        // - If break 2 works, OUTER stays at 1 (set before for loop)
3468        // - If break 2 fails, OUTER becomes 2 (set after for loop)
3469        let result = kernel
3470            .execute(r#"
3471                OUTER=0
3472                while true; do
3473                    OUTER=1
3474                    for X in "1 2"; do
3475                        break 2
3476                    done
3477                    OUTER=2
3478                done
3479            "#)
3480            .await
3481            .expect("nested break failed");
3482
3483        assert!(result.ok());
3484
3485        // OUTER should be 1 (set before for loop), not 2 (would be set after for loop)
3486        let outer = kernel.get_var("OUTER").await;
3487        assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
3488    }
3489
3490    #[tokio::test]
3491    async fn test_return_from_tool() {
3492        let kernel = Kernel::transient().expect("failed to create kernel");
3493
3494        // Define a function that returns early
3495        kernel
3496            .execute(r#"early_return() {
3497                if [[ $1 == 1 ]]; then
3498                    return 42
3499                fi
3500                echo "not returned"
3501            }"#)
3502            .await
3503            .expect("function definition failed");
3504
3505        // Call with arg=1 should return with exit code 42
3506        // (POSIX shell behavior: return N sets exit code, doesn't output N)
3507        let result = kernel
3508            .execute("early_return 1")
3509            .await
3510            .expect("function call failed");
3511
3512        // Exit code should be 42 (non-zero, so not ok())
3513        assert_eq!(result.code, 42);
3514        // Output should be empty (we returned before echo)
3515        assert!(result.out.is_empty());
3516    }
3517
3518    #[tokio::test]
3519    async fn test_return_without_value() {
3520        let kernel = Kernel::transient().expect("failed to create kernel");
3521
3522        // Define a function that returns without a value
3523        kernel
3524            .execute(r#"early_exit() {
3525                if [[ $1 == "stop" ]]; then
3526                    return
3527                fi
3528                echo "continued"
3529            }"#)
3530            .await
3531            .expect("function definition failed");
3532
3533        // Call with arg="stop" should return early
3534        let result = kernel
3535            .execute(r#"early_exit "stop""#)
3536            .await
3537            .expect("function call failed");
3538
3539        assert!(result.ok());
3540        assert!(result.out.is_empty() || result.out.trim().is_empty());
3541    }
3542
3543    #[tokio::test]
3544    async fn test_exit_stops_execution() {
3545        let kernel = Kernel::transient().expect("failed to create kernel");
3546
3547        // exit should stop further execution
3548        kernel
3549            .execute(r#"
3550                BEFORE="yes"
3551                exit 0
3552                AFTER="yes"
3553            "#)
3554            .await
3555            .expect("execution failed");
3556
3557        // BEFORE should be set, AFTER should not
3558        let before = kernel.get_var("BEFORE").await;
3559        assert_eq!(before, Some(Value::String("yes".into())));
3560
3561        let after = kernel.get_var("AFTER").await;
3562        assert!(after.is_none(), "AFTER should not be set after exit");
3563    }
3564
3565    #[tokio::test]
3566    async fn test_exit_with_code() {
3567        let kernel = Kernel::transient().expect("failed to create kernel");
3568
3569        // exit with code should propagate the exit code
3570        let result = kernel
3571            .execute("exit 42")
3572            .await
3573            .expect("exit failed");
3574
3575        assert_eq!(result.code, 42);
3576        assert!(result.out.is_empty(), "exit should not produce stdout");
3577    }
3578
3579    #[tokio::test]
3580    async fn test_set_e_stops_on_failure() {
3581        let kernel = Kernel::transient().expect("failed to create kernel");
3582
3583        // Enable error-exit mode
3584        kernel.execute("set -e").await.expect("set -e failed");
3585
3586        // Run a sequence where the middle command fails
3587        kernel
3588            .execute(r#"
3589                STEP1="done"
3590                false
3591                STEP2="done"
3592            "#)
3593            .await
3594            .expect("execution failed");
3595
3596        // STEP1 should be set, but STEP2 should NOT be set (exit on false)
3597        let step1 = kernel.get_var("STEP1").await;
3598        assert_eq!(step1, Some(Value::String("done".into())));
3599
3600        let step2 = kernel.get_var("STEP2").await;
3601        assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
3602    }
3603
3604    #[tokio::test]
3605    async fn test_set_plus_e_disables_error_exit() {
3606        let kernel = Kernel::transient().expect("failed to create kernel");
3607
3608        // Enable then disable error-exit mode
3609        kernel.execute("set -e").await.expect("set -e failed");
3610        kernel.execute("set +e").await.expect("set +e failed");
3611
3612        // Now failure should NOT stop execution
3613        kernel
3614            .execute(r#"
3615                STEP1="done"
3616                false
3617                STEP2="done"
3618            "#)
3619            .await
3620            .expect("execution failed");
3621
3622        // Both should be set since +e disables error exit
3623        let step1 = kernel.get_var("STEP1").await;
3624        assert_eq!(step1, Some(Value::String("done".into())));
3625
3626        let step2 = kernel.get_var("STEP2").await;
3627        assert_eq!(step2, Some(Value::String("done".into())));
3628    }
3629
3630    #[tokio::test]
3631    async fn test_set_ignores_unknown_options() {
3632        let kernel = Kernel::transient().expect("failed to create kernel");
3633
3634        // Bash idiom: set -euo pipefail (we support -e, ignore the rest)
3635        let result = kernel
3636            .execute("set -e -u -o pipefail")
3637            .await
3638            .expect("set with unknown options failed");
3639
3640        assert!(result.ok(), "set should succeed with unknown options");
3641
3642        // -e should still be enabled
3643        kernel
3644            .execute(r#"
3645                BEFORE="yes"
3646                false
3647                AFTER="yes"
3648            "#)
3649            .await
3650            .ok();
3651
3652        let after = kernel.get_var("AFTER").await;
3653        assert!(after.is_none(), "-e should be enabled despite unknown options");
3654    }
3655
3656    #[tokio::test]
3657    async fn test_set_no_args_shows_settings() {
3658        let kernel = Kernel::transient().expect("failed to create kernel");
3659
3660        // Enable -e
3661        kernel.execute("set -e").await.expect("set -e failed");
3662
3663        // Call set with no args to see settings
3664        let result = kernel.execute("set").await.expect("set failed");
3665
3666        assert!(result.ok());
3667        assert!(result.out.contains("set -e"), "should show -e is enabled: {}", result.out);
3668    }
3669
3670    #[tokio::test]
3671    async fn test_set_e_in_pipeline() {
3672        let kernel = Kernel::transient().expect("failed to create kernel");
3673
3674        kernel.execute("set -e").await.expect("set -e failed");
3675
3676        // Pipeline failure should trigger exit
3677        kernel
3678            .execute(r#"
3679                BEFORE="yes"
3680                false | cat
3681                AFTER="yes"
3682            "#)
3683            .await
3684            .ok();
3685
3686        let before = kernel.get_var("BEFORE").await;
3687        assert_eq!(before, Some(Value::String("yes".into())));
3688
3689        // AFTER should not be set if pipeline failure triggers exit
3690        // Note: The exit code of a pipeline is the exit code of the last command
3691        // So `false | cat` returns 0 (cat succeeds). This is bash-compatible behavior.
3692        // To test pipeline failure, we need the last command to fail.
3693    }
3694
3695    #[tokio::test]
3696    async fn test_set_e_with_and_chain() {
3697        let kernel = Kernel::transient().expect("failed to create kernel");
3698
3699        kernel.execute("set -e").await.expect("set -e failed");
3700
3701        // Commands in && chain should not trigger -e on the first failure
3702        // because && explicitly handles the error
3703        kernel
3704            .execute(r#"
3705                RESULT="initial"
3706                false && RESULT="chained"
3707                RESULT="continued"
3708            "#)
3709            .await
3710            .ok();
3711
3712        // In bash, commands in && don't trigger -e. The chain handles the failure.
3713        // Our implementation may differ - let's verify current behavior.
3714        let result = kernel.get_var("RESULT").await;
3715        // If we follow bash semantics, RESULT should be "continued"
3716        // If we trigger -e on the false, RESULT stays "initial"
3717        assert!(result.is_some(), "RESULT should be set");
3718    }
3719
3720    #[tokio::test]
3721    async fn test_set_e_exits_in_for_loop() {
3722        let kernel = Kernel::transient().expect("failed to create kernel");
3723
3724        kernel.execute("set -e").await.expect("set -e failed");
3725
3726        kernel
3727            .execute(r#"
3728                REACHED="no"
3729                for x in 1 2 3; do
3730                    false
3731                    REACHED="yes"
3732                done
3733            "#)
3734            .await
3735            .ok();
3736
3737        // With set -e, false should trigger exit; REACHED should remain "no"
3738        let reached = kernel.get_var("REACHED").await;
3739        assert_eq!(reached, Some(Value::String("no".into())),
3740            "set -e should exit on failure in for loop body");
3741    }
3742
3743    #[tokio::test]
3744    async fn test_for_loop_continues_without_set_e() {
3745        let kernel = Kernel::transient().expect("failed to create kernel");
3746
3747        // Without set -e, for loop should continue normally
3748        kernel
3749            .execute(r#"
3750                COUNT=0
3751                for x in 1 2 3; do
3752                    false
3753                    COUNT=$((COUNT + 1))
3754                done
3755            "#)
3756            .await
3757            .ok();
3758
3759        let count = kernel.get_var("COUNT").await;
3760        // Arithmetic produces Int values; accept either Int or String representation
3761        let count_val = match &count {
3762            Some(Value::Int(n)) => *n,
3763            Some(Value::String(s)) => s.parse().unwrap_or(-1),
3764            _ => -1,
3765        };
3766        assert_eq!(count_val, 3,
3767            "without set -e, loop should complete all iterations (got {:?})", count);
3768    }
3769
3770    // ═══════════════════════════════════════════════════════════════════════════
3771    // Source Tests
3772    // ═══════════════════════════════════════════════════════════════════════════
3773
3774    #[tokio::test]
3775    async fn test_source_sets_variables() {
3776        let kernel = Kernel::transient().expect("failed to create kernel");
3777
3778        // Write a script to the VFS
3779        kernel
3780            .execute(r#"write "/test.kai" 'FOO="bar"'"#)
3781            .await
3782            .expect("write failed");
3783
3784        // Source the script
3785        let result = kernel
3786            .execute(r#"source "/test.kai""#)
3787            .await
3788            .expect("source failed");
3789
3790        assert!(result.ok(), "source should succeed");
3791
3792        // Variable should be set in current scope
3793        let foo = kernel.get_var("FOO").await;
3794        assert_eq!(foo, Some(Value::String("bar".into())));
3795    }
3796
3797    #[tokio::test]
3798    async fn test_source_with_dot_alias() {
3799        let kernel = Kernel::transient().expect("failed to create kernel");
3800
3801        // Write a script to the VFS
3802        kernel
3803            .execute(r#"write "/vars.kai" 'X=42'"#)
3804            .await
3805            .expect("write failed");
3806
3807        // Source using . alias
3808        let result = kernel
3809            .execute(r#". "/vars.kai""#)
3810            .await
3811            .expect(". failed");
3812
3813        assert!(result.ok(), ". should succeed");
3814
3815        // Variable should be set in current scope
3816        let x = kernel.get_var("X").await;
3817        assert_eq!(x, Some(Value::Int(42)));
3818    }
3819
3820    #[tokio::test]
3821    async fn test_source_not_found() {
3822        let kernel = Kernel::transient().expect("failed to create kernel");
3823
3824        // Try to source a non-existent file
3825        let result = kernel
3826            .execute(r#"source "/nonexistent.kai""#)
3827            .await
3828            .expect("source should not fail with error");
3829
3830        assert!(!result.ok(), "source of non-existent file should fail");
3831        assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
3832    }
3833
3834    #[tokio::test]
3835    async fn test_source_missing_filename() {
3836        let kernel = Kernel::transient().expect("failed to create kernel");
3837
3838        // Call source with no arguments
3839        let result = kernel
3840            .execute("source")
3841            .await
3842            .expect("source should not fail with error");
3843
3844        assert!(!result.ok(), "source without filename should fail");
3845        assert!(result.err.contains("missing filename"), "error should mention missing filename");
3846    }
3847
3848    #[tokio::test]
3849    async fn test_source_executes_multiple_statements() {
3850        let kernel = Kernel::transient().expect("failed to create kernel");
3851
3852        // Write a script with multiple statements
3853        kernel
3854            .execute(r#"write "/multi.kai" 'A=1
3855B=2
3856C=3'"#)
3857            .await
3858            .expect("write failed");
3859
3860        // Source it
3861        kernel
3862            .execute(r#"source "/multi.kai""#)
3863            .await
3864            .expect("source failed");
3865
3866        // All variables should be set
3867        assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
3868        assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
3869        assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
3870    }
3871
3872    #[tokio::test]
3873    async fn test_source_can_define_functions() {
3874        let kernel = Kernel::transient().expect("failed to create kernel");
3875
3876        // Write a script that defines a function
3877        kernel
3878            .execute(r#"write "/functions.kai" 'greet() {
3879    echo "Hello, $1!"
3880}'"#)
3881            .await
3882            .expect("write failed");
3883
3884        // Source it
3885        kernel
3886            .execute(r#"source "/functions.kai""#)
3887            .await
3888            .expect("source failed");
3889
3890        // Use the defined function
3891        let result = kernel
3892            .execute(r#"greet "World""#)
3893            .await
3894            .expect("greet failed");
3895
3896        assert!(result.ok());
3897        assert!(result.out.contains("Hello, World!"));
3898    }
3899
3900    #[tokio::test]
3901    async fn test_source_inherits_error_exit() {
3902        let kernel = Kernel::transient().expect("failed to create kernel");
3903
3904        // Enable error exit
3905        kernel.execute("set -e").await.expect("set -e failed");
3906
3907        // Write a script that has a failure
3908        kernel
3909            .execute(r#"write "/fail.kai" 'BEFORE="yes"
3910false
3911AFTER="yes"'"#)
3912            .await
3913            .expect("write failed");
3914
3915        // Source it (should exit on false due to set -e)
3916        kernel
3917            .execute(r#"source "/fail.kai""#)
3918            .await
3919            .ok();
3920
3921        // BEFORE should be set, AFTER should NOT be set due to error exit
3922        let before = kernel.get_var("BEFORE").await;
3923        assert_eq!(before, Some(Value::String("yes".into())));
3924
3925        // Note: This test depends on whether error exit is checked within source
3926        // Currently our implementation checks per-statement in the main kernel
3927    }
3928
3929    // ═══════════════════════════════════════════════════════════════════════════
3930    // set -e with && / || chains
3931    // ═══════════════════════════════════════════════════════════════════════════
3932
3933    #[tokio::test]
3934    async fn test_set_e_and_chain_left_fails() {
3935        // set -e; false && echo hi; REACHED=1 → REACHED should be set
3936        let kernel = Kernel::transient().expect("failed to create kernel");
3937        kernel.execute("set -e").await.expect("set -e failed");
3938
3939        kernel
3940            .execute("false && echo hi; REACHED=1")
3941            .await
3942            .expect("execution failed");
3943
3944        let reached = kernel.get_var("REACHED").await;
3945        assert_eq!(
3946            reached,
3947            Some(Value::Int(1)),
3948            "set -e should not trigger on left side of &&"
3949        );
3950    }
3951
3952    #[tokio::test]
3953    async fn test_set_e_and_chain_right_fails() {
3954        // set -e; true && false; REACHED=1 → REACHED should NOT be set
3955        let kernel = Kernel::transient().expect("failed to create kernel");
3956        kernel.execute("set -e").await.expect("set -e failed");
3957
3958        kernel
3959            .execute("true && false; REACHED=1")
3960            .await
3961            .expect("execution failed");
3962
3963        let reached = kernel.get_var("REACHED").await;
3964        assert!(
3965            reached.is_none(),
3966            "set -e should trigger when right side of && fails"
3967        );
3968    }
3969
3970    #[tokio::test]
3971    async fn test_set_e_or_chain_recovers() {
3972        // set -e; false || echo recovered; REACHED=1 → REACHED should be set
3973        let kernel = Kernel::transient().expect("failed to create kernel");
3974        kernel.execute("set -e").await.expect("set -e failed");
3975
3976        kernel
3977            .execute("false || echo recovered; REACHED=1")
3978            .await
3979            .expect("execution failed");
3980
3981        let reached = kernel.get_var("REACHED").await;
3982        assert_eq!(
3983            reached,
3984            Some(Value::Int(1)),
3985            "set -e should not trigger when || recovers the failure"
3986        );
3987    }
3988
3989    #[tokio::test]
3990    async fn test_set_e_or_chain_both_fail() {
3991        // set -e; false || false; REACHED=1 → REACHED should NOT be set
3992        let kernel = Kernel::transient().expect("failed to create kernel");
3993        kernel.execute("set -e").await.expect("set -e failed");
3994
3995        kernel
3996            .execute("false || false; REACHED=1")
3997            .await
3998            .expect("execution failed");
3999
4000        let reached = kernel.get_var("REACHED").await;
4001        assert!(
4002            reached.is_none(),
4003            "set -e should trigger when || chain ultimately fails"
4004        );
4005    }
4006
4007    // ═══════════════════════════════════════════════════════════════════════════
4008    // Cancellation Tests
4009    // ═══════════════════════════════════════════════════════════════════════════
4010
4011    /// Helper: schedule a cancel after a delay from a background thread.
4012    /// Uses std::thread because cancel() is sync and Kernel is not Send.
4013    fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
4014        let k = Arc::clone(kernel);
4015        std::thread::spawn(move || {
4016            std::thread::sleep(delay);
4017            k.cancel();
4018        });
4019    }
4020
4021    #[tokio::test]
4022    async fn test_cancel_interrupts_for_loop() {
4023        let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4024
4025        // Schedule cancel after a short delay from a background OS thread
4026        schedule_cancel(&kernel, std::time::Duration::from_millis(10));
4027
4028        let result = kernel
4029            .execute("for i in $(seq 1 100000); do X=$i; done")
4030            .await
4031            .expect("execute failed");
4032
4033        assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
4034
4035        // The loop variable should be set to something < 100000
4036        let x = kernel.get_var("X").await;
4037        if let Some(Value::Int(n)) = x {
4038            assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
4039        }
4040    }
4041
4042    #[tokio::test]
4043    async fn test_cancel_interrupts_while_loop() {
4044        let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4045        kernel.execute("COUNT=0").await.expect("init failed");
4046
4047        schedule_cancel(&kernel, std::time::Duration::from_millis(10));
4048
4049        let result = kernel
4050            .execute("while true; do COUNT=$((COUNT + 1)); done")
4051            .await
4052            .expect("execute failed");
4053
4054        assert_eq!(result.code, 130);
4055
4056        let count = kernel.get_var("COUNT").await;
4057        if let Some(Value::Int(n)) = count {
4058            assert!(n > 0, "loop should have run at least once");
4059        }
4060    }
4061
4062    #[tokio::test]
4063    async fn test_reset_after_cancel() {
4064        // After cancellation, the next execute() should work normally
4065        let kernel = Kernel::transient().expect("failed to create kernel");
4066        kernel.cancel(); // cancel with nothing running
4067
4068        let result = kernel.execute("echo hello").await.expect("execute failed");
4069        assert!(result.ok(), "execute after cancel should succeed");
4070        assert_eq!(result.out.trim(), "hello");
4071    }
4072
4073    #[tokio::test]
4074    async fn test_cancel_interrupts_statement_sequence() {
4075        let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
4076
4077        // Schedule cancel after the first statement runs but before sleep finishes
4078        schedule_cancel(&kernel, std::time::Duration::from_millis(50));
4079
4080        let result = kernel
4081            .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
4082            .await
4083            .expect("execute failed");
4084
4085        assert_eq!(result.code, 130);
4086
4087        // STEP should be 1 (set before sleep), not 2 or 3
4088        let step = kernel.get_var("STEP").await;
4089        assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
4090    }
4091
4092    // ═══════════════════════════════════════════════════════════════════════════
4093    // Case Statement Tests
4094    // ═══════════════════════════════════════════════════════════════════════════
4095
4096    #[tokio::test]
4097    async fn test_case_simple_match() {
4098        let kernel = Kernel::transient().expect("failed to create kernel");
4099
4100        let result = kernel
4101            .execute(r#"
4102                case "hello" in
4103                    hello) echo "matched hello" ;;
4104                    world) echo "matched world" ;;
4105                esac
4106            "#)
4107            .await
4108            .expect("case failed");
4109
4110        assert!(result.ok());
4111        assert_eq!(result.out.trim(), "matched hello");
4112    }
4113
4114    #[tokio::test]
4115    async fn test_case_wildcard_match() {
4116        let kernel = Kernel::transient().expect("failed to create kernel");
4117
4118        let result = kernel
4119            .execute(r#"
4120                case "main.rs" in
4121                    "*.py") echo "Python" ;;
4122                    "*.rs") echo "Rust" ;;
4123                    "*") echo "Unknown" ;;
4124                esac
4125            "#)
4126            .await
4127            .expect("case failed");
4128
4129        assert!(result.ok());
4130        assert_eq!(result.out.trim(), "Rust");
4131    }
4132
4133    #[tokio::test]
4134    async fn test_case_default_match() {
4135        let kernel = Kernel::transient().expect("failed to create kernel");
4136
4137        let result = kernel
4138            .execute(r#"
4139                case "unknown.xyz" in
4140                    "*.py") echo "Python" ;;
4141                    "*.rs") echo "Rust" ;;
4142                    "*") echo "Default" ;;
4143                esac
4144            "#)
4145            .await
4146            .expect("case failed");
4147
4148        assert!(result.ok());
4149        assert_eq!(result.out.trim(), "Default");
4150    }
4151
4152    #[tokio::test]
4153    async fn test_case_no_match() {
4154        let kernel = Kernel::transient().expect("failed to create kernel");
4155
4156        // Case with no default branch and no match
4157        let result = kernel
4158            .execute(r#"
4159                case "nope" in
4160                    "yes") echo "yes" ;;
4161                    "no") echo "no" ;;
4162                esac
4163            "#)
4164            .await
4165            .expect("case failed");
4166
4167        assert!(result.ok());
4168        assert!(result.out.is_empty(), "no match should produce empty output");
4169    }
4170
4171    #[tokio::test]
4172    async fn test_case_with_variable() {
4173        let kernel = Kernel::transient().expect("failed to create kernel");
4174
4175        kernel.execute(r#"LANG="rust""#).await.expect("set failed");
4176
4177        let result = kernel
4178            .execute(r#"
4179                case ${LANG} in
4180                    python) echo "snake" ;;
4181                    rust) echo "crab" ;;
4182                    go) echo "gopher" ;;
4183                esac
4184            "#)
4185            .await
4186            .expect("case failed");
4187
4188        assert!(result.ok());
4189        assert_eq!(result.out.trim(), "crab");
4190    }
4191
4192    #[tokio::test]
4193    async fn test_case_multiple_patterns() {
4194        let kernel = Kernel::transient().expect("failed to create kernel");
4195
4196        let result = kernel
4197            .execute(r#"
4198                case "yes" in
4199                    "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
4200                    "n"|"no"|"N"|"NO") echo "negative" ;;
4201                esac
4202            "#)
4203            .await
4204            .expect("case failed");
4205
4206        assert!(result.ok());
4207        assert_eq!(result.out.trim(), "affirmative");
4208    }
4209
4210    #[tokio::test]
4211    async fn test_case_glob_question_mark() {
4212        let kernel = Kernel::transient().expect("failed to create kernel");
4213
4214        let result = kernel
4215            .execute(r#"
4216                case "test1" in
4217                    "test?") echo "matched test?" ;;
4218                    "*") echo "default" ;;
4219                esac
4220            "#)
4221            .await
4222            .expect("case failed");
4223
4224        assert!(result.ok());
4225        assert_eq!(result.out.trim(), "matched test?");
4226    }
4227
4228    #[tokio::test]
4229    async fn test_case_char_class() {
4230        let kernel = Kernel::transient().expect("failed to create kernel");
4231
4232        let result = kernel
4233            .execute(r#"
4234                case "Yes" in
4235                    "[Yy]*") echo "yes-like" ;;
4236                    "[Nn]*") echo "no-like" ;;
4237                esac
4238            "#)
4239            .await
4240            .expect("case failed");
4241
4242        assert!(result.ok());
4243        assert_eq!(result.out.trim(), "yes-like");
4244    }
4245
4246    // ═══════════════════════════════════════════════════════════════════════════
4247    // Cat Stdin Tests
4248    // ═══════════════════════════════════════════════════════════════════════════
4249
4250    #[tokio::test]
4251    async fn test_cat_from_pipeline() {
4252        let kernel = Kernel::transient().expect("failed to create kernel");
4253
4254        let result = kernel
4255            .execute(r#"echo "piped text" | cat"#)
4256            .await
4257            .expect("cat pipeline failed");
4258
4259        assert!(result.ok(), "cat failed: {}", result.err);
4260        assert_eq!(result.out.trim(), "piped text");
4261    }
4262
4263    #[tokio::test]
4264    async fn test_cat_from_pipeline_multiline() {
4265        let kernel = Kernel::transient().expect("failed to create kernel");
4266
4267        let result = kernel
4268            .execute(r#"echo "line1\nline2" | cat -n"#)
4269            .await
4270            .expect("cat pipeline failed");
4271
4272        assert!(result.ok(), "cat failed: {}", result.err);
4273        assert!(result.out.contains("1\t"), "output: {}", result.out);
4274    }
4275
4276    // ═══════════════════════════════════════════════════════════════════════════
4277    // Heredoc Tests
4278    // ═══════════════════════════════════════════════════════════════════════════
4279
4280    #[tokio::test]
4281    async fn test_heredoc_basic() {
4282        let kernel = Kernel::transient().expect("failed to create kernel");
4283
4284        let result = kernel
4285            .execute("cat <<EOF\nhello\nEOF")
4286            .await
4287            .expect("heredoc failed");
4288
4289        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
4290        assert_eq!(result.out.trim(), "hello");
4291    }
4292
4293    #[tokio::test]
4294    async fn test_arithmetic_in_string() {
4295        let kernel = Kernel::transient().expect("failed to create kernel");
4296
4297        let result = kernel
4298            .execute(r#"echo "result: $((1 + 2))""#)
4299            .await
4300            .expect("arithmetic in string failed");
4301
4302        assert!(result.ok(), "echo failed: {}", result.err);
4303        assert_eq!(result.out.trim(), "result: 3");
4304    }
4305
4306    #[tokio::test]
4307    async fn test_heredoc_multiline() {
4308        let kernel = Kernel::transient().expect("failed to create kernel");
4309
4310        let result = kernel
4311            .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
4312            .await
4313            .expect("heredoc failed");
4314
4315        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
4316        assert!(result.out.contains("line1"), "output: {}", result.out);
4317        assert!(result.out.contains("line2"), "output: {}", result.out);
4318        assert!(result.out.contains("line3"), "output: {}", result.out);
4319    }
4320
4321    #[tokio::test]
4322    async fn test_heredoc_variable_expansion() {
4323        // Bug N: unquoted heredoc should expand variables
4324        let kernel = Kernel::transient().expect("failed to create kernel");
4325
4326        kernel.execute("GREETING=hello").await.expect("set var");
4327
4328        let result = kernel
4329            .execute("cat <<EOF\n$GREETING world\nEOF")
4330            .await
4331            .expect("heredoc expansion failed");
4332
4333        assert!(result.ok(), "heredoc expansion failed: {}", result.err);
4334        assert_eq!(result.out.trim(), "hello world");
4335    }
4336
4337    #[tokio::test]
4338    async fn test_heredoc_quoted_no_expansion() {
4339        // Bug N: quoted heredoc (<<'EOF') should NOT expand variables
4340        let kernel = Kernel::transient().expect("failed to create kernel");
4341
4342        kernel.execute("GREETING=hello").await.expect("set var");
4343
4344        let result = kernel
4345            .execute("cat <<'EOF'\n$GREETING world\nEOF")
4346            .await
4347            .expect("quoted heredoc failed");
4348
4349        assert!(result.ok(), "quoted heredoc failed: {}", result.err);
4350        assert_eq!(result.out.trim(), "$GREETING world");
4351    }
4352
4353    #[tokio::test]
4354    async fn test_heredoc_default_value_expansion() {
4355        // Bug N: ${VAR:-default} should expand in unquoted heredocs
4356        let kernel = Kernel::transient().expect("failed to create kernel");
4357
4358        let result = kernel
4359            .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
4360            .await
4361            .expect("heredoc default expansion failed");
4362
4363        assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
4364        assert_eq!(result.out.trim(), "fallback");
4365    }
4366
4367    // ═══════════════════════════════════════════════════════════════════════════
4368    // Read Builtin Tests
4369    // ═══════════════════════════════════════════════════════════════════════════
4370
4371    #[tokio::test]
4372    async fn test_read_from_pipeline() {
4373        let kernel = Kernel::transient().expect("failed to create kernel");
4374
4375        // Pipe input to read
4376        let result = kernel
4377            .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
4378            .await
4379            .expect("read pipeline failed");
4380
4381        assert!(result.ok(), "read failed: {}", result.err);
4382        assert!(result.out.contains("Hello, Alice"), "output: {}", result.out);
4383    }
4384
4385    #[tokio::test]
4386    async fn test_read_multiple_vars_from_pipeline() {
4387        let kernel = Kernel::transient().expect("failed to create kernel");
4388
4389        let result = kernel
4390            .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
4391            .await
4392            .expect("read pipeline failed");
4393
4394        assert!(result.ok(), "read failed: {}", result.err);
4395        assert!(result.out.contains("John is 42"), "output: {}", result.out);
4396    }
4397
4398    // ═══════════════════════════════════════════════════════════════════════════
4399    // Shell-Style Function Tests
4400    // ═══════════════════════════════════════════════════════════════════════════
4401
4402    #[tokio::test]
4403    async fn test_posix_function_with_positional_params() {
4404        let kernel = Kernel::transient().expect("failed to create kernel");
4405
4406        // Define POSIX-style function
4407        kernel
4408            .execute(r#"greet() { echo "Hello, $1!" }"#)
4409            .await
4410            .expect("function definition failed");
4411
4412        // Call the function
4413        let result = kernel
4414            .execute(r#"greet "Amy""#)
4415            .await
4416            .expect("function call failed");
4417
4418        assert!(result.ok(), "greet failed: {}", result.err);
4419        assert_eq!(result.out.trim(), "Hello, Amy!");
4420    }
4421
4422    #[tokio::test]
4423    async fn test_posix_function_multiple_args() {
4424        let kernel = Kernel::transient().expect("failed to create kernel");
4425
4426        // Define function using $1 and $2
4427        kernel
4428            .execute(r#"add_greeting() { echo "$1 $2!" }"#)
4429            .await
4430            .expect("function definition failed");
4431
4432        // Call the function
4433        let result = kernel
4434            .execute(r#"add_greeting "Hello" "World""#)
4435            .await
4436            .expect("function call failed");
4437
4438        assert!(result.ok(), "function failed: {}", result.err);
4439        assert_eq!(result.out.trim(), "Hello World!");
4440    }
4441
4442    #[tokio::test]
4443    async fn test_bash_function_with_positional_params() {
4444        let kernel = Kernel::transient().expect("failed to create kernel");
4445
4446        // Define bash-style function (function keyword, no parens)
4447        kernel
4448            .execute(r#"function greet { echo "Hi $1" }"#)
4449            .await
4450            .expect("function definition failed");
4451
4452        // Call the function
4453        let result = kernel
4454            .execute(r#"greet "Bob""#)
4455            .await
4456            .expect("function call failed");
4457
4458        assert!(result.ok(), "greet failed: {}", result.err);
4459        assert_eq!(result.out.trim(), "Hi Bob");
4460    }
4461
4462    #[tokio::test]
4463    async fn test_shell_function_with_all_args() {
4464        let kernel = Kernel::transient().expect("failed to create kernel");
4465
4466        // Define function using $@ (all args)
4467        kernel
4468            .execute(r#"echo_all() { echo "args: $@" }"#)
4469            .await
4470            .expect("function definition failed");
4471
4472        // Call with multiple args
4473        let result = kernel
4474            .execute(r#"echo_all "a" "b" "c""#)
4475            .await
4476            .expect("function call failed");
4477
4478        assert!(result.ok(), "function failed: {}", result.err);
4479        assert_eq!(result.out.trim(), "args: a b c");
4480    }
4481
4482    #[tokio::test]
4483    async fn test_shell_function_with_arg_count() {
4484        let kernel = Kernel::transient().expect("failed to create kernel");
4485
4486        // Define function using $# (arg count)
4487        kernel
4488            .execute(r#"count_args() { echo "count: $#" }"#)
4489            .await
4490            .expect("function definition failed");
4491
4492        // Call with three args
4493        let result = kernel
4494            .execute(r#"count_args "x" "y" "z""#)
4495            .await
4496            .expect("function call failed");
4497
4498        assert!(result.ok(), "function failed: {}", result.err);
4499        assert_eq!(result.out.trim(), "count: 3");
4500    }
4501
4502    #[tokio::test]
4503    async fn test_shell_function_shared_scope() {
4504        let kernel = Kernel::transient().expect("failed to create kernel");
4505
4506        // Set a variable in parent scope
4507        kernel
4508            .execute(r#"PARENT_VAR="visible""#)
4509            .await
4510            .expect("set failed");
4511
4512        // Define shell function that reads and writes parent variable
4513        kernel
4514            .execute(r#"modify_parent() {
4515                echo "saw: ${PARENT_VAR}"
4516                PARENT_VAR="changed by function"
4517            }"#)
4518            .await
4519            .expect("function definition failed");
4520
4521        // Call the function - it SHOULD see PARENT_VAR (bash-compatible shared scope)
4522        let result = kernel.execute("modify_parent").await.expect("function failed");
4523
4524        assert!(
4525            result.out.contains("visible"),
4526            "Shell function should access parent scope, got: {}",
4527            result.out
4528        );
4529
4530        // Parent variable should be modified
4531        let var = kernel.get_var("PARENT_VAR").await;
4532        assert_eq!(
4533            var,
4534            Some(Value::String("changed by function".into())),
4535            "Shell function should modify parent scope"
4536        );
4537    }
4538
4539    // ═══════════════════════════════════════════════════════════════════════════
4540    // Script Execution via PATH Tests
4541    // ═══════════════════════════════════════════════════════════════════════════
4542
4543    #[tokio::test]
4544    async fn test_script_execution_from_path() {
4545        let kernel = Kernel::transient().expect("failed to create kernel");
4546
4547        // Create /bin directory and script
4548        kernel.execute(r#"mkdir "/bin""#).await.ok();
4549        kernel
4550            .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
4551            .await
4552            .expect("write script failed");
4553
4554        // Set PATH to /bin
4555        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4556
4557        // Call script by name (without .kai extension)
4558        let result = kernel
4559            .execute("hello")
4560            .await
4561            .expect("script execution failed");
4562
4563        assert!(result.ok(), "script failed: {}", result.err);
4564        assert_eq!(result.out.trim(), "Hello from script!");
4565    }
4566
4567    #[tokio::test]
4568    async fn test_script_with_args() {
4569        let kernel = Kernel::transient().expect("failed to create kernel");
4570
4571        // Create script that uses positional params
4572        kernel.execute(r#"mkdir "/bin""#).await.ok();
4573        kernel
4574            .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
4575            .await
4576            .expect("write script failed");
4577
4578        // Set PATH
4579        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4580
4581        // Call script with arg
4582        let result = kernel
4583            .execute(r#"greet "World""#)
4584            .await
4585            .expect("script execution failed");
4586
4587        assert!(result.ok(), "script failed: {}", result.err);
4588        assert_eq!(result.out.trim(), "Hello, World!");
4589    }
4590
4591    #[tokio::test]
4592    async fn test_script_not_found() {
4593        let kernel = Kernel::transient().expect("failed to create kernel");
4594
4595        // Set empty PATH
4596        kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
4597
4598        // Call non-existent script
4599        let result = kernel
4600            .execute("noscript")
4601            .await
4602            .expect("execution failed");
4603
4604        assert!(!result.ok(), "should fail with command not found");
4605        assert_eq!(result.code, 127);
4606        assert!(result.err.contains("command not found"));
4607    }
4608
4609    #[tokio::test]
4610    async fn test_script_path_search_order() {
4611        let kernel = Kernel::transient().expect("failed to create kernel");
4612
4613        // Create two directories with same-named script
4614        // Note: using "myscript" not "test" to avoid conflict with test builtin
4615        kernel.execute(r#"mkdir "/first""#).await.ok();
4616        kernel.execute(r#"mkdir "/second""#).await.ok();
4617        kernel
4618            .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
4619            .await
4620            .expect("write failed");
4621        kernel
4622            .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
4623            .await
4624            .expect("write failed");
4625
4626        // Set PATH with first before second
4627        kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
4628
4629        // Should find first one
4630        let result = kernel
4631            .execute("myscript")
4632            .await
4633            .expect("script execution failed");
4634
4635        assert!(result.ok(), "script failed: {}", result.err);
4636        assert_eq!(result.out.trim(), "from first");
4637    }
4638
4639    // ═══════════════════════════════════════════════════════════════════════════
4640    // Special Variable Tests ($?, $$, unset vars)
4641    // ═══════════════════════════════════════════════════════════════════════════
4642
4643    #[tokio::test]
4644    async fn test_last_exit_code_success() {
4645        let kernel = Kernel::transient().expect("failed to create kernel");
4646
4647        // true exits with 0
4648        let result = kernel.execute("true; echo $?").await.expect("execution failed");
4649        assert!(result.out.contains("0"), "expected 0, got: {}", result.out);
4650    }
4651
4652    #[tokio::test]
4653    async fn test_last_exit_code_failure() {
4654        let kernel = Kernel::transient().expect("failed to create kernel");
4655
4656        // false exits with 1
4657        let result = kernel.execute("false; echo $?").await.expect("execution failed");
4658        assert!(result.out.contains("1"), "expected 1, got: {}", result.out);
4659    }
4660
4661    #[tokio::test]
4662    async fn test_current_pid() {
4663        let kernel = Kernel::transient().expect("failed to create kernel");
4664
4665        let result = kernel.execute("echo $$").await.expect("execution failed");
4666        // PID should be a positive number
4667        let pid: u32 = result.out.trim().parse().expect("PID should be a number");
4668        assert!(pid > 0, "PID should be positive");
4669    }
4670
4671    #[tokio::test]
4672    async fn test_unset_variable_expands_to_empty() {
4673        let kernel = Kernel::transient().expect("failed to create kernel");
4674
4675        // Unset variable in interpolation should be empty
4676        let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
4677        assert_eq!(result.out.trim(), "prefix::suffix");
4678    }
4679
4680    #[tokio::test]
4681    async fn test_eq_ne_operators() {
4682        let kernel = Kernel::transient().expect("failed to create kernel");
4683
4684        // Test -eq operator
4685        let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
4686        assert_eq!(result.out.trim(), "eq works");
4687
4688        // Test -ne operator
4689        let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
4690        assert_eq!(result.out.trim(), "ne works");
4691
4692        // Test -eq with different values
4693        let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
4694        assert_eq!(result.out.trim(), "correct");
4695    }
4696
4697    #[tokio::test]
4698    async fn test_escaped_dollar_in_string() {
4699        let kernel = Kernel::transient().expect("failed to create kernel");
4700
4701        // \$ should produce literal $
4702        let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
4703        assert_eq!(result.out.trim(), "$100");
4704    }
4705
4706    #[tokio::test]
4707    async fn test_special_vars_in_interpolation() {
4708        let kernel = Kernel::transient().expect("failed to create kernel");
4709
4710        // Test $? in string interpolation
4711        let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
4712        assert_eq!(result.out.trim(), "exit: 0");
4713
4714        // Test $$ in string interpolation
4715        let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
4716        assert!(result.out.starts_with("pid: "), "unexpected output: {}", result.out);
4717        let pid_part = result.out.trim().strip_prefix("pid: ").unwrap();
4718        let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
4719    }
4720
4721    // ═══════════════════════════════════════════════════════════════════════════
4722    // Command Substitution Tests
4723    // ═══════════════════════════════════════════════════════════════════════════
4724
4725    #[tokio::test]
4726    async fn test_command_subst_assignment() {
4727        let kernel = Kernel::transient().expect("failed to create kernel");
4728
4729        // Command substitution in assignment
4730        let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
4731        assert_eq!(result.out.trim(), "hello");
4732    }
4733
4734    #[tokio::test]
4735    async fn test_command_subst_with_args() {
4736        let kernel = Kernel::transient().expect("failed to create kernel");
4737
4738        // Command substitution with string argument
4739        let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
4740        assert_eq!(result.out.trim(), "a b c");
4741    }
4742
4743    #[tokio::test]
4744    async fn test_command_subst_nested_vars() {
4745        let kernel = Kernel::transient().expect("failed to create kernel");
4746
4747        // Variables inside command substitution
4748        let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
4749        assert_eq!(result.out.trim(), "hello world");
4750    }
4751
4752    #[tokio::test]
4753    async fn test_background_job_basic() {
4754        use std::time::Duration;
4755
4756        let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
4757
4758        // Run a simple background command
4759        let result = kernel.execute("echo hello &").await.expect("execution failed");
4760        assert!(result.ok(), "background command should succeed: {}", result.err);
4761        assert!(result.out.contains("[1]"), "should return job ID: {}", result.out);
4762
4763        // Give the job time to complete
4764        tokio::time::sleep(Duration::from_millis(100)).await;
4765
4766        // Check job status
4767        let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
4768        assert!(status.ok(), "status should succeed: {}", status.err);
4769        assert!(
4770            status.out.contains("done:") || status.out.contains("running"),
4771            "should have valid status: {}",
4772            status.out
4773        );
4774
4775        // Check stdout
4776        let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
4777        assert!(stdout.ok());
4778        assert!(stdout.out.contains("hello"));
4779    }
4780
4781    #[tokio::test]
4782    async fn test_heredoc_piped_to_command() {
4783        // Bug 4: heredoc content should pipe through to next command
4784        let kernel = Kernel::transient().expect("kernel");
4785        let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
4786        assert!(result.ok(), "heredoc | cat failed: {}", result.err);
4787        assert_eq!(result.out.trim(), "hello world");
4788    }
4789
4790    #[tokio::test]
4791    async fn test_for_loop_glob_iterates() {
4792        // Bug 1: for F in $(glob ...) should iterate per file, not once
4793        let kernel = Kernel::transient().expect("kernel");
4794        let dir = format!("/tmp/kaish_test_glob_{}", std::process::id());
4795        kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4796        kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4797        kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4798        let result = kernel.execute(&format!(r#"
4799            N=0
4800            for F in $(glob "{dir}/*.txt"); do
4801                N=$((N + 1))
4802            done
4803            echo $N
4804        "#)).await.unwrap();
4805        assert!(result.ok(), "for glob failed: {}", result.err);
4806        assert_eq!(result.out.trim(), "2", "Should iterate 2 files, got: {}", result.out);
4807        kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4808        kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4809    }
4810
4811    #[tokio::test]
4812    async fn test_command_subst_echo_not_iterable() {
4813        // Regression guard: $(echo "a b c") must remain a single string
4814        let kernel = Kernel::transient().expect("kernel");
4815        let result = kernel.execute(r#"
4816            N=0
4817            for X in $(echo "a b c"); do N=$((N + 1)); done
4818            echo $N
4819        "#).await.unwrap();
4820        assert!(result.ok());
4821        assert_eq!(result.out.trim(), "1", "echo should be one item: {}", result.out);
4822    }
4823
4824    // -- accumulate_result / newline tests --
4825
4826    #[test]
4827    fn test_accumulate_no_double_newlines() {
4828        // When output already ends with \n, accumulate should not add another
4829        let mut acc = ExecResult::success("line1\n");
4830        let new = ExecResult::success("line2\n");
4831        accumulate_result(&mut acc, &new);
4832        assert_eq!(acc.out, "line1\nline2\n");
4833        assert!(!acc.out.contains("\n\n"), "should not have double newlines: {:?}", acc.out);
4834    }
4835
4836    #[test]
4837    fn test_accumulate_adds_separator_when_needed() {
4838        // When output does NOT end with \n, accumulate adds one
4839        let mut acc = ExecResult::success("line1");
4840        let new = ExecResult::success("line2");
4841        accumulate_result(&mut acc, &new);
4842        assert_eq!(acc.out, "line1\nline2");
4843    }
4844
4845    #[test]
4846    fn test_accumulate_empty_into_nonempty() {
4847        let mut acc = ExecResult::success("");
4848        let new = ExecResult::success("hello\n");
4849        accumulate_result(&mut acc, &new);
4850        assert_eq!(acc.out, "hello\n");
4851    }
4852
4853    #[test]
4854    fn test_accumulate_nonempty_into_empty() {
4855        let mut acc = ExecResult::success("hello\n");
4856        let new = ExecResult::success("");
4857        accumulate_result(&mut acc, &new);
4858        assert_eq!(acc.out, "hello\n");
4859    }
4860
4861    #[test]
4862    fn test_accumulate_stderr_no_double_newlines() {
4863        let mut acc = ExecResult::failure(1, "err1\n");
4864        let new = ExecResult::failure(1, "err2\n");
4865        accumulate_result(&mut acc, &new);
4866        assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
4867    }
4868
4869    #[tokio::test]
4870    async fn test_multiple_echo_no_blank_lines() {
4871        let kernel = Kernel::transient().expect("kernel");
4872        let result = kernel
4873            .execute("echo one\necho two\necho three")
4874            .await
4875            .expect("execution failed");
4876        assert!(result.ok());
4877        assert_eq!(result.out, "one\ntwo\nthree\n");
4878    }
4879
4880    #[tokio::test]
4881    async fn test_for_loop_no_blank_lines() {
4882        let kernel = Kernel::transient().expect("kernel");
4883        let result = kernel
4884            .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
4885            .await
4886            .expect("execution failed");
4887        assert!(result.ok());
4888        assert_eq!(result.out, "item: a\nitem: b\nitem: c\n");
4889    }
4890
4891    #[tokio::test]
4892    async fn test_for_command_subst_no_blank_lines() {
4893        let kernel = Kernel::transient().expect("kernel");
4894        let result = kernel
4895            .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
4896            .await
4897            .expect("execution failed");
4898        assert!(result.ok());
4899        assert_eq!(result.out, "n=1\nn=2\nn=3\n");
4900    }
4901
4902}