| name | PromptForge review notes |
|---|---|
| overview | Running notes from the PromptForge source review. Observe only. Do not implement. |
| todos | |
| isProject | false |
Observe these notes. Do nothing else. Append new notes when they arrive. Do not turn this into a design or a task list.
-
Duplication between H1 execution (
execute_live_h1) and H2 execution (walk_section_blocks/run_one_section). Same Lua/prose block loop shape, forked implementations. -
Rename
walk_section_blockstorun_one_section_impl. Keep wrapperrun_one_section. Fanout calls that impl too. Slightly dirty on purpose: breadcrumb that this is not a third abstraction, it is the section walker's engine, reused. Better than a grand engine type. Stranger coming from fanout has to know the history. Accepted. -
"Slightly dirty name" is the right phrase for that kind of breadcrumb. Observation for later naming, no action.
-
Review method: agent drives with A/B questions. Vinnie is the judgment engine. Decisions become refactor notes on this plan. No code until execute.
-
One block runner (A). H1 vs H2 differences are caller-set parameters / mode, not a second loop. Deviation stays visible at the call site. Illegal ops in a mode (example:
models.needoutside live H1) are runtime errors with a clear message, same family as other Lua errors. Do not grow a second language surface. -
Do not split
engine.rson line count. ~1000 lines can be reasonable if it is the program guts. Inspect the file first. Split later only if jobs do not actually belong together. -
Borrowed execute frames: prefer one struct, not Run/Walk/BlockWalk copies of the same fields. Differences should be unified at call sites if possible; leftover one-off fields become
Option(fanoutitem).ControlContextstays a separate owned snapshot for Lua closures. Field-audit when we read the file; drop unread fields. Three parallel structs with mostly the same fields is a smell. -
Preamble / prologue / epilog are documentation terminology for positions (H1; first Lua of a section; Lua after last prose). Behavior is emergent: missing model errors at first prose, epilog runs because it is the next block. Do not add phase types or extra enforcement. Keep the words in docs and comments so talk stays consistent. Comments must not imply a separate prologue/epilog engine.
-
jumptransfers control. Target is a visible child or sibling. The jumper does not resume. After a child-level walk exhausts, the parent walk continues at the next sibling under normal fall-through / off-walk rules. Docs should say this. Keep the current engine behavior. -
Children do not run by fall-through from the parent. Nesting is structure and an address space, not a call stack. Children run only via
jump/execute/fanout(or other addressing). Docs should say that. Keep it. -
Keep both
execute(subroutine, nested walk, returns a string) andjump(goto, no return). Same walk rules inside anexecutechain. -
fanoutis a core mechanic. Third driver of the same block runner (run_one_section_impl). Keep the share. Do not demote or isolate it. -
One full tool loop per section (last prose). Earlier lua/prose is setup: progressive
tools.add, store writes, then the final loop. Intended. Want another full loop:executeanother section. Keeploop_capableon last prose only. Docs: setup then loop, not "cute last-block flag." -
Keep off-walk. A leading
---(first content of the section) takes the section off fall-through so helpers / lists / workers can live in the file. Addressedjump/execute/fanoutstill run it. A---after any prose or Lua block cuts the section: the rest is a comment (not compiled, not run). Two roles, one token. Docs must say both. Intended. -
Keep
lua sharedin the preamble. One compiled library, replayed as the first chunk of every section VM. Not live H1. At most one fence; not under H1 as a second live program. Docs: shared helpers vs live preamble Lua. -
varsemantics (locked): the store is the intentional shared mutable world;varis the walk-local clipboard. Onevarper walk: fall-through andjumpshare it (H1 is the first writer, not a special seed).execute()andfanout()clonevarin; child writes do not affect the caller. Store stays shared (execute/fanout writes are visible). Fixes docs (promptforge.md,guide/src/lua.md) vs implementation. Fanout arms keep fresh-per-arm clones. -
replycarries to the next section on the same walk;jumppreserves it unless the author setsreply = nil. Conversation does not carry. Keep. Docs already say this. -
Progressive tool exposure (locked A):
tools.add/tools.add_localbetween blocks reach the next prose. Setup turns stay narrow; last prose is the full loop. This is the small-model pattern. Keep. -
varis JSON-only. Today a function invarfails at serialization (bad file/line). Refactor: guardvarwrites at assignment (proxy__newindex, same pattern as sealedsys) so non-JSON values fail at the Lua line that assigned them. -
One model per section.
models.useonce, before first prose binds it. Do not switch models mid-section: same conversation, KV cache poisoned.model:inferis fine (fresh context). Want another model:executeor another section. -
Capability binding in the preamble only (A).
tools.need/models.needresolve when that Lua runs in H1. H2 uses captured bindings only. Fail fast: do not burn inference then discover a missing model in section 12. Keep. -
models.infer(prompt)is the side-channel infer: one round, no tools, fresh conversation, does not touchreply. Default model is the section's current model. Must be able to pick another declared model (e.g.models.get("tag"):infer(...)or a two-arg form). Main model work stays in prose; this is for a narrow semantic question off the main thread.model:infer(in-context, tools, updatesreply) is separate; do not conflate. -
sysstays sealed read-only. Unknown field read or any write raises. Runtime facts are not author state. Keep. -
Lua
returnstays scalar-only (string / integer / number / boolean). Tables are an error. Structured data goes in the store. Open design question for later: tables as JSON string on the Rust boundary, and prompt-from-prompt calls decoding JSON to a Lua table. Do not build without a concrete use case. -
Keep
tools.alwaysin the preamble as the baseline for every model-facing section.tools.addis per-section on top. No pointless ceremony; frontier-model users can declare once up front. Keep. -
Keep near-duplicate tool detection. Fail before the model sees two tools that read the same. Predictable behavior is the point of the language. Keep.
-
Keep
untrusted(s)guard envelope for model-facing untrusted text (store reads, tool output, etc.). Required for handling hostile input. Keep. -
Keep
max_tool_iterations. Finite cap (can be large, e.g. 10000) prevents infinite tool loops. Configurable in frontmatter, runtime default otherwise. Keep. -
Observer is a side channel for progress reporting. It cannot steer or cancel the run. Keep.
-
Keep
CancelHandlefor cooperative cancellation. Long runs need a kill switch; this is the mechanism. Keep. -
Keep
DebugCaptureas opt-in raw request/response hook. Production leaves it unset. Debugging needs the wire. Keep. -
Keep
execute()recursion cap at 8.fanout()shares the same cap and counter (nested fanout counts against the same depth). 8 is deep enough; runaway recursion is a bug. Keep. -
Keep
tools.callsper-section counts. Needed so a prompt can require a small model to call a tool at least N times (e.g.assert(tools.calls.search >= 1)). Observer sees the calls;tools.callsis the in-language gate. Keep. -
Keep
models.defaultin the preamble as the prompt-wide baseline.models.useoverrides per section. One-model users should not repeat themselves. Keep. -
Keep frozen Model handles from
models.need. Read-only after capture. No values to change mid-run. Keep. -
Frozen Tool objects. No mutable
.description. Description override is a positional string parameter:tools.need(alias, catalog_desc, override?),tools.always(alias, override?),tools.add(alias, override?). Precedence:add>need/always> catalog. Named table only if more overrides appear later. Change. -
Keep
tools.add_local. Lua-backed tools are the structured-data capture mechanism (the alternative to Pydantic). The model calls a tool, the handler runs in the section VM, the caller gets structured arguments. Core value. Keep. -
Keep the store virtual and run-scoped. No real filesystem access through
store.*. If real FS access is ever added, it is a different shape:ls,grep,stat,mkdir, etc. as separate tools, not store paths. Keep. -
Keep
log()in some form. Need a way to dump internal state for prompt debugging. Whether it goes through the observer or a separate channel is open. The 256-char / no-newline cap may be too tight for state dumps. Revisit when we know what debugging actually needs. -
Prose substitution namespaces:
args,reply,var,sys, plus bare globals (section-local). Five sources. No more. Keep. -
Keep
{{ item }}in fanout arms. Arms need to distinguish each other and carry arm-specific data.itemis that mechanism. Keep. -
Drop the
taskstable. Strings only forexecute/jump/fanouttargets.tasks["## Research"]is a handle around a string with no added value. If a lookup table ever returns, name itsections, nottasks. Change. -
Keep
list_from_section. Markdown lists are the natural way to write a collection. Building the same array in Lua is ugly (no formatting, no bullets). List sections feedfanout. Keep. -
Keep
execute(target, input?). Subroutines take anargsstring, same as the prompt itself. The input becomes the subroutine'sargs. Symmetry with the top-level run. Keep. -
Keep fanout results as objects with
.text,.ok,.item,.exhausted.__tostringcoerces to.textfortostring/table.concat. Already an object; no reason to strip it. Keep. -
sys.idshould be a global counter across all chains. Every unit of execution (section, fanout arm, execute chain) gets its own unique small integer. Entering the same section twice gives two IDs. Change from per-chain counting. -
varpersists across sections on the same walk (transport). H1 is on the same walk as top-level H2, so H1varwrites persist into H2.executeandfanoutstart new walks with clonedvar. Bare globals (x = 42withoutlocal) are section-local and visible to{{ x }}in prose. No new keyword, no new table.var= transport, globals = scratch. Change. -
Guard
varwrites at assignment (JSON-only). Bare globals are unguarded scratch; substituting a non-string global is the author's bug.sys.whenandsys.nowboth stay.sys.modelandsys.reply_finish_reasonstay lazy (appear when known).store.readon missing path is an error;store.existsreturns boolean. Keep. -
Keep
store.str_replaceunique-anchor semantics. Fails if not found or found more than once. Predictable. Keep. -
Keep
store.globwith glob patterns (*,**,?). Enough for now. Can add regex later if needed. Keep. -
Keep both
store.read(verbatim) andstore.read_numbered(with line numbers). Numbered is for provenance: any tool that reports where data came from by line number. Not just editing. Keep. -
Keep the store operation set minimal: write, append, read, read_numbered, str_replace, delete, glob, exists. No rename, copy, or partial write. Add later if needed. Keep.
-
Store paths create parents implicitly. No
mkdirceremony. Keep. -
store.str_replaceon a missing file is an error. You cannot edit what is not there. Keep. -
store.deleteon a missing file is silent. Idempotent delete. Change. -
store.existsandstore.globnever error. Queries return boolean or empty array. Keep. -
store.readandstore.read_numberedwith line range clamp to file bounds. Reading past EOF gives what exists. Models are approximate; do not error on out-of-range. Keep. -
store.writewith empty content creates an empty file. Valid state. Prevents errors later when something tries to read it. Keep. -
store.appendto a missing file creates it. Required for fanout: all arms can append without checkingexistsfirst. No ceremony. Keep. -
store.appendwith empty content is a no-op. Fine. Keep. -
jumpinside a local tool handler: allowed in principle (models orchestrate through tool calls), but likely hard to implement. Workaround: save the jump target, invoke it after the tool returns. Keepjumpnilled for now. Revisit if a real prompt needs it. -
Cap
fanoutconcurrency. Limit can be high (e.g. 1000) but must be finite. Prevents runaway resource use. Change. -
Fanout arm failure: hard errors (Lua, store, model) abort siblings. Soft exhaustion (hit
max_tool_iterations) lets siblings finish; the failed arm returns.ok = false,.exhausted = true. Keep. -
fanoutresults are in collection order.results[i]corresponds toitems[i]. Index each arm consistently. Keep. -
fanoutwith an empty collection is an error. No work is likely a bug. Revisit if a real use case appears. Change. -
fanoutworker with no prose (Lua-only) is allowed. Valid for compute / store writes with no model. Keep. -
fanoutworker that falls through with no reply:.textis empty string."done"would be a lie. Caller needs accurate information. Change. -
fanoutworker that jumps: allowed. Jump starts a child walk, same as a section. No special cases. Uniform behavior. Keep. -
fanoutworker that callsexecute: allowed. Subroutines are what makes computation powerful. Max depth limit (8, shared withexecute) still applies. Keep. -
fanoutworker that callsfanout: allowed. Same principle: uniform behavior, no special cases, max depth limit. Keep. -
fanoutworker that callsmodel:inferormodels.infer: allowed. Both are fresh context, no tools, noreplytouch. Uniform with regular sections. Keep. -
fanoutworker that callstools.addortools.add_local: allowed. Arms start from scratch (H1tools.alwaysbaseline only). They do not inherit the caller's tool bag. Keep. -
fanoutworker that callsmodels.use: allowed. Arms pick their own model from the captured bindings. Symmetric with sections. Keep. -
fanoutworker that callsstore.write/store.append: shared store. All arms see the same store. Keep. Consider: two arms callingwriteon the same path is a hard error (write-write race). Append is fine (order unspecified). Open. -
fanoutworker that callslog: allowed. Goes to the observer with the arm's section name. Symmetry principle. Keep. -
fanoutworker that callsuntrusted: allowed. Pure function. Keep. -
fanoutworker that callslist_from_section: allowed. Symmetry. Keep. -
Nested fanout inside
executeinside an arm: allowed. Depth cap is the only limit. Symmetry. Keep. -
fanoutworker that callsmodels.default: error.defaultis preamble-only. Arms usemodels.useto override. Keep. -
fanoutworker that callstools.needormodels.need: error.needis preamble-only (live resolution). Arms use captured bindings. Keep. -
Arms are sections plus
item. Same capabilities, no special cases.itemis the arm's identity (the collection member). Everything else is uniform. Keep. -
Add
sys.index: 1-based arm index within the current fanout. Nested fanout starts from 1. Absent outside fanout.sys.idis the global execution unit ID (note 46).sys.indexis the arm position. Different questions. Change. -
One
infershape only: one round, no tools, fresh conversation, never setsreply, never touchessys. Two forms:models.infer(prompt)(the section's current model) andmodels.get(alias):infer(prompt)(any declared model's handle). The tool-loopmodel:inferis deleted outright - tools advertised,reply/sys.reply_finish_reasonupdates,ToolBagand its generation cache all go; it was never a wanted capability. Supersedes the "separate; do not conflate" half of note 22 and matches note 71's description of both forms. A Lua block that needs tools usesexecuteon a section. Change. -
Note 17's
reply = nilescape was unimplemented: the walk carriedreplyas a Rust local and nothing read the Lua global back. Now the block runner reads thereplyglobal back after each Lua block (nil clears, a string carries, anything else errors), so the documented escape works on jumps and fall-through. Fixed and tested in the drift-fix commit. Change. -
Fanout arms seed
replyfrom the caller's section-start incoming reply (captured once inmake_control_globals), not the caller's current reply at the call site. Keep, and the fanout docs now state it. Keep. -
Note 83 naming residue:
engine.rs/arm.rs/support.rscomments andguide/src/lua.md's local-tools section still saidmodel:infer;design-core.mddescribed the in-contextmodel:inferand claimedjumpinsideexecute()is a hard error (superseded by note 11). Fixed in the drift-fix commit. Change. -
design-core.mdresidue beyond note 86: item 17 still claimed jump clears the cross-sectionreply(superseded by note 17; item 6 and the code already preserved it), and the section-lifecycle list still loaded the shared library before host injection with host APIs unavailable (the replay runs after setup with the full environment installed) and listed the droppedtaskstable in the inject set. Fixed in the same commit as this note. Change.