res-playbook — multi-language notebook runner #18

Open
opened 2026-08-03 01:37:12 +00:00 by agent · 3 comments
Member

res-playbook — multi-language notebook runner

A notebook-style utility for extra/. Cells in a markdown file, executed via language runtimes, sharing state through three memory tiers. Functional parity with Jupyter notebooks is the long-term goal; v1 starts with Lua embedded and shell external.

Repo: extra/ (new binary cmd/res-playbook, all internal packages under internal/playbook/)


file format

The notebook is a standard markdown file with TOML frontmatter and tagged runtime code blocks:

---
title = "Sales Analysis"
author = "ops"
languages = ["lua", "shell"]
---

# Load Data

This is regular markdown — not executed.

-#runtime
```lua
data = res.readFile("sales.csv")
summary = res.groupBy(data, "region")

Summarize

-#runtime

echo "Total rows: $(wc -l < sales.csv)"

Regular code blocks (without -#runtime) are rendered as documentation, not executed.


**Key format rules:**
- TOML frontmatter between `---` delimiters
- `-#runtime` on its own line immediately before the opening ``` fence
- Language tag follows the opening ``` (e.g. ` ```lua `, ` ```shell `)
- Code blocks without `-#runtime` prefix are documentation only
- Can promote/demote code blocks by adding/removing `-#runtime`
- Notebook save is in scope — format is just markdown, saving is writing back the same file

