From 6e66fead0e488e35d337442775296d9953f82029 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Fri, 13 Nov 2026 19:30:16 -0800 Subject: [PATCH 1/2] State the shape rules of `var` and the keys that `opts` takes Two entries follow the code. The entry on `var` in `prompt-language.md` says that a write is checked by the rules of `json.encode` and that a write which would leave its table neither a list nor an object raises, with the reason for the rule. The entry on `tasks.spawn` in `tasks-and-fanout.md` says that `opts` takes `args`, `item`, and `index`, and that any other key raises. Plan: runvil-harness/plan/2026-11-13-3-json-args.plan.md --- decisions/prompt-language.md | 4 ++-- decisions/tasks-and-fanout.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/decisions/prompt-language.md b/decisions/prompt-language.md index cfaf80e..6229b26 100644 --- a/decisions/prompt-language.md +++ b/decisions/prompt-language.md @@ -259,9 +259,9 @@ The owner's reason: "Simpler implementation. Simpler mental model for authors. T ### `var` is a guarded table of JSON data -`var` is an empty proxy table over a hidden data table, and each table that an author reads from it is a view of the same kind. The proxy checks each write at the assigning line: a function, a userdata, or a thread raises an error that says the value `must be JSON data`, and a key that JSON data has no place for, such as a boolean or an index past the end of a list, raises an error that names the key. A view measures and iterates like the table it stands for: `#` gives the length of the data, `pairs` and `next` visit its entries, and `var.items[#var.items + 1] = x` appends to a list. +`var` is an empty proxy table over a hidden data table, and each table that an author reads from it is a view of the same kind. The proxy checks each write at the assigning line, by the rules of `json.encode`: a function, a userdata, or a thread raises an error that says the value `must be JSON data`, and a table that mixes list items and named keys raises an error that names it. A write that would leave its table neither a list nor an object raises too, such as a boolean key, a named field in a list, or an index past the end of a list. A view measures and iterates like the table it stands for: `#` gives the length of the data, `pairs` and `next` visit its entries, and `var.items[#var.items + 1] = x` appends to a list. -The proxy is empty because Lua calls `__newindex` only for a key that a table lacks, so only an empty table sees every write. The Engine sets `__len` on each view, and its own `pairs` and `next` read through one, so that an author treats a `var` table as a plain table. +The proxy is empty because Lua calls `__newindex` only for a key that a table lacks, so only an empty table sees every write. The guard refuses a write that `var` cannot hold as JSON data, where the write is made, because the read-back of `var` at the end of the block would otherwise fail on it or drop a part of it with no message. The Engine sets `__len` on each view, and its own `pairs` and `next` read through one, so that an author treats a `var` table as a plain table. ### `sys` is a sealed table of facts about the run and the section entry diff --git a/decisions/tasks-and-fanout.md b/decisions/tasks-and-fanout.md index c1cd0ee..4137067 100644 --- a/decisions/tasks-and-fanout.md +++ b/decisions/tasks-and-fanout.md @@ -42,7 +42,7 @@ They are methods of `Scheduler` because the owners depend on each other through ### `tasks.spawn` starts a chain over a section and returns a handle at once -`tasks.spawn(target, opts)` queues a new chain that starts at the target section, and returns before that chain runs. `opts.args` gives the chain its own arguments, a table of named fields or a string, by the rule of the second argument of `call` ([The prompt language](prompt-language.md)), and a chain that gets none reads its spawner's `args`. The handle is a plain table, `{ task = id }`, and every `tasks` function that names a task takes the handle or the id string. The `tasks` table has thirteen functions: `spawn`, `join_any`, `join`, `ready`, `status`, `pending`, `note`, `cancel`, `concurrency`, `receive`, `send`, `reply`, and `fanout`. +`tasks.spawn(target, opts)` queues a new chain that starts at the target section, and returns before that chain runs. `opts.args` gives the chain its own arguments, a table of named fields or a string, by the rule of the second argument of `call` ([The prompt language](prompt-language.md)), and a chain that gets none reads its spawner's `args`. `opts` takes the keys `args`, `item`, and `index`, and any other key raises an error that names it, so a misspelled option fails at the call. The handle is a plain table, `{ task = id }`, and every `tasks` function that names a task takes the handle or the id string. The `tasks` table has thirteen functions: `spawn`, `join_any`, `join`, `ready`, `status`, `pending`, `note`, `cancel`, `concurrency`, `receive`, `send`, `reply`, and `fanout`. ### The chain that spawns a task owns it From c45aaf53e427ac9f2244ccf4cc7c7830771c41c8 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Fri, 13 Nov 2026 19:59:44 -0800 Subject: [PATCH 2/2] State what the Run window's contract holds for `args` The contract route of Workshop sends a prompt's `args` declaration as JSON Schema and has no `input` field, and the Run window draws one row for each argument the prompt declares at the top level. `workshop-app.md` and `workshop-ui.md` say so. - `workshop-app.md` lists the fields of `ContractResponse` without `input` and names `ArgsSchema::as_json` as the source of `args`. - `workshop-ui.md` names the control of each argument type, and says that a string named `text` gets a multiline box. Plan: runvil-harness/plan/2026-11-13-3-json-args.plan.md --- decisions/workshop-app.md | 2 +- decisions/workshop-ui.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/decisions/workshop-app.md b/decisions/workshop-app.md index 6e81fe2..d351c58 100644 --- a/decisions/workshop-app.md +++ b/decisions/workshop-app.md @@ -194,7 +194,7 @@ The names are the Agent Client Protocol's because the draft of that protocol has ### `POST /prompts/contract` parses prompt text and returns what its front matter declares -The Run window posts a prompt's name and text. The route calls `Prompt::parse` and answers a `ContractResponse` with `name`, `description`, `runvil`, `max_tool_iterations`, `input`, `output`, `uses`, `args`, and `models`. A prompt that fails to parse answers 422 with a code such as `parse_frontmatter` or `parse_lua`. +The Run window posts a prompt's name and text. The route calls `Prompt::parse` and answers a `ContractResponse` with `name`, `description`, `runvil`, `max_tool_iterations`, `output`, `uses`, `args`, and `models`. `args` is the prompt's declaration as JSON Schema, as `ArgsSchema::as_json` returns it ([The prompt language](prompt-language.md)). A prompt that fails to parse answers 422 with a code such as `parse_frontmatter` or `parse_lua`. The route is in the server crate because it only parses a prompt: its work is independent of the granted-folder check, and in `workshop-workspace` it would be that crate's only reason to depend on the Engine. diff --git a/decisions/workshop-ui.md b/decisions/workshop-ui.md index 0d33a08..c4961b1 100644 --- a/decisions/workshop-ui.md +++ b/decisions/workshop-ui.md @@ -176,7 +176,7 @@ CodeMirror is a ready-made editor, and the interface isolates it from the rest o ### The Run window shows what a prompt declares, and its Run button has an empty click handler -Run > New Run Window runs `workbench.action.newRunWindow`, which opens a `run` panel in the main zone and loads the focused editor's file. The panel posts the prompt's text to the server route `POST /prompts/contract` and draws one row for each front-matter key except `uses`. The owner's rule: "let's keep the UI simple, one row for each thing." +Run > New Run Window runs `workbench.action.newRunWindow`, which opens a `run` panel in the main zone and loads the focused editor's file. The panel posts the prompt's text to the server route `POST /prompts/contract` and draws one row for each front-matter key except `uses`. For `args` it draws one row for each argument the prompt declares at the top level: a switch for a boolean, a number field for an integer or a number, a text field for a string, and a box of JSON text for a list or an object. A string named `text` gets a multiline box, because `text` is the Engine's name for free text. The owner's rule: "let's keep the UI simple, one row for each thing." The Run button exists so that the window has its final shape. Its click handler is empty because running a prompt from this window is still to be built (section 8).