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