**Frontmatter schema (v1):**
```toml
title = "Notebook Title"
author = "ops"
languages = ["lua", "shell", "python"]  # languages needed by this notebook

The languages list is declarative. The playbook checks if runtimes are available for each. Missing runtimes get a warning: "hey! we cannot run LANG, you may experience reduced functionality or none at all."


language runtime model

Languages are provided by runtimes. A runtime declares itself with an ID, version, and supported languages. The playbook doesn't care whether a runtime is embedded or external — it just needs a runtime for each language.

built-in runtimes

  • Lua — embedded via gopher-lua (always available, no external process)

external runtimes

Spawned as subprocesses, communicate via IPC over UDS.

runtime naming convention

Distributed runtimes follow the naming pattern: res-playbook-runtime-LANG

  • res-playbook-runtime-shell
  • res-playbook-runtime-python
  • res-playbook-runtime-go

auto-discovery

Runtimes named res-playbook-runtime-* are auto-discovered:

  1. Playbook scans PATH for executables matching res-playbook-runtime-*
  2. Each discovered runtime is spawned and connects to the IPC socket
  3. Runtime sends declare message within 5 seconds
  4. If no declare within 5 seconds, playbook kills the process
  5. Runtime is now registered as handling its declared languages

manual configuration

Runtimes that don't follow the naming convention can be configured in ~/.config/residual/playbook.toml:

[runtimes.go]
command = "userplaybook-go --ipc={{IPCSOCKET}}"

[runtimes.rust]
command = "rust-playbook --socket={{IPCSOCKET}} --dir={{WORKSPACE}}"

Template variables (direct string replacement):

  • {{IPCSOCKET}} — absolute path to the IPC socket
  • {{WORKSPACE}} — absolute path to the directory containing the notebook (defaults to CWD)

Manual runtimes can also be added from within the program (TUI or CLI), which updates the config file.

runtime declaration handshake

  1. Playbook spawns external runtime process
  2. Runtime connects to IPC socket
  3. Runtime sends declare message:
    {
      "type": "declare",
      "payload": {
        "id": "res-playbook-python",
        "version": "0.1.0",
        "languages": ["python"]
      }
    }
    
  4. If no declare within 5 seconds, playbook kills the process
  5. Runtime is registered for its declared languages

A single runtime can declare support for multiple languages (monolith runtime).

runtime resolution

  1. Check if a runtime is registered for the language
  2. If multiple runtimes, prefer external if prefer_external = true in config
  3. Fall back to embedded if available
  4. If no runtime, warn user and skip cells of that language

Config:

[preferences]
prefer_external = false  # if true, use external runtime even if embedded exists

memory model

Three distinct layers:

Layer Mechanism Scope
lang-mem Language runtime's own variables Per-session (persistent VM)
cross-lang-mem Executor queries runtimes for their variables, injects into downstream cells Across cells, automatic
shared-mem Explicit KV store via playbook.set()/playbook.get() Across cells, explicit

shared-mem clarification

The playbook.set()/playbook.get() functions store data in the playbook program's memory, not in the runtime. This is for:

  • Runtimes that can't expose their variables (e.g., shell has no named variables)
  • Explicit data passing between languages when cross-mem isn't sufficient
  • Persistent storage across cell re-runs

Runtimes that DO support cross-mem (like Lua with globals) don't need shared-mem for variable passing — the executor queries them directly via ListVars().

cross-lang-mem flow

  1. Lua cell runs → x = 5
  2. Executor queries Lua runtime: ListVars(){x: 5}
  3. Executor stores in cross-mem snapshot
  4. Shell cell runs → executor injects cross-mem as environment variables
  5. Shell can read $x directly

shared-mem flow

  1. Lua cell: playbook.set("mydata", {1,2,3})
  2. Helper library sends to playbook program via socket
  3. Program stores in shared-mem
  4. Shell cell: playbook get mydata → reads from shared-mem

lang-mem flow

  1. Lua cell: x = [1,2,3]
  2. Lives in Lua VM's global state
  3. Accessible only within Lua cells (same session)
  4. Queried by executor for cross-mem if needed

Lua specifics

  • Globals persist across cells (same session)
  • Locals are chunk-scoped — you can't declare a local in cell 1 and use it in cell 2
  • This is a feature: locals exist only for the running chunk

output model

Each cell captures structured output. The Result type carries a list of OutputEntry values — one per output event during execution.

type Result struct {
    Stdout string           // raw stdout for log files
    Stderr string           // raw stderr for log files
    Vars   map[string]VarInfo
    Output []OutputEntry    // structured output for TUI rendering
}

type OutputEntry struct {
    Type string          // "text", "table", "json", "sql", "image", etc.
    MIME string          // MIME type for future use
    Data json.RawMessage // type-specific data
}

v1 behavior

  • Runtimes capture stdout/stderr
  • Output contains a single text entry with stdout
  • TUI renders text entries as plain text

stdout vs playbook.log()

stdout is the primary output source. Runtimes that support stdout capture intercept print() / echo() / etc. and route it to OutputEntry{Type: "text"}.

playbook.log() is a fallback for runtimes that can't intercept stdout (e.g., shell). It explicitly creates a text OutputEntry from within the runtime's helper library.

For embedded runtimes like Lua, print() is overridden to capture output directly — no need for playbook.log().

future expansion

Runtimes can return structured output:

  • sql{"columns": [...], "rows": [[...]]} → rendered as a table
  • json{"data": {...}} → syntax-highlighted JSON
  • table{"headers": [...], "rows": [[...]]} → formatted table
  • image → base64 PNG → terminal image protocol

TUI renderer

type OutputRenderer interface {
    Render(entry OutputEntry, width int) string
}

Each output type gets its own renderer. The TUI dispatches by OutputEntry.Type.

Cell output accumulation

Each cell accumulates a list of OutputEntry values as it runs. Multiple output entries per cell are supported (e.g., print() calls + final return value). The output pane is scrollable if content exceeds visible area.


IPC library

The IPC library stays generic for now. Program-specific wrappers live in internal/playbook/runtime/.

Core library (core/lib/ipc/):

  • Message{Type, Payload, ID} — wire format
  • Server — UDS listener with handler dispatch
  • Client — UDS connector with Call/Notify

Program-specific wrappers (internal/playbook/runtime/):

  • runtime.Client — wraps ipc.Client, adds declare handshake, execute/list_vars helpers
  • runtime.Server — wraps ipc.Server, adds runtime handler registration
  • runtime.Protocol — playbook-specific message types and payloads

IPC simplifications are deferred to a separate issue.


execution model

Two modes sharing the same engine:

  1. Sequential (batch): res-playbook run notebook.md — execute all runtime cells top to bottom, log output, exit
  2. Interactive (TUI): res-playbook notebook.md — open TUI, navigate cells, run on demand, inspect variables

Cell isolation: Cells share a persistent VM (same language session carries state). Unless marked isolate: true in metadata (future feature).

Error handling:

  • Sequential: error stops execution, logs error with cell reference, exits non-zero
  • TUI: just that cell fails, error shown in output pane

helper library (playbook stdlib)

Each language gets a helper library that talks to the playbook program via the runtime.

Lua:

playbook.set("key", value)      -- write to shared-mem (program memory)
playbook.get("key")             -- read from shared-mem
playbook.list()                 -- list all shared-mem keys
playbook.var(name)              -- read cross-mem variable
playbook.log(message)           -- fallback: explicitly create text OutputEntry

Shell:

playbook set key value
playbook get key
playbook list
playbook var name
playbook log "message"

CLI mode

# one-shot: run all cells, log output
res-playbook run notebook.md

# output goes to a log file
res-playbook run notebook.md --log output.log

# or stdout
res-playbook run notebook.md --stdout

# explicit save
res-playbook save notebook.md

TUI mode

# interactive
res-playbook notebook.md

Layout:

┌─ Cells ──────────────────────────────────────────────┐
│ > [lua] Load Data           ✓ 0.12s                   │
│   [lua] Process             ✓ 0.03s                   │
│   [sh] Output              ✗ failed                   │
│   [py] Final Analysis      ○ not run                  │
├─ Output ─────────────────────────────────────────────┤
│ Loaded 1500 rows                                    │
│ Regions: north, south, east, west                   │
├─ Memory ────────────────────────────────────────────┤
│ lang: x=5, data=[1,2,3]                             │
│ cross: x=5, y=10                                    │
│ shared: mydata={1,2,3}                              │
└──────────────────────────────────────────────────────┘

Keybindings (Ctrl modkey, per residual convention):

Key Action
Ctrl+R Run selected cell
Ctrl+Shift+R Run all cells
Ctrl+↑/↓ Navigate cells
Ctrl+I Toggle memory inspector
Ctrl+S Save notebook
Ctrl+Q Quit

file layout

All playbook packages live under internal/playbook/.

extra/
├── cmd/res-playbook/
│   └── main.go                    # CLI entry: run vs interactive
├── internal/
│   └── playbook/
│       ├── notebook/
│       │   ├── notebook.go        # Notebook type, TOML frontmatter, markdown parser
│       │   └── cell.go            # Cell type, status, output
│       ├── store/
│       │   ├── store.go           # shared-mem KV store (program memory)
│       │   ├── value.go           # Value type, serialization (JSON)
│       │   └── crossmem.go        # cross-lang-mem snapshot manager
│       ├── executor/
│       │   ├── executor.go        # cell orchestration, memory routing
│       │   ├── runner.go          # sequential/individual run modes
│       │   └── context.go         # execution context passed to runtimes
│       ├── runtime/
│       │   ├── protocol.go        # message types, serialize/deserialize
│       │   ├── runtime.go         # Runtime interface + registry
│       │   ├── embedded.go        # embedded runtime adapter (Lua)
│       │   ├── external.go        # external subprocess adapter
│       │   ├── client.go          # IPC client wrapper (declare handshake)
│       │   ├── discovery.go       # auto-discovery of res-playbook-runtime-* binaries
│       │   └── lua/
│       │       ├── lua.go         # embedded Lua implementation
│       │       └── helpers.go     # playbook.set/get for Lua
│       └── tui/
│           ├── tui.go             # bubbletea model
│           ├── cellview.go        # cell list + output
│           ├── varspanel.go       # memory inspector (lang/cross/shared)
│           └── inspector.go       # inspect individual variables

implementation steps

phase 1: core types and parser

  1. Define Cell, Notebook, Metadata types in internal/playbook/notebook/
  2. Implement TOML frontmatter parsing (use github.com/BurntSushi/toml — already in go.mod)
  3. Implement markdown parser — split on fences, extract -#runtime tags, language tags, body, preceding markdown as cell name/description
  4. Define Value type with JSON serialization in internal/playbook/store/
  5. Define VariableStore (shared-mem) and CrossMem (cross-lang-mem) in internal/playbook/store/
  6. Unit tests for parser and store

phase 2: language runtime layer

  1. Define Runtime interface in internal/playbook/runtime/:
    Execute(ctx, code, env) → Result, ListVars(ctx) → map[string]VarInfo
  2. Implement RuntimeRegistry — tracks available runtimes, resolves by language
  3. Implement embedded Lua adapter wrapping core/lib/script:
    • Engine for persistent sessions (same-language cells share state)
    • RegisterAddon("playbook", loader) for the helper library
  4. Implement Lua helper library: playbook.set(), playbook.get(), playbook.list(), playbook.log()
  5. Implement external adapter wrapping core/lib/ipc:
    • runtime.Client wraps ipc.Client, adds declare handshake
    • runtime.Server wraps ipc.Server, adds runtime handler registration
    • Playbook-specific payloads: ExecuteRequest, Result, VarInfo
  6. Implement auto-discovery: scan PATH for res-playbook-runtime-* binaries
  7. Unit tests

phase 3: executor

  1. Implement Executor — orchestrates cell execution, manages runtimes, routes memory
  2. Implement Runner — sequential batch mode
  3. Wire executor to CLI (res-playbook run)
  4. Test: run a Lua-only notebook end-to-end

phase 4: TUI

  1. Implement bubbletea model in internal/playbook/tui/
  2. Cell list view with status indicators (reuse tui/list from core/lib)
  3. Output pane with OutputRenderer dispatch by OutputEntry.Type
  4. Memory inspector panel (lang/cross/shared tabs)
  5. Keybindings: Ctrl+R, Ctrl+Shift+R, Ctrl+↑/↓, Ctrl+I, Ctrl+S, Ctrl+Q
  6. Wire TUI to CLI (res-playbook notebook.md)

phase 5: shell adapter + polish

  1. Implement ExternalAdapter for shell subprocess
  2. Shell helper library (playbook command)
  3. Log file output for CLI mode
  4. Error reporting with cell references
  5. Notebook save (Ctrl+S + CLI)
  6. Documentation update in docs/extra/extra.md

scope boundaries

In scope for v1:

  • Markdown notebook parser with TOML frontmatter
  • -#runtime tag system for code blocks
  • Structured output model (v1: text only; framework for future types)
  • Variable store (shared-mem)
  • Cross-lang-mem (basic)
  • Embedded Lua server
  • Shell adapter (external)
  • Auto-discovery of res-playbook-runtime-* binaries
  • Manual runtime configuration with {{IPCSOCKET}} and {{WORKSPACE}} template variables
  • CLI one-shot runner
  • TUI interactive mode
  • Variable inspector
  • Helper library for Lua + Shell
  • Notebook save

Out of scope for v1:

  • Python/Go/Rust servers (use external runtimes)
  • Rich output renderers (images, SQL tables, JSON) — framework only
  • Plugin system
  • External Lua server (embedded is sufficient for v1)
  • isolate: true cells
  • IPC library simplifications (separate issue)

dependency notes

  • gopher-lua is already in extra/go.mod
  • bubbletea + lipgloss already in go.mod
  • github.com/BurntSushi/toml already in go.mod (indirect)
  • No new external dependencies required for v1

verification

After each phase:

  1. go build ./cmd/res-playbook/...
  2. go test ./internal/playbook/...
  3. Manual test: create a test notebook, run it

Final verification (full extra/ suite):

  1. go build ./... from extra/
  2. go test ./... from extra/
  3. Manual test: Lua notebook with cross-mem + shared-mem
  4. Manual test: Shell notebook
  5. Manual test: TUI interactive mode
  6. Manual test: auto-discovery of a res-playbook-runtime-* binary
## res-playbook — multi-language notebook runner A notebook-style utility for `extra/`. Cells in a markdown file, executed via language runtimes, sharing state through three memory tiers. Functional parity with Jupyter notebooks is the long-term goal; v1 starts with Lua embedded and shell external. **Repo:** `extra/` (new binary `cmd/res-playbook`, all internal packages under `internal/playbook/`) --- ## file format The notebook is a standard markdown file with TOML frontmatter and tagged runtime code blocks: ```markdown --- title = "Sales Analysis" author = "ops" languages = ["lua", "shell"] --- # Load Data This is regular markdown — not executed. -#runtime ```lua data = res.readFile("sales.csv") summary = res.groupBy(data, "region") ``` # Summarize -#runtime ```shell echo "Total rows: $(wc -l < sales.csv)" ``` Regular code blocks (without `-#runtime`) are rendered as documentation, not executed. ``` **Key format rules:** - TOML frontmatter between `---` delimiters - `-#runtime` on its own line immediately before the opening ``` fence - Language tag follows the opening ``` (e.g. ` ```lua `, ` ```shell `) - Code blocks without `-#runtime` prefix are documentation only - Can promote/demote code blocks by adding/removing `-#runtime` - Notebook save is in scope — format is just markdown, saving is writing back the same file **Frontmatter schema (v1):** ```toml title = "Notebook Title" author = "ops" languages = ["lua", "shell", "python"] # languages needed by this notebook ``` The `languages` list is declarative. The playbook checks if runtimes are available for each. Missing runtimes get a warning: "hey! we cannot run LANG, you may experience reduced functionality or none at all." --- ## language runtime model Languages are provided by **runtimes**. A runtime declares itself with an ID, version, and supported languages. The playbook doesn't care whether a runtime is embedded or external — it just needs a runtime for each language. ### built-in runtimes - **Lua** — embedded via `gopher-lua` (always available, no external process) ### external runtimes Spawned as subprocesses, communicate via IPC over UDS. ### runtime naming convention Distributed runtimes follow the naming pattern: `res-playbook-runtime-LANG` - `res-playbook-runtime-shell` - `res-playbook-runtime-python` - `res-playbook-runtime-go` ### auto-discovery Runtimes named `res-playbook-runtime-*` are auto-discovered: 1. Playbook scans PATH for executables matching `res-playbook-runtime-*` 2. Each discovered runtime is spawned and connects to the IPC socket 3. Runtime sends `declare` message within 5 seconds 4. If no declare within 5 seconds, playbook kills the process 5. Runtime is now registered as handling its declared languages ### manual configuration Runtimes that don't follow the naming convention can be configured in `~/.config/residual/playbook.toml`: ```toml [runtimes.go] command = "userplaybook-go --ipc={{IPCSOCKET}}" [runtimes.rust] command = "rust-playbook --socket={{IPCSOCKET}} --dir={{WORKSPACE}}" ``` **Template variables (direct string replacement):** - `{{IPCSOCKET}}` — absolute path to the IPC socket - `{{WORKSPACE}}` — absolute path to the directory containing the notebook (defaults to CWD) Manual runtimes can also be added from within the program (TUI or CLI), which updates the config file. ### runtime declaration handshake 1. Playbook spawns external runtime process 2. Runtime connects to IPC socket 3. Runtime sends `declare` message: ```json { "type": "declare", "payload": { "id": "res-playbook-python", "version": "0.1.0", "languages": ["python"] } } ``` 4. If no declare within 5 seconds, playbook kills the process 5. Runtime is registered for its declared languages A single runtime can declare support for multiple languages (monolith runtime). ### runtime resolution 1. Check if a runtime is registered for the language 2. If multiple runtimes, prefer external if `prefer_external = true` in config 3. Fall back to embedded if available 4. If no runtime, warn user and skip cells of that language **Config:** ```toml [preferences] prefer_external = false # if true, use external runtime even if embedded exists ``` --- ## memory model Three distinct layers: | Layer | Mechanism | Scope | |-------|-----------|-------| | **lang-mem** | Language runtime's own variables | Per-session (persistent VM) | | **cross-lang-mem** | Executor queries runtimes for their variables, injects into downstream cells | Across cells, automatic | | **shared-mem** | Explicit KV store via `playbook.set()`/`playbook.get()` | Across cells, explicit | ### shared-mem clarification The `playbook.set()`/`playbook.get()` functions store data in the **playbook program's memory**, not in the runtime. This is for: - Runtimes that can't expose their variables (e.g., shell has no named variables) - Explicit data passing between languages when cross-mem isn't sufficient - Persistent storage across cell re-runs Runtimes that DO support cross-mem (like Lua with globals) don't need shared-mem for variable passing — the executor queries them directly via `ListVars()`. ### cross-lang-mem flow 1. Lua cell runs → `x = 5` 2. Executor queries Lua runtime: `ListVars()` → `{x: 5}` 3. Executor stores in cross-mem snapshot 4. Shell cell runs → executor injects cross-mem as environment variables 5. Shell can read `$x` directly ### shared-mem flow 1. Lua cell: `playbook.set("mydata", {1,2,3})` 2. Helper library sends to playbook program via socket 3. Program stores in shared-mem 4. Shell cell: `playbook get mydata` → reads from shared-mem ### lang-mem flow 1. Lua cell: `x = [1,2,3]` 2. Lives in Lua VM's global state 3. Accessible only within Lua cells (same session) 4. Queried by executor for cross-mem if needed ### Lua specifics - Globals persist across cells (same session) - Locals are chunk-scoped — you can't declare a local in cell 1 and use it in cell 2 - This is a feature: locals exist only for the running chunk --- ## output model Each cell captures structured output. The `Result` type carries a list of `OutputEntry` values — one per output event during execution. ```go type Result struct { Stdout string // raw stdout for log files Stderr string // raw stderr for log files Vars map[string]VarInfo Output []OutputEntry // structured output for TUI rendering } type OutputEntry struct { Type string // "text", "table", "json", "sql", "image", etc. MIME string // MIME type for future use Data json.RawMessage // type-specific data } ``` ### v1 behavior - Runtimes capture stdout/stderr - `Output` contains a single `text` entry with stdout - TUI renders text entries as plain text ### stdout vs playbook.log() **stdout is the primary output source.** Runtimes that support stdout capture intercept `print()` / `echo()` / etc. and route it to `OutputEntry{Type: "text"}`. `playbook.log()` is a fallback for runtimes that can't intercept stdout (e.g., shell). It explicitly creates a `text` OutputEntry from within the runtime's helper library. For embedded runtimes like Lua, `print()` is overridden to capture output directly — no need for `playbook.log()`. ### future expansion Runtimes can return structured output: - `sql` → `{"columns": [...], "rows": [[...]]}` → rendered as a table - `json` → `{"data": {...}}` → syntax-highlighted JSON - `table` → `{"headers": [...], "rows": [[...]]}` → formatted table - `image` → base64 PNG → terminal image protocol ### TUI renderer ```go type OutputRenderer interface { Render(entry OutputEntry, width int) string } ``` Each output type gets its own renderer. The TUI dispatches by `OutputEntry.Type`. ### Cell output accumulation Each cell accumulates a list of `OutputEntry` values as it runs. Multiple output entries per cell are supported (e.g., print() calls + final return value). The output pane is scrollable if content exceeds visible area. --- ## IPC library The IPC library stays generic for now. Program-specific wrappers live in `internal/playbook/runtime/`. **Core library (`core/lib/ipc/`):** - `Message{Type, Payload, ID}` — wire format - `Server` — UDS listener with handler dispatch - `Client` — UDS connector with Call/Notify **Program-specific wrappers (`internal/playbook/runtime/`):** - `runtime.Client` — wraps `ipc.Client`, adds declare handshake, execute/list_vars helpers - `runtime.Server` — wraps `ipc.Server`, adds runtime handler registration - `runtime.Protocol` — playbook-specific message types and payloads IPC simplifications are deferred to a separate issue. --- ## execution model Two modes sharing the same engine: 1. **Sequential (batch):** `res-playbook run notebook.md` — execute all runtime cells top to bottom, log output, exit 2. **Interactive (TUI):** `res-playbook notebook.md` — open TUI, navigate cells, run on demand, inspect variables **Cell isolation:** Cells share a persistent VM (same language session carries state). Unless marked `isolate: true` in metadata (future feature). **Error handling:** - Sequential: error stops execution, logs error with cell reference, exits non-zero - TUI: just that cell fails, error shown in output pane --- ## helper library (playbook stdlib) Each language gets a helper library that talks to the playbook program via the runtime. **Lua:** ```lua playbook.set("key", value) -- write to shared-mem (program memory) playbook.get("key") -- read from shared-mem playbook.list() -- list all shared-mem keys playbook.var(name) -- read cross-mem variable playbook.log(message) -- fallback: explicitly create text OutputEntry ``` **Shell:** ```bash playbook set key value playbook get key playbook list playbook var name playbook log "message" ``` --- ## CLI mode ```bash # one-shot: run all cells, log output res-playbook run notebook.md # output goes to a log file res-playbook run notebook.md --log output.log # or stdout res-playbook run notebook.md --stdout # explicit save res-playbook save notebook.md ``` --- ## TUI mode ```bash # interactive res-playbook notebook.md ``` Layout: ``` ┌─ Cells ──────────────────────────────────────────────┐ │ > [lua] Load Data ✓ 0.12s │ │ [lua] Process ✓ 0.03s │ │ [sh] Output ✗ failed │ │ [py] Final Analysis ○ not run │ ├─ Output ─────────────────────────────────────────────┤ │ Loaded 1500 rows │ │ Regions: north, south, east, west │ ├─ Memory ────────────────────────────────────────────┤ │ lang: x=5, data=[1,2,3] │ │ cross: x=5, y=10 │ │ shared: mydata={1,2,3} │ └──────────────────────────────────────────────────────┘ ``` Keybindings (Ctrl modkey, per residual convention): | Key | Action | |-----|--------| | `Ctrl+R` | Run selected cell | | `Ctrl+Shift+R` | Run all cells | | `Ctrl+↑/↓` | Navigate cells | | `Ctrl+I` | Toggle memory inspector | | `Ctrl+S` | Save notebook | | `Ctrl+Q` | Quit | --- ## file layout All playbook packages live under `internal/playbook/`. ``` extra/ ├── cmd/res-playbook/ │ └── main.go # CLI entry: run vs interactive ├── internal/ │ └── playbook/ │ ├── notebook/ │ │ ├── notebook.go # Notebook type, TOML frontmatter, markdown parser │ │ └── cell.go # Cell type, status, output │ ├── store/ │ │ ├── store.go # shared-mem KV store (program memory) │ │ ├── value.go # Value type, serialization (JSON) │ │ └── crossmem.go # cross-lang-mem snapshot manager │ ├── executor/ │ │ ├── executor.go # cell orchestration, memory routing │ │ ├── runner.go # sequential/individual run modes │ │ └── context.go # execution context passed to runtimes │ ├── runtime/ │ │ ├── protocol.go # message types, serialize/deserialize │ │ ├── runtime.go # Runtime interface + registry │ │ ├── embedded.go # embedded runtime adapter (Lua) │ │ ├── external.go # external subprocess adapter │ │ ├── client.go # IPC client wrapper (declare handshake) │ │ ├── discovery.go # auto-discovery of res-playbook-runtime-* binaries │ │ └── lua/ │ │ ├── lua.go # embedded Lua implementation │ │ └── helpers.go # playbook.set/get for Lua │ └── tui/ │ ├── tui.go # bubbletea model │ ├── cellview.go # cell list + output │ ├── varspanel.go # memory inspector (lang/cross/shared) │ └── inspector.go # inspect individual variables ``` --- ## implementation steps ### phase 1: core types and parser 1. Define `Cell`, `Notebook`, `Metadata` types in `internal/playbook/notebook/` 2. Implement TOML frontmatter parsing (use `github.com/BurntSushi/toml` — already in go.mod) 3. Implement markdown parser — split on fences, extract `-#runtime` tags, language tags, body, preceding markdown as cell name/description 4. Define `Value` type with JSON serialization in `internal/playbook/store/` 5. Define `VariableStore` (shared-mem) and `CrossMem` (cross-lang-mem) in `internal/playbook/store/` 6. Unit tests for parser and store ### phase 2: language runtime layer 1. Define `Runtime` interface in `internal/playbook/runtime/`: `Execute(ctx, code, env) → Result`, `ListVars(ctx) → map[string]VarInfo` 2. Implement `RuntimeRegistry` — tracks available runtimes, resolves by language 3. Implement embedded Lua adapter wrapping `core/lib/script`: - `Engine` for persistent sessions (same-language cells share state) - `RegisterAddon("playbook", loader)` for the helper library 4. Implement Lua helper library: `playbook.set()`, `playbook.get()`, `playbook.list()`, `playbook.log()` 5. Implement external adapter wrapping `core/lib/ipc`: - `runtime.Client` wraps `ipc.Client`, adds declare handshake - `runtime.Server` wraps `ipc.Server`, adds runtime handler registration - Playbook-specific payloads: `ExecuteRequest`, `Result`, `VarInfo` 6. Implement auto-discovery: scan PATH for `res-playbook-runtime-*` binaries 7. Unit tests ### phase 3: executor 1. Implement `Executor` — orchestrates cell execution, manages runtimes, routes memory 2. Implement `Runner` — sequential batch mode 3. Wire executor to CLI (`res-playbook run`) 4. Test: run a Lua-only notebook end-to-end ### phase 4: TUI 1. Implement `bubbletea` model in `internal/playbook/tui/` 2. Cell list view with status indicators (reuse `tui/list` from core/lib) 3. Output pane with `OutputRenderer` dispatch by `OutputEntry.Type` 4. Memory inspector panel (lang/cross/shared tabs) 5. Keybindings: Ctrl+R, Ctrl+Shift+R, Ctrl+↑/↓, Ctrl+I, Ctrl+S, Ctrl+Q 6. Wire TUI to CLI (`res-playbook notebook.md`) ### phase 5: shell adapter + polish 1. Implement `ExternalAdapter` for shell subprocess 2. Shell helper library (`playbook` command) 3. Log file output for CLI mode 4. Error reporting with cell references 5. Notebook save (Ctrl+S + CLI) 6. Documentation update in `docs/extra/extra.md` --- ## scope boundaries **In scope for v1:** - Markdown notebook parser with TOML frontmatter - `-#runtime` tag system for code blocks - Structured output model (v1: text only; framework for future types) - Variable store (shared-mem) - Cross-lang-mem (basic) - Embedded Lua server - Shell adapter (external) - Auto-discovery of `res-playbook-runtime-*` binaries - Manual runtime configuration with `{{IPCSOCKET}}` and `{{WORKSPACE}}` template variables - CLI one-shot runner - TUI interactive mode - Variable inspector - Helper library for Lua + Shell - Notebook save **Out of scope for v1:** - Python/Go/Rust servers (use external runtimes) - Rich output renderers (images, SQL tables, JSON) — framework only - Plugin system - External Lua server (embedded is sufficient for v1) - `isolate: true` cells - IPC library simplifications (separate issue) --- ## dependency notes - `gopher-lua` is already in `extra/go.mod` - `bubbletea` + `lipgloss` already in `go.mod` - `github.com/BurntSushi/toml` already in `go.mod` (indirect) - No new external dependencies required for v1 --- ## verification After each phase: 1. `go build ./cmd/res-playbook/...` 2. `go test ./internal/playbook/...` 3. Manual test: create a test notebook, run it Final verification (full `extra/` suite): 1. `go build ./...` from `extra/` 2. `go test ./...` from `extra/` 3. Manual test: Lua notebook with cross-mem + shared-mem 4. Manual test: Shell notebook 5. Manual test: TUI interactive mode 6. Manual test: auto-discovery of a `res-playbook-runtime-*` binary
Author
Member

