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