res-playbook — multi-language notebook runner #18
Labels
No labels
harness
proj-core-console
proj-core-demo
proj-core-edit
proj-core-init
proj-core-lib
proj-core-login
proj-core-res
proj-core-sh
proj-core-theme
proj-docs
proj-extra-calc
proj-extra-code
proj-extra-playbook
proj-extra-sheets
proj-harness
proj-os
residual
tier-0-trivial
tier-1-easy
tier-2-medium
tier-3-hard
tier-4-major
tier-5-epic
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
residual/.agent#18
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 binarycmd/res-playbook, all internal packages underinternal/playbook/)file format
The notebook is a standard markdown file with TOML frontmatter and tagged runtime code blocks:
Summarize
-#runtime
Regular code blocks (without
-#runtime) are rendered as documentation, not executed.The
languageslist 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
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-LANGres-playbook-runtime-shellres-playbook-runtime-pythonres-playbook-runtime-goauto-discovery
Runtimes named
res-playbook-runtime-*are auto-discovered:res-playbook-runtime-*declaremessage within 5 secondsmanual configuration
Runtimes that don't follow the naming convention can be configured in
~/.config/residual/playbook.toml: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
declaremessage:A single runtime can declare support for multiple languages (monolith runtime).
runtime resolution
prefer_external = truein configConfig:
memory model
Three distinct layers:
playbook.set()/playbook.get()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 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
x = 5ListVars()→{x: 5}$xdirectlyshared-mem flow
playbook.set("mydata", {1,2,3})playbook get mydata→ reads from shared-memlang-mem flow
x = [1,2,3]Lua specifics
output model
Each cell captures structured output. The
Resulttype carries a list ofOutputEntryvalues — one per output event during execution.v1 behavior
Outputcontains a singletextentry with stdoutstdout vs playbook.log()
stdout is the primary output source. Runtimes that support stdout capture intercept
print()/echo()/ etc. and route it toOutputEntry{Type: "text"}.playbook.log()is a fallback for runtimes that can't intercept stdout (e.g., shell). It explicitly creates atextOutputEntry from within the runtime's helper library.For embedded runtimes like Lua,
print()is overridden to capture output directly — no need forplaybook.log().future expansion
Runtimes can return structured output:
sql→{"columns": [...], "rows": [[...]]}→ rendered as a tablejson→{"data": {...}}→ syntax-highlighted JSONtable→{"headers": [...], "rows": [[...]]}→ formatted tableimage→ base64 PNG → terminal image protocolTUI renderer
Each output type gets its own renderer. The TUI dispatches by
OutputEntry.Type.Cell output accumulation
Each cell accumulates a list of
OutputEntryvalues 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 formatServer— UDS listener with handler dispatchClient— UDS connector with Call/NotifyProgram-specific wrappers (
internal/playbook/runtime/):runtime.Client— wrapsipc.Client, adds declare handshake, execute/list_vars helpersruntime.Server— wrapsipc.Server, adds runtime handler registrationruntime.Protocol— playbook-specific message types and payloadsIPC simplifications are deferred to a separate issue.
execution model
Two modes sharing the same engine:
res-playbook run notebook.md— execute all runtime cells top to bottom, log output, exitres-playbook notebook.md— open TUI, navigate cells, run on demand, inspect variablesCell isolation: Cells share a persistent VM (same language session carries state). Unless marked
isolate: truein metadata (future feature).Error handling:
helper library (playbook stdlib)
Each language gets a helper library that talks to the playbook program via the runtime.
Lua:
Shell:
CLI mode
TUI mode
Layout:
Keybindings (Ctrl modkey, per residual convention):
Ctrl+RCtrl+Shift+RCtrl+↑/↓Ctrl+ICtrl+SCtrl+Qfile layout
All playbook packages live under
internal/playbook/.implementation steps
phase 1: core types and parser
Cell,Notebook,Metadatatypes ininternal/playbook/notebook/github.com/BurntSushi/toml— already in go.mod)-#runtimetags, language tags, body, preceding markdown as cell name/descriptionValuetype with JSON serialization ininternal/playbook/store/VariableStore(shared-mem) andCrossMem(cross-lang-mem) ininternal/playbook/store/phase 2: language runtime layer
Runtimeinterface ininternal/playbook/runtime/:Execute(ctx, code, env) → Result,ListVars(ctx) → map[string]VarInfoRuntimeRegistry— tracks available runtimes, resolves by languagecore/lib/script:Enginefor persistent sessions (same-language cells share state)RegisterAddon("playbook", loader)for the helper libraryplaybook.set(),playbook.get(),playbook.list(),playbook.log()core/lib/ipc:runtime.Clientwrapsipc.Client, adds declare handshakeruntime.Serverwrapsipc.Server, adds runtime handler registrationExecuteRequest,Result,VarInfores-playbook-runtime-*binariesphase 3: executor
Executor— orchestrates cell execution, manages runtimes, routes memoryRunner— sequential batch moderes-playbook run)phase 4: TUI
bubbleteamodel ininternal/playbook/tui/tui/listfrom core/lib)OutputRendererdispatch byOutputEntry.Typeres-playbook notebook.md)phase 5: shell adapter + polish
ExternalAdapterfor shell subprocessplaybookcommand)docs/extra/extra.mdscope boundaries
In scope for v1:
-#runtimetag system for code blocksres-playbook-runtime-*binaries{{IPCSOCKET}}and{{WORKSPACE}}template variablesOut of scope for v1:
isolate: truecellsdependency notes
gopher-luais already inextra/go.modbubbletea+lipglossalready ingo.modgithub.com/BurntSushi/tomlalready ingo.mod(indirect)verification
After each phase:
go build ./cmd/res-playbook/...go test ./internal/playbook/...Final verification (full
extra/suite):go build ./...fromextra/go test ./...fromextra/res-playbook-runtime-*binaryaddendum: core/lib reuse analysis
Evaluated every
core/lib/package for direct reuse inres-playbook. Here's what's available and how it maps.directly reusable
core/lib/script— Lua VM managementThe highest-value reuse target. Provides everything needed for the embedded Lua adapter:
script.EngineLoadString()executes cells,SetGlobal()injects cross-mem variables,RegisterAddon()loads theplaybookhelper libraryscript.Poolisolate: true).Run()compiles + executes with per-run env injectionscript.CompileString()FunctionProtowithout a live VMscript.SandboxSetup()script.NewEngine()Usage pattern for embedded Lua adapter:
core/lib/ipc— JSON-over-UNIX socket RPCThe protocol message format maps directly to the language server protocol:
ipc.MessageTypefield distinguishes declare/execute/result/list_vars/get_var/varsipc.ServerHandle()registers message handlers. Listens on UDSipc.ClientCall()sends execute/list_vars/get_var and blocks for response.Notify()for fire-and-forgetThe playbook protocol is ipc.Message with custom type names:
External language servers (shell, future Python) just need to import
core/lib/ipcand implement the handler loop.core/lib/log— dual-output loggerCLI 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 conventionsconfig.IPCSocketPath("playbook")/run/user/1000/residual/playbook.sockconfig.RuntimeDir()config.UserConfigDir()~/.config/residual/core/lib/proc— process managementFor external language servers (shell, future Python):
proc.NewReaper()— harvests zombie server processesproc.GracefulShutdown()— SIGTERM → wait → SIGKILL for cleanupproc.SignalForwarder— relay signals to server process groupscore/lib/tui/list— scrollable list widgetDirect reuse for the cell list in the TUI:
list.NewList(count)— initializes for cell countlist.Up()/Down()/PageUp()/PageDown()— cursor navigationlist.ClampScroll(visibleRows)— scroll managementlist.RenderListRow(label, tag, active, width)— renders cell rows with[lang]tagcore/lib/tui/theme— amber palette and stylesTUI inherits the full residual aesthetic:
theme.InitUnified()— loads theme.toml, applies amber palettetheme.StyleStatusBar,theme.StyleStatusVal,theme.StyleStatusDim— status bartheme.StyleBorder— panel borderstheme.OverlayBoxStyle— memory inspector overlaycore/lib/tui/widget— widget interfaceFor status bar widgets: cell count, running indicator, memory usage. Implement
widget.Widgetinterface.core/lib/util— path helpersutil.ExpandHome(),util.ShortCWD(),util.CurrentUser()— used in notebook metadata and log output.not directly reusable (playbook-specific)
These stay in
internal/playbook/:Valuetype /VariableStore/CrossMemNotebook/Cell/ markdown parserExecutor/Runner/ExecutionContextLanguageServerinterface +ServerManagerbubbleteaModel/Update/View)recommended integration points
Language adapter interface wrapping core/lib/script:
External adapter wrapping core/lib/ipc:
CLI logger:
dependency summary
scriptgit.merith.xyz/residual/core/lib/scriptipcgit.merith.xyz/residual/core/lib/ipcloggit.merith.xyz/residual/core/lib/logconfiggit.merith.xyz/residual/core/lib/configprocgit.merith.xyz/residual/core/lib/proctui/listgit.merith.xyz/residual/core/lib/tui/listtui/themegit.merith.xyz/residual/core/lib/tui/themetui/widgetgit.merith.xyz/residual/core/lib/tui/widgetutilgit.merith.xyz/residual/core/lib/utilNo 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
script.Enginefor embedded Lua adapter (reusescript.NewEngine,LoadString,SetGlobal)LanguageAdapterinterfacescript.Engine.RegisterAddon("playbook", loader)ipc.Client/ipc.Serverfor external adapter protocolphase 3: executor — uses adapters from phase 2, no direct core/lib deps
phase 4: TUI — reuse
tui/listfor cell navigation,tui/themefor styling,tui/widgetfor status barphase 5: shell adapter + CLI — reuse
procfor server management,logfor batch output,configfor socket pathsaddendum: structured output model
Added extensible output system to the plan. The
Resulttype carries structuredOutputEntryvalues, not just stdout text.Result type
v1 behavior
Outputcontains a singletextentry with stdoutFuture expansion
Runtimes can return structured output:
sql→{"columns": [...], "rows": [[...]]}→ rendered as a tablejson→{"data": {...}}→ syntax-highlighted JSONtable→{"headers": [...], "rows": [[...]]}→ formatted tableimage→ base64 PNG → terminal image protocolTUI renderer
Each output type gets its own renderer. The TUI dispatches by
OutputEntry.Type.Cell output accumulation
Each cell accumulates a list of
OutputEntryvalues 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.Resolved — integrated into main body under
§output model. The structured output model (Result.Output []OutputEntry),playbook.log()vs stdout clarification, andOutputRendererinterface are all in the updated issue body now.