addendum: core/lib reuse analysis

Evaluated every core/lib/ package for direct reuse in res-playbook. Here's what's available and how it maps.


directly reusable

core/lib/script — Lua VM management

The highest-value reuse target. Provides everything needed for the embedded Lua adapter:

Type/Func Reuse in playbook
script.Engine Per-language-session VM. LoadString() executes cells, SetGlobal() injects cross-mem variables, RegisterAddon() loads the playbook helper library
script.Pool Pooled VMs for isolated cell execution (when isolate: true). Run() compiles + executes with per-run env injection
script.CompileString() Pre-compile cell code into FunctionProto without a live VM
script.SandboxSetup() Control what Lua can access per cell (ScopeBase, ScopeMath, ScopeString, ScopeTable)
script.NewEngine() Creates VM with res-core stdlib already registered (has_binary, has_file, read_file, env, sleep)

Usage pattern for embedded Lua adapter:

// Single persistent session (default)
eng := script.NewEngine(ctx)
eng.SetGlobal("playbook", helperLib)  // inject shared-mem helper
eng.SetGlobal("x", lua.LNumber(5))    // inject cross-mem
eng.LoadString(cellCode, cellName)     // execute cell

// Isolated cell (when isolate: true)
pool := script.NewPool(1, script.SandboxSetup(script.ScopeBase, script.ScopeMath, script.ScopeString, script.ScopeTable))
rets, err := pool.Run(ctx, cellCode, map[string]lua.LValue{
    "playbook": helperLib,
    "x": lua.LNumber(5),
})

