mlld
Version:
mlld: llm scripting language
568 lines (482 loc) • 128 kB
Markdown
# Changelog
All notable changes to the mlld project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.0-rc81]
### Added
- **Self-documenting help system**: `mlld howto` provides LLM-accessible documentation directly in the CLI
- `mlld howto` - Show topic tree with intro pinned at top
- `mlld howto intro` - Introduction with mental model and key concepts
- `mlld howto <topic>` - Show all help for a specific topic (e.g., `mlld howto when`)
- `mlld howto <topic> <subtopic>` - Show specific subtopic (e.g., `mlld howto when first`)
- `mlld howto grep <pattern>` - Search across all atoms for matching lines
- `mlld qs` / `mlld quickstart` - Quick start guide
- Built on atom-based documentation architecture (docs/src/atoms/) enabling DRY content reuse
- Pattern documented in docs/dev/HOWTO-PATTERN.md for adoption by other tools
- Documentation atoms: 106 atoms extracted covering intro, syntax, commands, modules, patterns, security, configuration, and common mistakes
- Git pre-commit hook auto-updates atom 'updated' dates when modified atoms are staged
- Colorized terminal output: syntax-highlighted code blocks, colored headers, topic tree with colored categories and IDs
- **Prose execution**: Define executable functions that invoke a prose interpreter via LLM
- Syntax: `exe @fn(params) = prose:@config { inline content }`
- File-based: `exe @fn(params) = prose:@config "file.prose"`
- Template files: `.prose.att` (`@var`) and `.prose.mtt` (`{{var}}`); `.prose` files do not interpolate
- Config uses model executors: `{ model: @opus, skills: [...] }`
- Pre-built configs available from `@mlld/prose` public module
- Requires [OpenProse](https://prose.md) skill or another prose interpreter
- Skills must be approved in Claude Code before use
- See [docs/user/prose.md](docs/user/prose.md) for full documentation
- **`mlld validate`**: Static analysis command for syntax validation without execution
- `mlld validate <file>` - Validate syntax and show module structure (exports, imports, executables, guards, needs)
- `--format json` - Machine-readable JSON output for tooling integration
- `--ast` - Include parsed AST in JSON output (requires `--format json`)
- Returns exit code 1 for invalid files, enabling CI/toolchain integration
- `mlld analyze` as alias
### Changed
- **Terminology**: "prose mode" renamed to "markdown mode" to avoid confusion with prose execution
- `.md` and `.mld.md` files use "markdown mode" (slash-prefixed directives)
- "prose" now refers to OpenProse/prose execution, not the file format
### Fixed
- **`mlld howto` shows all atom categories**: Fixed howto command to load atoms from all 8 categories (syntax, commands, control-flow, modules, patterns, configuration, security, mistakes) instead of only control-flow
## [2.0.0-rc80]
### Added
- **`mlld docs @author/module`**: New command to display module documentation, showing `# tldr` section followed by `# docs` section from published modules
- **`mlld info` shows real registry data**: Now fetches actual module metadata from registry (version, needs, license, repo, keywords) and appends the `# tldr` section
- **Colorized CLI output**: Module info and docs display with syntax highlighting for mlld code blocks, colored headers, and formatted metadata
- **`mlld-run` blocks use strict mode**: Code inside `mlld-run` fenced blocks now parses in strict mode (slashes optional, no prose between directives)
- **Transitive dependency installation**: `mlld install` now automatically installs mlld module dependencies (npm-style)
- When installing `@alice/utils`, its `dependencies` from frontmatter are discovered and installed
- Recursive: transitive deps of deps are also installed
- All modules recorded in `mlld-lock.json` (config unchanged - only direct deps in `mlld-config.json`)
- Install summary shows breakdown: "3 modules installed (1 direct, 2 transitive)"
- Lazy runtime fetching still works as fallback for modules not pre-installed
### Fixed
- **Module publish validation for exe declarations**: Fixed `ExportValidator` not recognizing exe declarations where the identifier is a `VariableReference` node. This caused `mlld publish` to fail with "Exported name is not declared" errors for modules like `@mlld/array` and `@mlld/string`.
- **Module scope isolation for nested imports**: Fixed bug where importing a module that internally imports from another module would cause "variable already imported" errors. Child module scopes are now properly isolated from parent scope during import evaluation.
- **Executable preservation in object properties**: Fixed bug where executables stored as object properties would lose their Variable wrapper during import, causing `isExecutableVariable()` to return false. Object property executables are now properly reconstructed during import.
- **Registry publish SHA error**: Fixed "sha wasn't supplied" error when publishing new versions of existing modules. The existing tags.json SHA is now properly fetched and provided when updating.
- **Duplicate version publish check**: `mlld publish` now checks if the specific version already exists in the registry before attempting to create a PR, preventing wasted effort on duplicate publishes.
## [2.0.0-rc79]
### Added
- **Strict mode for .mld files**: Bare directive syntax without slash prefixes
- `.mld` files use strict mode: bare directives (`var`, `show`, `exe`), text lines error, blank lines ignored
- `.mld.md` and `.md` files use markdown mode: require `/` prefix, text becomes content (existing behavior)
- Slash prefix optional in strict mode for backward compatibility (`/var` and `var` both work)
- SDK defaults to strict mode for raw strings (no file path) and unknown extensions
- File extension determines parsing mode: `.mld` → strict, `.mld.md`/`.md` → markdown
- CLI flags: `--loose`/`--markdown`/`--md`/`--prose` force markdown mode
- Mode included in AST cache keys to differentiate same file parsed in different modes
- **Block syntax for exe and for**: Multi-statement bodies using `[...]` delimiters
- Exe blocks: `/exe @func() = [let @x = 1; let @y = 2; => @x + @y]` (statements separated by newlines or semicolons)
- For blocks: `/for @item in @items [show @item; let @count += 1]` (`=>` optional for block bodies)
- When-expression exe block actions: `when first [ @cond => [...] ]` supports full exe block semantics (let, side effects, return)
- `let @var = value` creates block-scoped variables; `let @var += value` for augmented assignment (arrays, strings, objects, numbers)
- `=> @value` optional return must be the last statement when present in exe blocks
- Nested for/when inside blocks supported; inner directives are slashless
- Blocks use `[...]` (not `{...}`) to distinguish mlld control flow from code/command/data
- **Block syntax for var**: `/var @value = [let @x = ...; => @x]` evaluates a local statement block and returns its value
- **Block comments in bracket bodies**: `>>`/`<<` comments inside `[...]` blocks (exe, for, when, guard, when-expressions) are consumed as whitespace
- **While loops**: Bounded iteration with `done`/`continue` control flow
- `/while (100) @processor` - directive form with iteration cap
- `@input | while(100, 1s) @processor` - pipeline stage with optional pacing
- `done @value` terminates iteration and returns value
- `continue @newState` advances to next iteration with new state
- `@mx.while.iteration` (1-based) and `@mx.while.limit` available in processor
- **Streaming format adapters**: NDJSON streaming parsed via configurable adapters
- `with { streamFormat: "claude-code" }` for Claude SDK-specific parsing
- Default `ndjson` adapter handles generic JSON streaming
- `with { streamFormat: @adapterConfig }` accepts custom AdapterConfig objects
- **Command working directories**: `cmd:/abs/path`, `sh:/abs/path`, `bash:/abs/path`, `js:/abs/path`, `node:/abs/path`, and `python:/abs/path` set the execution directory for `/run`, inline pipelines, and `/exe` definitions; execution fails on relative, missing, or non-Unix paths
- **Template collection imports**: Load entire directories of templates with shared parameter signatures
- `/import templates from "@base/agents" as @agents(message, context)` imports all `.att`/`.mtt` files
- Access by filename: `@agents["alice"](@msg, @ctx)` or nested: `@agents.support["helper"]`
- **Directory module imports**: Import a directory that loads each immediate subdirectory `index.mld`
- Returns an object keyed by sanitized directory name with each `index.mld` export set as the value
- Skips `_*` and `.*` directories by default, override with `with { skipDirs: [] }`
- **@exists() builtin**: Returns true when an expression evaluates without error (string args check paths; glob args require at least one match)
- **When separators**: Semicolons separate when arms in directives and expressions
- **Bound-value when-expressions**: `when @value first [...]` and `when @value [...]` support pattern arms like `>= 0.7`, `>= 0.3 && < 0.7`, and `*`
- **Nullish coalescing operator**: `??` returns the left operand when it is not nullish, otherwise returns the right operand
- **For-when filter sugar**: `for ... when ...` drops non-matches without null placeholders via implicit `none => skip` branch
- **Conditional inclusion (`@var?`)**: Universal pattern for conditionally including content based on truthiness
- Commands/Templates: `@var?`...`` - include backtick template if truthy (e.g., `cmd { echo @tools?`--tools` }`)
- Strings: `@var?"..."` - include quoted fragment if truthy (e.g., `"Hello @title?"@title "`)
- Arrays: `[@a, @b?, @c]` - omit element if falsy
- Objects: `{key?: @val}` - omit pair if value is falsy
- Field access supported: `@obj.field?`...`` evaluates full path before truthiness check
- **Object mx helpers**: `@obj.mx.keys`, `@obj.mx.values`, and `@obj.mx.entries` expose object utilities
- **For-loop item keys**: `@item.mx.key` exposes the current object key (alongside `@item_key`)
- **Parallel execution error accumulation**: Errors accumulate in `@mx.errors` array instead of failing fast
- `/for parallel @x in @xs [complex-block]` supported with block-scoped `let` only
- Failed iterations produce error markers `{ index, key?, message, error, value }` in results
- Parallel pipeline groups (`|| @a || @b || @c`) also accumulate errors
- **Comments in block bodies**: `>>` (start-of-line) and `<<` (end-of-line) comments work inside `[...]` blocks
### Changed
- **Renamed `@ctx` to `@mx`**: The execution context variable is now `@mx` ("mlld execution"). Access retry count via `@mx.try`, hints via `@mx.hint`, stage info via `@mx.stage`, etc. The `.ctx` metadata namespace on variables is now `.mx` ("metadata")—use `@file.mx.filename`, `@file.mx.tokens`, etc.
- `/exe` RHS pipe sugar accepts direct `@value | cmd { ... }` pipelines (legacy `run` form still works); identity definitions keep with-clause pipelines when evaluating parameters
- **Mode-aware parsing**: Environment variable `MLLD_STRICT=1` forces strict mode, `MLLD_STRICT=0` forces markdown mode, overriding file extension inference
- FormatAdapterSink and TerminalSink are mutually exclusive
- **Import from @payload and @state**: Route files can now import fields from execute() payload and state
- `/import { @message, @userId } from @payload` imports specific fields
- `/import { @conversationId } from @state` imports state fields
- Enables explicit, auditable access to runtime-injected data
- Similar pattern to `/import { USER } from @input` for environment variables
- **Live @state and literal payload strings**: `@state` reads stay fresh after state writes, and `@payload/@state` dynamic modules emit literal strings so @mentions and user data do not interpolate.
- **LoadContentResult implements StructuredValue surface**: File loading now returns values with `.text`, `.data`, and `.mx` surfaces. Access metadata via `.mx.filename`, `.mx.tokens`, `.mx.fm` etc. Direct property access (`.content`, `.filename`) remains for backward compatibility but `.mx` is recommended for new code.
- **Simplified .keep usage**: Metadata now accessible in mlld contexts without `.keep` (e.g., `@file.mx.filename` works directly). `.keep` only needed when passing to JS/Node to preserve StructuredValue wrapper. Apply `.keep` at call site (`@process(@file.keep)`) rather than variable creation.
### Fixed
- **@debug variable stack overflow**: Fixed infinite recursion when accessing `@debug`. Variable metadata getter was calling itself recursively when building context snapshot.
- **Exe block return-only syntax**: Exe blocks can now return directly without a let statement: `exe @f() = [ => { name: "hello" } ]`
- **Method calls in object literal values**: Method calls like `@x.trim()` now work as object literal values in returns: `=> { file: @f.mx.relative, review: @review.trim() }`
- **`.mx` access in for loops**: File metadata (`@f.mx.relative`, `@f.mx.filename`) now accessible when iterating over glob results in all contexts: direct interpolation, object literals (`{ file: @f.mx.relative }`), and exe function parameters.
- **@json error clarity**: `@json` throws clear errors when parsing fails instead of silently mangling input. Detects markdown code fences and suggests `@json.llm`.
- **Pipeline filter error**: Writing `| json` instead of `| @json` now gives helpful error: "Pipeline filters require the @ prefix"
- **Arithmetic operators in exe blocks**: Math operators (`+`, `-`, `*`, `/`, `%`) work in exe blocks, let assignments, and return values
- **Universal StructuredValue model**: All runtime values flow as StructuredValues with `.text`, `.data`, and `.mx` surfaces. Boundaries use `asData()`/`asText()` for extraction. Fixes when-expressions returning numbers, object serialization, and numeric comparisons.
- **Field access precedence**: User data properties take precedence over guard quantifiers (`.all`, `.any`, `.none`). Core metadata (`.type`, `.mx`) always from Variable.
- **Standalone @ in double-quoted strings**: `@` not followed by identifier treated as literal character (`.startsWith("@")` now works)
- **Setup in nested directories**: `mlld setup` detects parent config and prompts to update parent or create new local config
- Effect actions (`show`, `log`, `output`, `append`) work uniformly in all RHS contexts
- Streaming no longer produces duplicate output when using format adapters
- Regex arguments are parsed as RegExp values, so `.match(/.../)` conditions (including grouped patterns) work in when-expressions and other exec calls without falling back to strings
- Block directive parse errors reparse with correct offsets for better error locations
- Registry publish flow improvements: recreates missing fork refs, minimal PR bodies, better error messages
- Module installer honors requested versions by purging mismatched cache entries
- Lock file normalization strips version suffixes to prevent duplicates
- Variable boundary escaping (`@var\.ext`) works in all interpolation contexts
- `@@` and `\@` both escape to literal `@`
- Template paths support `@var` interpolation in double-quoted strings
- CLI `--payload` alias for `--inject`
- **ESM bundle compatibility**: MJS bundle fixed for Node 24+ ESM projects (converted `require()` calls to ESM imports)
- **LSP: Mode-aware highlighting**: Language server detects `.mld` (strict) vs `.mld.md` (markdown) and highlights bare directives correctly; text content in strict mode shows diagnostics; completions adapt to mode
- **/var augmented assignment errors**: Invalid `@x = @y += @z` errors stop at the `+=` instead of earlier lines in LSP diagnostics
- **Exe block += evaluation**: `let @result = @a; @result += @b` concatenates arrays instead of replacing them
- `run` statements work inside `[...]` blocks for `/exe` and `/for` bodies
- Fixed `/run @value | cmd { ... }` parsing so `@value` becomes `with { stdin: ... }` (matches `/exe` RHS pipe sugar)
- **LSP: rc78 syntax support**: Semantic tokens for block syntax `[...]`, `let` keyword, `+=` augmented assignment, `while`/`done`/`continue`, `stream` directive, working directories `cmd:/path`, when semicolons
- **LSP bug fixes**: When block assignments ([#327](https://github.com/mlld-lang/mlld/issues/327)), pipe transform parity ([#328](https://github.com/mlld-lang/mlld/issues/328)), EOL comments in when ([#329](https://github.com/mlld-lang/mlld/issues/329)), variable interpolation in /run ([#330](https://github.com/mlld-lang/mlld/issues/330)), function execution in /run ([#331](https://github.com/mlld-lang/mlld/issues/331)), array/object value highlighting ([#332](https://github.com/mlld-lang/mlld/issues/332))
- **LSP debugging tools**: `npm run validate:tokens`, `npm run test:nvim-lsp <file>` for testing semantic highlighting
- **LSP tokenization fixes**: Negative char positions causing Neovim crashes, missing visitor registrations (field/numericField/arrayIndex/LetAssignment/ExeReturn), container object recursion in visitChildren(), ExecInvocation wrong token type.
## [2.0.0-rc77]
### Added
- **CLI `--structured` mode**: New `--structured` flag outputs JSON with effects, exports, stateWrites, and full security metadata for auditing and programmatic consumption
- **CLI `--inject` flag**: Runtime module injection via `--inject @module=value` or `--inject @module=@file.json`. Enables testing with mock data and dynamic context without temp files. Multiple `--inject` flags supported.
- **MCP static analysis**: `mlld mcp` now uses `analyzeModule()` for tool discovery instead of code execution, improving security by discovering tools without running arbitrary code
- **SDK execution modes**: `interpret(mode)` with four modes for different consumption patterns
- `document` (default): Returns plain string output
- `structured`: Returns `{ output, effects, exports, environment }` with security metadata on all effects
- `stream`: Returns `StreamExecution` handle with real-time event delivery (`.on()`, `.off()`, `.done()`, `.result()`, `.abort()`)
- `debug`: Returns `DebugResult` with AST, variables, ordered trace, and timing
- **Dynamic module injection**: `processMlld(script, { dynamicModules: {...} })` enables runtime context injection without filesystem I/O. All dynamic imports automatically labeled `src:dynamic` for guard enforcement. Enables multi-tenant applications (inject per-user/project context from database). Optional `dynamicModuleSource` parameter adds custom source labels (e.g., `src:user-upload`, `src:database`) for fine-grained guard policies distinguishing between trusted and untrusted dynamic data.
- **State write protocol**: `/output @value to "state://path"` captures state updates as structured data instead of writing to filesystem. State writes included in `StructuredResult.stateWrites` with security metadata.
- **SDK runtime execution**: `execute(filepath, payload, options)` provides file-based route execution with in-memory AST caching, state hydration (`@state`, `@payload`), timeout/cancellation, and full effects logging.
- **SDK analysis tools**: `analyzeModule(filepath)` extracts capabilities, imports, exports, guards, and security metadata without execution. Enables static analysis, capability checking, and module introspection.
- **Effect security metadata**: All effects in structured/stream/debug modes include `security` field with labels, taint tracking, and provenance for auditing and policy enforcement.
- **Execution events**: `ExecutionEmitter` bridges streaming infrastructure to SDK events (`stream:chunk`, `command:start/complete`, `effect`, `execution:complete`) for real-time monitoring.
- **Directory-based taint tracking**: File loads now include `dir:*` labels for all parent directories, enabling guards like `@input.ctx.taint.includes('dir:/tmp/uploads')` to prevent executing uploaded files.
### Changed
- **Security model streamlined**: `SecurityDescriptor` now uses `taint: DataLabel[]` (accumulated labels) instead of single `taintLevel` enum. Automatic labels added: `src:exec` (commands), `src:file` (file loads), `src:dynamic` (runtime injection), `dir:/path` (file directories).
- Effect handler now records effects when `mode: 'structured' | 'stream' | 'debug'`; default `document` mode skips recording for performance.
- **`mlld run` now uses `execute()`**: Run command leverages AST caching, metrics, and timeout support from SDK's `execute()`. New `--timeout` and `--debug` flags available.
### Fixed
- **Whitespace normalization** ([#396](https://github.com/mlld-lang/mlld/issues/396)): Introduced OutputIntent abstraction with collapsible breaks to eliminate extra blank lines. Newlines from document structure now collapse automatically, producing consistent output spacing.
- **Prettier dependency removed** ([#281](https://github.com/mlld-lang/mlld/issues/281)): Replaced Prettier with simple line-based normalizer. Eliminates hanging bug, removes JSON protection hacks, and improves performance (~0ms vs ~50ms). The `@md` transformer now normalizes output (strips trailing whitespace, collapses blank lines) rather than reformatting.
- Array slicing now supports variable interpolation in slice indices ([#457](https://github.com/mlld-lang/mlld/issues/457)). Previously `@arr[0:@limit]` would fail to parse; now `@arr[@start:@end]`, `@arr[0:@limit]`, and `@arr[@offset:]` all work as expected.
- Fixed issue where `/var @item = cmd {..}` would fail due to missing grammar pattern
- Pipeline effects (`output`, `show`, `append`, `log`) run through guard pre/post hooks. `op:output`/`op:show`/`op:append`/`op:log` guards block both directives and inline effects; guard retries on effects deny with a clear message.
## [2.0.0-rc76]
### Fixed
- Circular reference detection for executables ([#255](https://github.com/mlld-lang/mlld/issues/255)): mlld now detects when an executable calls itself recursively without a terminating condition and throws a clear `CircularReferenceError` instead of causing a stack overflow. This includes both direct recursion (`@f()` calling `@f()`) and mutual recursion (`@ping()` ↔ `@pong()`). Legitimate patterns like pipeline retries and builtin method calls are excluded from detection.
- Liberal import syntax: quoted module paths like `"@local/module"` and `"@base/file.mld"` now work alongside unquoted forms ([#300](https://github.com/mlld-lang/mlld/issues/300)). The interpreter detects resolver patterns in quoted strings and routes them correctly instead of treating them as variable interpolation.
- ProjectPathResolver now recognizes `.mld.md` and `.mlld.md` extensions as modules, fixing imports from `@base/...` paths
- SpecialVariablePath in grammar now stops at line boundaries, preventing path parsing from consuming content across newlines
## [2.0.0-rc75]
### Added
- Object spread syntax `{ ...@var, key: value }` for composing objects with left-to-right overrides; spreading non-objects now errors.
- Augmented assignment `@var += value` for local variable accumulation in when blocks. Supports arrays (concat), strings (append), and objects (shallow merge). Only works with local `let` bindings, maintaining global immutability.
### Fixed
- Improved error message for alligator field access inside XML/HTML tags - now detects pattern and suggests variable workaround
- Fixed `as` transform pattern for glob patterns - `<*.md> as "### <>.ctx.filename"` now correctly transforms each file instead of returning empty array
- `/show` directive now errors on multiple arguments instead of silently ignoring extras ([#370](https://github.com/mlld-lang/mlld/issues/370)). Use templates for multiple values: `/show \`@a @b\``
## [2.0.0-rc74]
### Fixed
- `run cmd {...}` syntax now works consistently in `/var` and `/exe` contexts, not just `/run` directives. Previously `/var @x = run cmd {echo "hi"}` and `/exe @f() = run cmd {echo "hi"}` would fail to parse. Both `run {...}` (implicit cmd) and `run cmd {...}` (explicit) are now supported everywhere for backwards compatibility.
## [2.0.0-rc73]
### Added
- `let` keyword for local variables in `/when` blocks: `let @x = value` creates block-scoped variables before conditions, enabling cleaner conditional logic without polluting outer scope
- `/run cmd {command}` syntax for shell commands, consistent with `cmd {..}` in other contexts. Bare `/run {command}` still works for backwards compatibility.
- AST selector wildcards ([#505](https://github.com/mlld-lang/mlld/issues/505)): `{ handle* }`, `{ *Validator }`, `{ *Request* }`, `{ get? }` for pattern-based symbol matching
- AST type filters: `{ *fn }`, `{ *var }`, `{ *class }`, `{ *interface }`, `{ *type }`, `{ *enum }`, `{ *struct }`, `{ *trait }`, `{ *module }`, `{ * }` to get all definitions of a specific type
- AST name listing: `{ ?? }`, `{ fn?? }`, `{ var?? }`, `{ class?? }` return string arrays of definition names instead of code
- Single file: returns plain string array for simple iteration
- Glob patterns: returns per-file structured results `[{ names: string[], file, relative, absolute }]` enabling queries like `/for @f in <**/*.py { class?? }> => show "@f.names.length classes in @f.relative"`
- Section listing for markdown: `# ??`, `# ##??`, `# ###??` return arrays of heading titles
- Single file: plain string array
- Glob patterns: per-file structured results `[{ names: string[], file, relative, absolute }]`
- Variable interpolation in AST selectors: `{ *@type }`, `{ @type?? }` for dynamic pattern construction
- Usage patterns with wildcards and type filters: `{ (handle*) }`, `{ (*fn) }` find functions that use matched symbols
- Validation: mixing content selectors with name-list selectors now throws clear error
- LSP/syntax highlighting: `/guard`, `/stream`, `/append`, `/export` directives; guard keywords (`before`, `after`, `always`, `allow`, `deny`, `retry`); `let`/`var` in when blocks; import types (`module`, `static`, `live`, `cached`, `local`); data labels; pipeline operators (`|`, `||`); type-checking methods (`.isArray()`, etc.); AST selector patterns
### Changed
- **BREAKING**: Variable assignments in `/when` actions now require explicit `var` prefix. Use `var @x = value` for outer-scope variables, `let @x = value` for block-local variables. Bare `@x = value` syntax now throws an educational error.
### Fixed
- Field access with pipes in `/show` now correctly extracts field values before piping ([#506](https://github.com/mlld-lang/mlld/issues/506)). Previously `@data.0.code | cmd {head -3}` would pipe the parent array instead of the code field value. Field access now happens before pipeline processing for both `VariableReference` and `VariableReferenceWithTail` node types.
- Export directive grammar now correctly distinguishes guards from variables ([#498](https://github.com/mlld-lang/mlld/issues/498)). Previously all exports were marked as `guardExport`, breaking `/export` for executables and variables. Now uses runtime guard registry check.
- `/export` directive now recognized by grammar context detection - added missing `export` keyword to `DirectiveKind` enum. Export filtering now works correctly for namespace imports.
- `/export { * }` wildcard syntax now parses correctly - added `*` as valid export identifier
- Module tests updated to use current `/export { name }` and `/exe @func() = \`...\`` syntax
- Documentation updated: `/export guard @name` changed to `/export { @guardName }`
- JSON field access in executables now requires explicit `.data` accessor (e.g., `@var.data.field`)
- Glob pattern test files renamed with unique prefixes to prevent virtual filesystem collisions
- Frontmatter access in glob results now uses `.ctx.fm.field` accessor
- Test expectations updated for current JSON formatting and blank line behavior
## [2.0.0-rc72]
### Added
- Type-checking builtin methods: `.isArray()`, `.isObject()`, `.isString()`, `.isNumber()`, `.isBoolean()`, `.isNull()`, `.isDefined()` return booleans for conditional logic ([#414](https://github.com/mlld-lang/mlld/issues/414)). Note: `.isDefined()` safely returns `false` for missing variables or fields without throwing.
### Fixed
- Method chaining after array access now works: `@msg.split("_")[0].toUpperCase()` ([#408](https://github.com/mlld-lang/mlld/issues/408))
- `mlld init` is now path-aware - selecting "llm/modules" while already in that directory no longer creates nested paths ([#453](https://github.com/mlld-lang/mlld/issues/453))
### Removed
- Grammar cleanup: Removed undocumented `when any` and `when all` modifiers. Use `&&` and `||` operators for AND/OR logic in conditions.
## [2.0.0-rc71]
### Fixed
- JSON escape sequences (`\n`, `\t`, etc.) now preserved when piping data through shell commands like `echo` ([#456](https://github.com/mlld-lang/mlld/issues/456)). Previously, escape sequence normalization in command executables would convert properly-escaped `\\n` back to actual newlines, corrupting JSON data.
- Pipeline synthetic source stage preserves StructuredValue wrappers so with-clause pipelines keep JSON arrays/objects intact through `/exe` when-expression actions ([#461](https://github.com/mlld-lang/mlld/issues/461)).
## [2.0.0-rc70]
### Added
- Streaming support: `stream` keyword, `/stream` directive, and `with { stream: true }` enable live chunk emission with progress sinks and executor streaming (shell, bash, node). Parallel groups stream concurrently and buffer results. Suppress with `--no-stream` or `MLLD_NO_STREAM`.
- Streaming UX MVP:
- Auto-parse NDJSON for `stream` execs (paths: message.content[].text/result/delta.text/completion/error.message).
- Live stdout for message text with spacing/dedupe; thinking/tool-use to stderr (`💭 text`, `🔧 name input=preview`); tool results suppressed for noise.
- Raw event visibility: `--show-json` (or `MLLD_SHOW_JSON=true`) mirrors NDJSON to stderr; `--append-json [file]` writes NDJSON to JSONL (default `YYYY-MM-DD-HH-MM-SS-stream.jsonl` when omitted).
- Streaming `/show ...` avoids double-print of streamed content.
### Security
**Guards**:
- Policy enforcement for data access and operations
- Syntax: `/guard <label> { allow/deny/retry }` with optional conditions
- Guards trigger on data labels (`secret`, `pii`) or operations (`op:run`, `op:exe`)
- Support `allow`, `deny`, and `retry` decisions
- Can transform data with `allow @transform(@input)`
- Fire before operations (input validation) or after operations (output validation)
**Expression Tracking**:
- Guards see security labels through all transformations (closes `@secret.trim()` bypass hole)
- Provenance preserved through: chained builtin methods, template interpolation, field access, iterators, pipelines, nested expressions
- Example: `@secret.trim().slice(0, 5)` preserves `secret` label through entire chain
- Guards fire at directive boundaries, exe invocations, and pipeline stages
**Guard Composition**:
- All guards execute in registration order (file top-to-bottom, imports flatten at position)
- Multiple guards compose with decision precedence: deny > retry > allow @value > allow
- Transform chaining: Guard N output → Guard N+1 input with full provenance tracking
- Guard history exposed via `@ctx.guard.trace/hints/reasons` for denied handlers
- Pipeline guard history via `@p.guards` tracks guard activity across all pipeline stages
- Deterministic IDs for unnamed guards: `<unnamed-guard-N>`
- Per-input guards (data labels) and per-operation guards (`op:run`, `op:exe`, etc.)
**Before/After Guards**:
- Guards fire before operations (input validation) or after operations (output validation)
- Syntax: `/guard @name before datalabel = when [...]` where TIMING is `before`, `after`, or `always`
- Syntactic sugar: `/guard @name for LABEL` is equivalent to `before` timing (explicit `before` recommended)
- Context: `@input` in before guards, `@output` in after guards, both available in denied handlers
- Execution order: before guards → operation → after guards
- Retries supported in pipeline context for both before and after guards
**Allow @value Transforms**:
- Guards transform inputs/outputs: `allow @redact(@input)` or `allow @sanitize(@output)`
- Transforms chain with metadata preservation (`guard:@name` appended to sources)
- Works in both before guards (input sanitization) and after guards (output sanitization)
- Cross-scope chaining: per-input guard transforms flow to per-operation guards
- Provenance tracking: labels union, taint maximum, sources accumulate
**Guard Overrides**:
- Per-operation control: `with { guards: { only: [...], except: [...], false } }`
- `guards: false` disables all guards (emits warning to stderr)
- `only: ["@guard"]` runs specified guards only (unnamed guards excluded)
- `except: ["@guard"]` skips named guards (unnamed guards still run)
- Conflict detection: throws error if both `only` and `except` specified
### Added
- StructuredValue `.ctx`/`.internal` surfaces power provenance, security, and behavior metadata
- `/append` directive and `| append` pipeline builtin for incremental file writes (JSONL/text) with shared `/output` source evaluation
- `@json.llm` transformer extracts JSON from LLM responses with code fences or embedded prose. Returns `false` when no JSON found.
- `@json.fromlist` transformer converts plain text lists (one item per line) to JSON arrays
- Chained builtin methods on variables: string methods (slice, substring, substr, replace, replaceAll, padStart, padEnd, repeat, split, join) and array methods (slice, concat, reverse, sort) work in chains like `@secret.trim().slice(0, 6)` with security labels preserved
- Structured-value helpers: added `keepStructured`/`keep` helper and `.keepStructured`/`.keep` field-access sugar to retain metadata/provenance without unwrapping content. Built-in `@keep`/`@keepStructured` executables allow helper-style usage in scripts.
- For loops accept dotted iteration variables and bind both the base element and its field (e.g., `for @item.path in @files`) with proper field access errors.
- For loop bodies can be `when [...]` across /for, /var, and /exe, using first-match semantics per iteration and feeding branch results into loop outputs.
- Alligator JSON ergonomics: `<*.json>` and `<*.jsonl>` auto-parse to StructuredValues (parsed `.data`, raw `.text`, `.ctx` preserved); use `.text` when raw strings are needed.
### Fixed
- Templates now correctly parse comparison operators like `<70%` and `< 70` instead of treating them as file references
- Inline `/for` loops in templates only trigger at line start (not mid-line)
- **When-expression pipelines**: `/exe … = when [...]` actions now accept `| append`, `| log`, `| output`, and `| show` stages without misparsing ternary expressions (fixes `slash/when/exe-when-expressions-operators`).
- Backtick and `::` templates handle XML-like tags identically
- Fixed false circular reference warnings when parallel tasks load the same file
- Inline pipeline effect detection now differentiates builtin `append` from user-defined commands, restoring stage execution for execs named `append`
- Alligator syntax in for expressions: `for @f in @files => <@f>` and property access like `for @f in @files => <@f>.fm.title` now work correctly
- Module content suppression during imports - imported module content no longer appears in stdout
- Shell pipe detection respects quoted strings - pipe characters inside quoted arguments no longer trigger pipe handling
- Transformer variant resolution in pipelines - `@json.fromlist`, `@json.llm`, `@json.loose`, and `@json.strict` work correctly in all pipeline contexts
- Alligator `.relative` resolves from inferred `@base` (or the script path when base is unavailable) so metadata matches project-root paths
- Comma in `when` condition lists now emits a targeted parse error instead of a generic /exe syntax failure
- Wrong parallel syntax order (`/for parallel 18` instead of `/for 18 parallel`) now shows helpful error with correct syntax examples
### Changed
- Braced commands require explicit `cmd { ... }`; bare `{ ... }` parses as structured data, pipelines accept inline value stages with structured output, and bare brace commands raise a targeted parse error
- Enhanced error message for `run sh` in `/exe` explains distinction between bare commands and shell scripts
- Shell commands now run from project root when `@base` is inferred, otherwise from script directory
- `/for` parallel syntax uses `parallel(cap, pacing)` instead of `(cap, pacing) parallel`. Old syntax still parses with a warning.
- Unified file loading uses StructuredValue metadata consistently: text files unwrap to strings by default, JSON/JSONL unwrap to parsed objects/arrays, `.ctx` carries file/URL metadata, `.keep` passes wrappers into JS/Node, and `MLLD_LOAD_JSON_RAW` is removed in favor of `.text` for raw access.
## [2.0.0-rc69]
### Fixed
- JS and Node executors treat expression-style blocks as implicit returns, so `/var` assignments and pipelines receive native objects/arrays and property access like `@repo.name` works without helper wrappers.
- Node shadow executor surfaces the underlying runtime error message while still cleaning up timers, restoring the `node-shadow-cleanup` regression coverage.
## [2.0.0-rc68]
### Fixed
- Template executables detect JSON-looking strings and wrap them as structured values, so downstream pipelines receive native objects instead of escaped text (#435).
- Foreach iteration normalizes stage outputs that stringify JSON and passes parsed arrays/objects forward, restoring the behaviour users expect from `| @json` inputs.
- `/show` array rendering unwraps structured elements to their `.data` view when possible, keeping canonical text intact for load-content metadata and structured JSON displays.
## [2.0.0-rc67]
### Fixed
- Pipelines sanitize JSON-like shell output by escaping control characters inside string literals, so `/run` stages that echo newline-bearing JSON feed structured data forward correctly.
## [2.0.0-rc64]
### Fixed
- **Alligator section parsing with "as" substring**: Fixed grammar bug where section names containing "as" (like "Gotchas", "Installation", "Basic Usage") were rejected
- Changed `AlligatorSectionChar` rule from `!"as"` to `!(" as")` to only exclude the `as` keyword when used for renaming
- Section syntax like `<file.md # Gotchas>` now works correctly
- Distinguishes between section names with "as" and the rename keyword: `<file.md # Section> as "New Title"`
- Added test coverage in `tests/cases/slash/show/alligator-section-as-substring/`
- Preserved structured pipeline outputs across chained executables by wrapping JSON-like strings returned from JS/Node stages, preventing downstream stages from receiving `[object Object]` text (#435).
- Updated run/exec structured handling and regression fixtures so batch/parallel pipelines, foreach separators, and retry fallbacks assert native arrays/objects instead of stringified JSON, closing the remaining gaps from #435 user scenarios.
## [2.0.0-rc63]
### Fixed
- Fixed local resolver to recognize all mlld extensions as modules when they contain directives. Previously only .mlld.md files were explicitly treated as modules, causing "Import target is not a module" errors when importing .mld files via custom resolver prefixes like @context/.
- Improved content type detection to parse file contents for mlld directives across all module extensions, maintaining backward compatibility for files with non-standard extensions that contain valid mlld code.
- Missing `--tag` on cli added
- Update docs to cover modules, registry, resolvers
### Added
- Batch and condensed pipeline stages now receive the structured wrapper instead of raw strings, so helpers can work with native arrays/objects without JSON.parse.
- **Custom tag support for publishing**: `mlld publish --tag <name>` allows publishing modules with custom version tags
- Publish with beta/alpha tags: `mlld publish module.mld --tag beta`
- Tag validation ensures alphanumeric + hyphens only, 2-50 character length
- Reserved tags (`latest`, `stable`) are rejected with clear error messages
- Users can import using custom tags: `/import { @helper } from @alice/utils@beta`
## [2.0.0-rc62]
### Added
- **Batch pipelines for collection expressions**: `for` and `foreach` now accept a trailing `=> |` pipeline that runs after iteration completes. The batch phase reuses standard pipeline syntax, applies to the gathered array, and may return arrays, scalars, or objects. Grammar attaches the pipeline to `ForExpression.meta.batchPipeline` and `ForeachCommandExpression`, and the interpreter processes the results via `processPipeline()` before emitting the final variable or display output.
### Notes
- Batch pipelines behave like condensed pipelines: each stage receives string input, so helpers that expect arrays should parse the string back to JSON. Currently parallel groups (`||`) share the same semantics but are not fully supported/tested.
## [2.0.0-rc61]
### Added
- **Loose JSON parsing modes**: `@json` now accepts relaxed JSON syntax (single quotes, trailing commas, comments) using JSON5, with explicit `@json.loose` and `@json.strict` variants for opting in or enforcing strict parsing. Error messages direct users to the loose mode when strict parsing fails.
### Fixed
- **Structured data handling in field access**: Fixed array operations on nested StructuredValue wrappers
- Field access now properly unwraps nested StructuredValue before array operations
- Fixes potential runtime errors with deeply nested structured data (e.g., `@nested[0]` where `@nested` is a wrapped array)
- Related to #435 structured data edge cases
- Fixed in `interpreter/utils/field-access.ts:477` and `:248`
- **Exec invocation stdin handling**: Fixed stdin coercion missing StructuredValue unwrapping
- Exec invocations now properly unwrap StructuredValue when preparing stdin data
- Aligns with run.ts stdin handling (same pattern as the golden standard)
- Prevents double-wrapping or incorrect stringification of structured values passed via stdin
- Related to #435 structured data edge cases
- Fixed in `interpreter/eval/exec-invocation.ts:49`
- **Shell interpolation of structured values**: Complex arrays/objects now survive shell argument quoting
- Shared `classifyShellValue` helper drives `/run` and `@exe` stdin/argument coercion
- Interpolation tracks both single- and double-quoted spans, avoiding `[object Object]` and broken quoting
- File-content fixtures confirm literal `$`, `` ` ``, and quotes reach the shell intact
- Covers regressions from #435 user scenario
- **Variable assignment with wrapped values**: Fixed String() conversions producing [object Object]
- Variable assignments now use `valueToString()` helper that checks for StructuredValue wrappers
- Uses `asText()` helper for StructuredValue wrappers instead of naive String() conversion
- Applies fix to 7 locations in var.ts where String() was used on complex values (lines 725, 751, 763, 773, 782, 820, 823)
- Variable type detection now properly unwraps StructuredValue before Array.isArray() checks (3 locations: lines 719, 745, 757)
- Related to #435 structured data edge cases
- Fixed in `interpreter/eval/var.ts`
## [2.0.0-rc60]
### Fixed
- **Shell command interpolation with nested arrays**: Fixed arrays of objects/arrays being converted to `[object Object]` in shell commands
- Shell command context (e.g., `echo @array`) now properly JSON-stringifies complex array elements
- Previously `String(object)` produced `[object Object]`, breaking data flow through shell executables
- Example: `/exe @func(e) = run { echo @e }` now correctly outputs JSON for nested arrays
- Fixes remaining edge case from #435 (https://github.com/mlld-lang/mlld/issues/435#issuecomment-3386904732)
- Addressed instances of old mlld.lock.json file expectations throughout codebase
## [2.0.0-rc59]
### Changed
- **CLI commands aligned with new config file naming**: Updated all CLI commands to reference the new dual-file configuration system
- `mlld-config.json`: User-editable project settings (dependencies, preferences, resolver configuration)
- `mlld-lock.json`: Auto-generated lock file (versions, hashes, sources)
- Replaced `mlld.lock.json` references throughout CLI commands and help text
- Commands updated: `setup`, `alias`, `run`, `init-module`
- Editor integrations updated: Neovim LSP, VS Code Language Server
- Backward compatibility maintained: LSP and editor tooling check for old `mlld.lock.json` as fallback
- All commands now use `ProjectConfig` abstraction
## [2.0.0-rc58]
### Fixed
- **Foreach with structured values**: `foreach` now unwraps StructuredValue arguments
- Previously failed with "got structured text" when array came from pipeline
- Example: `/var @chunked = @data | @chunk(2)` then `foreach @process(@chunked)` now works
- Aligns with JavaScript stages which already unwrap automatically
## [2.0.0-rc57]
### Added
- **MCP server**: `mlld mcp` serves exported `/exe` functions as MCP tools
- Exposes functions over JSON-RPC stdio transport
- Default discovery: `llm/mcp/` directory when no path specified
- Config modules: `--config module.mld.md` exports `@config = { tools?, env? }`
- Environment overrides: `--env KEY=VAL` (MLLD_ prefix required)
- Tool filtering: `--tools tool1,tool2` or via config
- Duplicate tool names halt with error showing conflicting sources
- Example: `/exe @greet(name) = js { return \`Hello ${name}\`; }` becomes `greet` tool
### Changed
- **Data flow between stages**: Native types preserved throughout pipelines
- Loaders return parsed data: `<data.json>` yields object, not JSON string
- Pipeline stages pass arrays/objects directly: `@data | @process` receives native type
- JavaScript functions receive parsed values without `JSON.parse()`
- Templates and output convert to text automatically
- Fixes #435
### Breaking
- Remove `JSON.parse()` calls in JavaScript stages - will fail on already-parsed data
- Use `.text` to access stringified data, `.data` to get structured data in string context
- Pipelines expecting JSON strings will receive objects/arrays instead
## [2.0.0-rc56]
### Added
- **Import Types System**: Control how modules and resources are resolved
- `module` imports: Pre-installed registry modules (offline after install)
- `static` imports: Content embedded at parse time (zero runtime cost)
- `live` imports: Always fresh data (fetched every execution)
- `cached(TTL)` imports: Smart caching with time limits (5m, 1h, 7d, etc.)
- `local` imports: Direct access to development modules in `llm/modules/`
- Example: `/import module { api } from @corp/tools`, `/import cached(1h) <https://api.example.com> as @data`
- **Module management**:
- `mlld install @author/module`: Install modules from public registry
- `mlld update`: Update modules to latest compatible versions
- `mlld outdated`: Check for available updates
- `mlld ls`: View installed modules with status and sizes
- Registry integration with CDN-distributed module catalog
- **Configuration Files**:
- `mlld-config.json`: Your project settings (dependencies, preferences)
- `mlld-lock.json`: Auto-generated locks (versions, hashes, sources)
- **Simplified Development Workflow**:
- Use `/import local { helper } from @author/module` to access modules in `llm/modules/` using published name (if you are @author or can publish to private @author registry)
- Useful for iterating on modules before publishing
### Changed
- Import syntax now requires `@` prefix on imported names: `/import { @helper } from module`
- Module publishing requires explicit `/export { ... }` manifests
- Import failures now stop execution (exit code 1) instead of continuing
- Smart import type inference based on source patterns
- Pipelines support leading `||` operator for immediate parallel execution: `/var @result = || @a() || @b() || @c()` runs all three functions concurrently
- Leading parallel syntax works in `/var`, `/run`, and `/exe` definitions
- Pipeline concurrency controls: `(n, wait)` shorthand syntax and `with { parallel: n, delay: wait }` for caps and pacing
### Fixed
- Module installation fetches from real registry instead of placeholders
- Version resolution respects "latest" tags and semantic versioning
- Module integrity verified with SHA-256 hashes
## [2.0.0-rc55]
### Added
- Stdin support for `/run` directive and `/exe` definitions:
- New syntax: `/run { command } with { stdin: @variable }` passes data directly via stdin without shell escaping
- Pipe sugar: `/run @data | { command }` normalizes to `with { stdin: @data }` for cleaner syntax
- Works in executable definitions: `/exe @func(data) = run { command } with { stdin: @data }`
- Pipe sugar in executables: `/exe @func(data) = run @data | { command }`
- Eliminates JSON double-stringification when passing structured data to commands like `jq`, `cat`, etc.
- Preserves shell safety while enabling proper JSON/CSV/XML data flow through pipelines
- JSON data access pattern for JavaScript functions (addresses #428):
- `.data` and `.json` accessors parse JSON strings during variable evaluation before passing to functions
- `.text` and `.content` accessors preserve original string content
- Eliminates need for manual `JSON.parse()` calls in JavaScript functions
- Works consistently across files, variables, and command output
- Example: `/var @json = '{"items": []}'; /run @process(@json.data)` passes parsed array to function
- Native mlld functions in pipelines:
- `/exe` functions using `for` and `foreach` constructs now work as pipeline sta