core/lib/ipc — JSON-over-UNIX socket RPC

The protocol message format maps directly to the language server protocol:

ipc type playbook reuse
ipc.Message Wire format for client↔server communication. Type field distinguishes declare/execute/result/list_vars/get_var/vars
ipc.Server Language server side. Handle() registers message handlers. Listens on UDS
ipc.Client Program side. Call() sends execute/list_vars/get_var and blocks for response. Notify() for fire-and-forget

The playbook protocol is ipc.Message with custom type names:

// client → server (execute)
ipc.Message{Type: "execute", ID: 1, Payload: json.RawMessage(`{"code":"x=5","cross_mem":{...},"shared_mem":{...}}`)}

// server → client (result)
ipc.Message{Type: "result", ID: 1, Payload: json.RawMessage(`{"stdout":"","vars":{"x":5},"errors":[]}`)}

External language servers (shell, future Python) just need to import core/lib/ipc and implement the handler loop.

core/lib/log — dual-output logger

CLI batch mode: log.New("res-playbook") writes to both stderr and a timestamped log file at /var/log/residual/res-playbook/ (or ~/.local/share/residual/logs/res-playbook/). Section stack maps to cell naming: l.SetSection("cell:load-data")().

core/lib/config — path conventions

Func Use
config.IPCSocketPath("playbook") Socket path for language server communication: /run/user/1000/residual/playbook.sock
config.RuntimeDir() Runtime directory for UDS and temp files
config.UserConfigDir() Config dir: ~/.config/residual/

core/lib/proc — process management

For external language servers (shell, future Python):

  • proc.NewReaper() — harvests zombie server processes
  • proc.GracefulShutdown() — SIGTERM → wait → SIGKILL for cleanup
  • proc.SignalForwarder — relay signals to server process groups

core/lib/tui/list — scrollable list widget

Direct reuse for the cell list in the TUI:

  • list.NewList(count) — initializes for cell count
  • list.Up()/Down()/PageUp()/PageDown() — cursor navigation
  • list.ClampScroll(visibleRows) — scroll management
  • list.RenderListRow(label, tag, active, width) — renders cell rows with [lang] tag

core/lib/tui/theme — amber palette and styles

TUI inherits the full residual aesthetic:

  • theme.InitUnified() — loads theme.toml, applies amber palette
  • theme.StyleStatusBar, theme.StyleStatusVal, theme.StyleStatusDim — status bar
  • theme.StyleBorder — panel borders
  • theme.OverlayBoxStyle — memory inspector overlay

core/lib/tui/widget — widget interface

For status bar widgets: cell count, running indicator, memory usage. Implement widget.Widget interface.

core/lib/util — path helpers

util.ExpandHome(), util.ShortCWD(), util.CurrentUser() — used in notebook metadata and log output.


not directly reusable (playbook-specific)

These stay in internal/playbook/:

Component Why not in core/lib
Value type / VariableStore / CrossMem No other tool needs typed variable storage yet
Notebook / Cell / markdown parser Playbook-specific format
Protocol message payloads (execute context, result types) ipc.Message is the wire format; the payloads are playbook-specific structs
Executor / Runner / ExecutionContext Orchestration logic specific to notebook execution
LanguageServer interface + ServerManager Wrapper around ipc that adds declare/execute semantics
TUI model (bubbletea Model/Update/View) Application-specific layout

Language adapter interface wrapping core/lib/script:

// internal/playbook/langs/embedded.go
type EmbeddedAdapter struct {
    engine *script.Engine
}

func (a *EmbeddedAdapter) Execute(code string, crossMem, sharedMem map[string]any) (Result, error) {
    // inject cross-mem as globals
    for k, v := range crossMem {
        a.engine.SetGlobal(k, toLuaValue(v))
    }
    // inject shared-mem helper
    a.engine.SetGlobal("playbook", a.helperLib)
    // execute
    err := a.engine.LoadString(code, "=cell")
    // capture results
    return Result{...}, err
}

func (a *EmbeddedAdapter) ListVars() (map[string]VarInfo, error) {
    // iterate Lua globals, return names + types
}

External adapter wrapping core/lib/ipc:

// internal/playbook/langs/external.go
type ExternalAdapter struct {
    client *ipc.Client
    lang   string
}

func (a *ExternalAdapter) Execute(code string, crossMem, sharedMem map[string]any) (Result, error) {
    payload, _ := json.Marshal(ExecuteRequest{Code: code, CrossMem: crossMem, SharedMem: sharedMem})
    resp, err := a.client.Call(context.Background(), ipc.Message{Type: "execute", Payload: payload})
    // deserialize result
    return result, nil
}

CLI logger:

// cmd/res-playbook/main.go
logger := log.New("res-playbook")
defer logger.Close()
// batch runner logs each cell's output
logger.Printf("[%s] ✓ %.3fs — %s", cell.Lang, dur, cell.Name)

dependency summary

core/lib package Import path Used for
script git.merith.xyz/residual/core/lib/script Lua VM engine + pool + sandbox
ipc git.merith.xyz/residual/core/lib/ipc Language server protocol wire format
log git.merith.xyz/residual/core/lib/log CLI batch output logging
config git.merith.xyz/residual/core/lib/config Socket path, runtime dir
proc git.merith.xyz/residual/core/lib/proc External server process management
tui/list git.merith.xyz/residual/core/lib/tui/list Cell list navigation + rendering
tui/theme git.merith.xyz/residual/core/lib/tui/theme Amber palette + styles
tui/widget git.merith.xyz/residual/core/lib/tui/widget Status bar widgets
util git.merith.xyz/residual/core/lib/util Path helpers

No new core/lib packages needed. All reuse is from existing packages.


updated implementation phases

With core/lib reuse factored in:

phase 1: core types and parser — no core/lib deps, pure playbook types

phase 2: language adapter layer

  • Wrap script.Engine for embedded Lua adapter (reuse script.NewEngine, LoadString, SetGlobal)
  • Define LanguageAdapter interface
  • Implement Lua helper library as script.Engine.RegisterAddon("playbook", loader)
  • Wrap ipc.Client/ipc.Server for external adapter protocol
  • Define playbook-specific protocol payloads (ExecuteRequest, Result, VarInfo)

phase 3: executor — uses adapters from phase 2, no direct core/lib deps

phase 4: TUI — reuse tui/list for cell navigation, tui/theme for styling, tui/widget for status bar

phase 5: shell adapter + CLI — reuse proc for server management, log for batch output, config for socket paths

## addendum: core/lib reuse analysis Evaluated every `core/lib/` package for direct reuse in `res-playbook`. Here's what's available and how it maps. --- ### directly reusable **`core/lib/script` — Lua VM management** The highest-value reuse target. Provides everything needed for the embedded Lua adapter: | Type/Func | Reuse in playbook | |-----------|-------------------| | `script.Engine` | Per-language-session VM. `LoadString()` executes cells, `SetGlobal()` injects cross-mem variables, `RegisterAddon()` loads the `playbook` helper library | | `script.Pool` | Pooled VMs for isolated cell execution (when `isolate: true`). `Run()` compiles + executes with per-run env injection | | `script.CompileString()` | Pre-compile cell code into `FunctionProto` without a live VM | | `script.SandboxSetup()` | Control what Lua can access per cell (ScopeBase, ScopeMath, ScopeString, ScopeTable) | | `script.NewEngine()` | Creates VM with res-core stdlib already registered (has_binary, has_file, read_file, env, sleep) | **Usage pattern for embedded Lua adapter:** ```go // Single persistent session (default) eng := script.NewEngine(ctx) eng.SetGlobal("playbook", helperLib) // inject shared-mem helper eng.SetGlobal("x", lua.LNumber(5)) // inject cross-mem eng.LoadString(cellCode, cellName) // execute cell // Isolated cell (when isolate: true) pool := script.NewPool(1, script.SandboxSetup(script.ScopeBase, script.ScopeMath, script.ScopeString, script.ScopeTable)) rets, err := pool.Run(ctx, cellCode, map[string]lua.LValue{ "playbook": helperLib, "x": lua.LNumber(5), }) ``` **`core/lib/ipc` — JSON-over-UNIX socket RPC** The protocol message format maps directly to the language server protocol: | ipc type | playbook reuse | |----------|----------------| | `ipc.Message` | Wire format for client↔server communication. `Type` field distinguishes declare/execute/result/list_vars/get_var/vars | | `ipc.Server` | Language server side. `Handle()` registers message handlers. Listens on UDS | | `ipc.Client` | Program side. `Call()` sends execute/list_vars/get_var and blocks for response. `Notify()` for fire-and-forget | **The playbook protocol is ipc.Message with custom type names:** ```go // client → server (execute) ipc.Message{Type: "execute", ID: 1, Payload: json.RawMessage(`{"code":"x=5","cross_mem":{...},"shared_mem":{...}}`)} // server → client (result) ipc.Message{Type: "result", ID: 1, Payload: json.RawMessage(`{"stdout":"","vars":{"x":5},"errors":[]}`)} ``` External language servers (shell, future Python) just need to import `core/lib/ipc` and implement the handler loop. **`core/lib/log` — dual-output logger** CLI batch mode: `log.New("res-playbook")` writes to both stderr and a timestamped log file at `/var/log/residual/res-playbook/` (or `~/.local/share/residual/logs/res-playbook/`). Section stack maps to cell naming: `l.SetSection("cell:load-data")()`. **`core/lib/config` — path conventions** | Func | Use | |------|-----| | `config.IPCSocketPath("playbook")` | Socket path for language server communication: `/run/user/1000/residual/playbook.sock` | | `config.RuntimeDir()` | Runtime directory for UDS and temp files | | `config.UserConfigDir()` | Config dir: `~/.config/residual/` | **`core/lib/proc` — process management** For external language servers (shell, future Python): - `proc.NewReaper()` — harvests zombie server processes - `proc.GracefulShutdown()` — SIGTERM → wait → SIGKILL for cleanup - `proc.SignalForwarder` — relay signals to server process groups **`core/lib/tui/list` — scrollable list widget** Direct reuse for the cell list in the TUI: - `list.NewList(count)` — initializes for cell count - `list.Up()/Down()/PageUp()/PageDown()` — cursor navigation - `list.ClampScroll(visibleRows)` — scroll management - `list.RenderListRow(label, tag, active, width)` — renders cell rows with `[lang]` tag **`core/lib/tui/theme` — amber palette and styles** TUI inherits the full residual aesthetic: - `theme.InitUnified()` — loads theme.toml, applies amber palette - `theme.StyleStatusBar`, `theme.StyleStatusVal`, `theme.StyleStatusDim` — status bar - `theme.StyleBorder` — panel borders - `theme.OverlayBoxStyle` — memory inspector overlay **`core/lib/tui/widget` — widget interface** For status bar widgets: cell count, running indicator, memory usage. Implement `widget.Widget` interface. **`core/lib/util` — path helpers** `util.ExpandHome()`, `util.ShortCWD()`, `util.CurrentUser()` — used in notebook metadata and log output. --- ### not directly reusable (playbook-specific) These stay in `internal/playbook/`: | Component | Why not in core/lib | |-----------|---------------------| | `Value` type / `VariableStore` / `CrossMem` | No other tool needs typed variable storage yet | | `Notebook` / `Cell` / markdown parser | Playbook-specific format | | Protocol message *payloads* (execute context, result types) | ipc.Message is the wire format; the payloads are playbook-specific structs | | `Executor` / `Runner` / `ExecutionContext` | Orchestration logic specific to notebook execution | | `LanguageServer` interface + `ServerManager` | Wrapper around ipc that adds declare/execute semantics | | TUI model (`bubbletea` Model/Update/View) | Application-specific layout | --- ### recommended integration points **Language adapter interface wrapping core/lib/script:** ```go // internal/playbook/langs/embedded.go type EmbeddedAdapter struct { engine *script.Engine } func (a *EmbeddedAdapter) Execute(code string, crossMem, sharedMem map[string]any) (Result, error) { // inject cross-mem as globals for k, v := range crossMem { a.engine.SetGlobal(k, toLuaValue(v)) } // inject shared-mem helper a.engine.SetGlobal("playbook", a.helperLib) // execute err := a.engine.LoadString(code, "=cell") // capture results return Result{...}, err } func (a *EmbeddedAdapter) ListVars() (map[string]VarInfo, error) { // iterate Lua globals, return names + types } ``` **External adapter wrapping core/lib/ipc:** ```go // internal/playbook/langs/external.go type ExternalAdapter struct { client *ipc.Client lang string } func (a *ExternalAdapter) Execute(code string, crossMem, sharedMem map[string]any) (Result, error) { payload, _ := json.Marshal(ExecuteRequest{Code: code, CrossMem: crossMem, SharedMem: sharedMem}) resp, err := a.client.Call(context.Background(), ipc.Message{Type: "execute", Payload: payload}) // deserialize result return result, nil } ``` **CLI logger:** ```go // cmd/res-playbook/main.go logger := log.New("res-playbook") defer logger.Close() // batch runner logs each cell's output logger.Printf("[%s] ✓ %.3fs — %s", cell.Lang, dur, cell.Name) ``` --- ### dependency summary | core/lib package | Import path | Used for | |------------------|-------------|----------| | `script` | `git.merith.xyz/residual/core/lib/script` | Lua VM engine + pool + sandbox | | `ipc` | `git.merith.xyz/residual/core/lib/ipc` | Language server protocol wire format | | `log` | `git.merith.xyz/residual/core/lib/log` | CLI batch output logging | | `config` | `git.merith.xyz/residual/core/lib/config` | Socket path, runtime dir | | `proc` | `git.merith.xyz/residual/core/lib/proc` | External server process management | | `tui/list` | `git.merith.xyz/residual/core/lib/tui/list` | Cell list navigation + rendering | | `tui/theme` | `git.merith.xyz/residual/core/lib/tui/theme` | Amber palette + styles | | `tui/widget` | `git.merith.xyz/residual/core/lib/tui/widget` | Status bar widgets | | `util` | `git.merith.xyz/residual/core/lib/util` | Path helpers | **No new core/lib packages needed.** All reuse is from existing packages. --- ### updated implementation phases With core/lib reuse factored in: **phase 1: core types and parser** — no core/lib deps, pure playbook types **phase 2: language adapter layer** - Wrap `script.Engine` for embedded Lua adapter (reuse `script.NewEngine`, `LoadString`, `SetGlobal`) - Define `LanguageAdapter` interface - Implement Lua helper library as `script.Engine.RegisterAddon("playbook", loader)` - Wrap `ipc.Client`/`ipc.Server` for external adapter protocol - Define playbook-specific protocol payloads (ExecuteRequest, Result, VarInfo) **phase 3: executor** — uses adapters from phase 2, no direct core/lib deps **phase 4: TUI** — reuse `tui/list` for cell navigation, `tui/theme` for styling, `tui/widget` for status bar **phase 5: shell adapter + CLI** — reuse `proc` for server management, `log` for batch output, `config` for socket paths
Author
Member

addendum: structured output model

Added extensible output system to the plan. The Result type carries structured OutputEntry values, not just stdout text.

Result type

type Result struct {
    Stdout string           // raw stdout for log files
    Stderr string           // raw stderr for log files
    Vars   map[string]VarInfo
    Output []OutputEntry    // structured output for TUI rendering
}

type OutputEntry struct {
    Type string          // "text", "table", "json", "sql", "image", etc.
    MIME string          // MIME type for future use
    Data json.RawMessage // type-specific data
}

v1 behavior

  • Runtimes capture stdout/stderr
  • Output contains a single text entry with stdout
  • TUI renders text entries as plain text

Future expansion

Runtimes can return structured output:

  • sql{"columns": [...], "rows": [[...]]} → rendered as a table
  • json{"data": {...}} → syntax-highlighted JSON
  • table{"headers": [...], "rows": [[...]]} → formatted table
  • image → base64 PNG → terminal image protocol

TUI renderer

type OutputRenderer interface {
    Render(entry OutputEntry, width int) string
}

Each output type gets its own renderer. The TUI dispatches by OutputEntry.Type.

Cell output accumulation

Each cell accumulates a list of OutputEntry values as it runs. Multiple output entries per cell are supported (e.g., print() calls + final return value). The output pane is scrollable if content exceeds visible area.

## addendum: structured output model Added extensible output system to the plan. The `Result` type carries structured `OutputEntry` values, not just stdout text. ### Result type ```go type Result struct { Stdout string // raw stdout for log files Stderr string // raw stderr for log files Vars map[string]VarInfo Output []OutputEntry // structured output for TUI rendering } type OutputEntry struct { Type string // "text", "table", "json", "sql", "image", etc. MIME string // MIME type for future use Data json.RawMessage // type-specific data } ``` ### v1 behavior - Runtimes capture stdout/stderr - `Output` contains a single `text` entry with stdout - TUI renders text entries as plain text ### Future expansion Runtimes can return structured output: - `sql` → `{"columns": [...], "rows": [[...]]}` → rendered as a table - `json` → `{"data": {...}}` → syntax-highlighted JSON - `table` → `{"headers": [...], "rows": [[...]]}` → formatted table - `image` → base64 PNG → terminal image protocol ### TUI renderer ```go type OutputRenderer interface { Render(entry OutputEntry, width int) string } ``` Each output type gets its own renderer. The TUI dispatches by `OutputEntry.Type`. ### Cell output accumulation Each cell accumulates a list of `OutputEntry` values as it runs. Multiple output entries per cell are supported (e.g., print() calls + final return value). The output pane is scrollable if content exceeds visible area.
Author
Member

Resolved — integrated into main body under §output model. The structured output model (Result.Output []OutputEntry), playbook.log() vs stdout clarification, and OutputRenderer interface are all in the updated issue body now.

Resolved — integrated into main body under `§output model`. The structured output model (`Result.Output []OutputEntry`), `playbook.log()` vs stdout clarification, and `OutputRenderer` interface are all in the updated issue body now.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
residual/.agent#18
No description provided.