lua-cli 3.32.1 โ†’ 3.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # lua-cli v3.32.1
1
+ # lua-cli v3.32.2
2
2
 
3
- Welcome to the comprehensive API documentation for lua-cli v3.32.1. This guide covers every API, class, and function exported by the package.
3
+ Welcome to the comprehensive API documentation for lua-cli v3.32.2. This guide covers every API, class, and function exported by the package.
4
4
 
5
5
  ## ๐Ÿ“š Documentation Index
6
6
 
@@ -40,7 +40,7 @@ A typed code step. `inputSchema` / `outputSchema` / `resumeSchema` are zod schem
40
40
  | `onError` | What the **final** failure does to the run: `'fail'` (default โ€” the run unwinds `failed`) ยท `'continue'` (the run goes on; the step's result is the **continued-failure value** `{ __lua_workflow:'continued_failure', failed:true, error:{code,message}, text:'' }` โ€” `${stepResults.<id>.text}` renders `''`, `getStepResult(id)` returns it, a `conditional` can branch on `stepResults.<id>.failed`) ยท `'park'` (the step parks on an exception gate for `retry-step` / `resolve-step` โ€” see [When a step parks](../workflows/recovery.md)) |
41
41
  | `requiredConnections` | Connection ids that must mount before `execute` runs |
42
42
  | `tier` / `workspace` / `jobResources` / `jobTools` | Job-tier fields โ€” see [Workspaces and long steps](../workflows/workspaces-and-long-steps.md) |
43
- | `execute(ctx)` | `ctx`: `inputData`, `resumeData`, `getInitData()`, `getStepResult(id)`, `state`, `suspend()`, `bail()`, `bailRun()`, `once()`, `log()`, `env`, `artefacts`, `occurrenceId`, `workspace?` |
43
+ | `execute(ctx)` | `ctx`: `inputData`, `resumeData`, `getInitData()`, `getStepResult(id)`, `state`, `suspend()`, `bail()`, `bailRun()`, `once()` (worker tier โ€” not on the Job tier yet, LUA-706), `log()`, `env`, `signal`, `runtime`, `artefacts`, `occurrenceId`, `lineageId`, `workspace?` (Job tier; `exec?` / `$?` with it) |
44
44
 
45
45
  `ctx.suspend(payload)` parks the step for `Workflows.resume(runId, stepId, resumeData)`; on resume **`execute` re-runs from the top** with `ctx.resumeData` set.
46
46
 
@@ -54,21 +54,21 @@ Config: `name`, `description?`, `inputSchema`, `outputSchema?`, `budget?` (`maxC
54
54
 
55
55
  **The call site places.** Every builder call โ€” `then`, `agentStep`, `specialistStep`, `toolStep`, `map`, `approval`, `waitForSignal`, `sleep`, `sleepUntil`, `foreach`, `parallel`, `branch`, `switch`, `dowhile`, `dountil`, `workflow` โ€” appends **exactly one** entry to the chain where it is called. Containers take `StepRef`s: an inline step object, or a **string** naming an `agentStep`/`specialistStep`/`toolStep` declared elsewhere in the same chain โ€” a string ref inside a container means "declare here, inside me" and does not append a second top-level entry. An id declared but never placed is `WORKFLOW_UNPLACED_STEP` (warning locally, error at push); referenced but never declared is `unknown-step-ref` at `.commit()`.
56
56
 
57
- | Verb | Entry |
58
- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
59
- | `.then(step, input?)` | Sequential step |
60
- | `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, โ€ฆ })` | Agent turn (`prompt` is a `template(...)`) |
61
- | `.specialistStep(id, { role, prompt, โ€ฆ })` | Ephemeral role on the owning agent (D25) |
62
- | `.toolStep(id, { toolId, input? })` | One tool call โ€” `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
63
- | `.map(descriptors, { id })` | Data reshaping โ€” `id` is **required** once the workflow has โ‰ฅ 2 maps (`map-id-required`) |
64
- | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm (lowered to an implicit subrun) |
65
- | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
- | `.foreach(step, { concurrency?, maxItems?, chunk?, rateLimit? })` / `.foreach({ items })` | Fan-out over an upstream array |
67
- | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
68
- | `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits โ€” `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
69
- | `.approval(id, { title, approver, timeoutHours, onTimeout, editable?, editablePaths?, itemsPath?, โ€ฆ })` | Human gate โ€” see [Approvals](../workflows/approvals.md) |
70
- | `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` โ€” completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }` |
71
- | `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
57
+ | Verb | Entry |
58
+ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `.then(step, input?)` | Sequential step |
60
+ | `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, โ€ฆ })` | Agent turn (`prompt` is a `template(...)`) |
61
+ | `.specialistStep(id, { role, prompt, โ€ฆ })` | Ephemeral role on the owning agent (D25) |
62
+ | `.toolStep(id, { toolId, input? })` | One tool call โ€” `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
63
+ | `.map(descriptors, { id })` | Data reshaping โ€” `id` is **required** once the workflow has โ‰ฅ 2 maps (`map-id-required`) |
64
+ | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm (lowered to an implicit subrun) |
65
+ | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
+ | `.foreach(step, { concurrency?, maxItems?, chunk?, rateLimit? })` / `.foreach({ items })` | Fan-out over an upstream array |
67
+ | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
68
+ | `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits โ€” `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
69
+ | `.approval(id, { title, approver, timeoutHours, onTimeout, editable?, editablePaths?, itemsPath?, โ€ฆ })` | Human gate โ€” see [Approvals](../workflows/approvals.md) |
70
+ | `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` โ€” completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }`; payload keys named like a credential (`token`, `secret`, `apiKey`, `accessToken`, โ€ฆ) are persisted `[REDACTED]` (the `signal-reserved-key` warning says so at push / compose) โ€” see [Reserved payload keys](Workflows.md#reserved-payload-keys) |
71
+ | `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
72
72
 
73
73
  ### Typed predicates
74
74
 
@@ -69,6 +69,16 @@ Resumes a step suspended with `ctx.suspend(...)`. The loser of a resume race rec
69
69
 
70
70
  Delivers a named signal to a run waiting on `waitForSignal(name)`. `opts.dedupeKey` makes a re-send a no-op (`{ accepted: true, duplicate: true }`). Returns `{ accepted, reason?: 'source_not_accepted' | 'duplicate' | 'parked' }`. The waiting step completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }` โ€” read the payload as `stepResults.<id>.payload`.
71
71
 
72
+ #### Reserved payload keys
73
+
74
+ The run ledger redacts by **key name**, not by value: a payload member whose key is one of
75
+
76
+ `secret`, `token`, `password`, `passwd`, `pwd`, `passphrase`, `api_key`, `apikey`, `access_key`, `secret_key`, `private_key`, `client_secret`, `authorization`, `auth_token`, `access_token`, `id_token`, `refresh_token`, `session_key`, `credential`, `credentials`
77
+
78
+ is persisted as `[REDACTED]` whatever it holds. A key matches when one of those names is a whole segment of it โ€” segments split on `_`, `-`, `.` and spaces, case-insensitive (`token`, `user_token`, `x-token`, `Token`; the compounds also as one camelCase word: `apiKey`, `refreshToken`, `accessToken`) โ€” while a name glued to another word (`tokens`, `tokenCount`, `accessTokenExpiry`) is not. A member whose value is an object or array is walked, not replaced whole.
79
+
80
+ This is by design (a credential must never land in a run), so the placeholder is what `stepResults.<id>.payload`, the run output, `lua workflows status --steps`, exports and the chat run card show โ€” a workflow cannot read a data field with one of these names back. Name data fields otherwise (`ticketRef`, `handle`, `code`, `verifier`). The same rule applies to every step output and suspend payload. A `waitForSignal` whose `schema` declares a property with a reserved name draws the non-blocking `signal-reserved-key` warning at push / compose (the property, the placeholder it would read as, the rename), and `lua workflows signal` warns before sending a payload that carries a reserved key (the signal is still delivered).
81
+
72
82
  ### Reserved members
73
83
 
74
84
  `startBatch`, `signalByKey`, `raiseBudget`, `setGoal` and `goals.{list,get,pause,resume,close}` are part of the API surface (the member list is frozen) and throw `WorkflowApiError { code: 'WORKFLOWS_API_UNAVAILABLE' }` until their server routes ship. For `startBatch`: from an agent turn, prefer the `startWorkflowRunBatch` tool (one consent card for the whole batch, D23-r5).
@@ -6,6 +6,7 @@ _Source of truth: workflows-spec (P1-3 / P1-14). This page is the developer summ
6
6
 
7
7
  - `Workflows.signalByKey(nameOrId, correlationKey, name, payload?)` delivers a signal **by business key** โ€” no runId bookkeeping in your webhook; `allowMultiple` fans out when several runs share the key.
8
8
  - `Workflows.list({ correlationKey })` / `lua workflows runs --correlation-key` find the run(s).
9
+ - A signal payload member named like a credential (`token`, `secret`, `password`, `apiKey`, `authorization`, `credentials`, โ€ฆ) is persisted `[REDACTED]` by design and cannot be read back by the workflow โ€” name data fields otherwise; the full list and the rule are under [Reserved payload keys](../api/Workflows.md#reserved-payload-keys). `lua workflows signal` warns before sending one.
9
10
  - `tags` are free-form labels (AND-ed in list filters); `replyTo: { channel, threadId }` routes the terminal reply back to the originating conversation.
10
11
 
11
12
  Offline: `--correlation-key`, `--tag`, `--reply-to <channel>:<threadId>` and `--on-behalf-of` stamp `ctx.runtime.*`; the would-be reply prints on the terminal instead of sending.
@@ -4,6 +4,12 @@ _Source of truth: workflows-spec 10 / 16. This page is the developer summary; th
4
4
 
5
5
  Per-run: `budget.maxCredits` (over the org's `maxCreditsPerRun` default 250 โ‡’ push blocker `budget-exceeds-cap` until an org admin raises it, R23), `budget.maxDurationSeconds`, `budget.maxJobSeconds`. Structural: `foreach` `maxItems` (default 256 โ€” with `chunk:{size}` the cap bounds CHUNKS), loop `maxIterations` (default 100), nesting depth โ‰ค 3, `startBatch` per-item `idempotencyKey`. Push-time caps preflight reports `warnings[]` + `blockers[]`; `lua push --strict-caps` promotes warnings.
6
6
 
7
+ ## Credits
8
+
9
+ `budget.maxCredits` counts agent steps, never tokens. An inline agent step settles a flat **1** credit when it completes; a `tier:'job'` attempt settles a flat **4** at its first claim (a retry is a new attempt; a seat-metered org settles 0 and is metered from the `workflow:job` usage event instead). Before dispatching an agent step the engine checks `remaining = maxCredits โˆ’ spent โˆ’ reserved` against the step's reserve (1 inline, 4 Job) and parks the run `suspended{gate.kind:'budget'}` โ€” `nextAction:'raise_budget'`, `lua workflows raise-budget <runId> --credits <n>` โ€” when it is short. So `maxCredits: 40` buys ten Job-tier attempts, and a `maxCredits` under 4 can never dispatch one.
10
+
11
+ Nothing meters a Job-tier attempt's tokens against the credits while it runs. Each attempt is bounded by its own ceilings: the step wall (`timeoutSeconds`), `maxInputTokens` (input-side tokens โ€” prompt + cache read + cache creation โ€” default **4M**, โ‰ˆ $1โ€“13 per attempt on a Sonnet-class model; 1M..500M), `maxMessages` (default 400) and `maxTurns`. Crossing one ends the attempt `attempt_budget_exhausted` from its last checkpoint (see [Long steps and checkpoints](long-steps-and-checkpoints.md)). Size `maxInputTokens` for what one attempt may cost and `maxCredits` for how many attempts the run may make.
12
+
7
13
  ## Org pacing
8
14
 
9
15
  `workflowPolicy.pacing` is an org-admin rate limit **per outbound host** shared by every run in the org. A paced step shows as `ready (pacing_deferred โ€ฆ)` and counts against no per-run budget except `maxDurationSeconds`. Code-step `fetch` calls are **not** paced (B26, normative) โ€” pacing governs `tool`/`http` steps. Only `lua features` / the desktop **Workflows policy** page / R23 can change it.
@@ -5,9 +5,15 @@ _Source of truth: workflows-spec 04 ยง4.3 (D15-r2, Cluster J B12 โ€” WF-527, WB-
5
5
  A script workflow is a plain JavaScript ES module โ€” no TypeScript, no `import`/`require` โ€” that begins with `export const meta = {โ€ฆ}` and continues with a body run in an async context whose top-level `return` is the run output. The host API (`agent`, `tool`, `parallel`, `foreach`, `sleep`, `approval`, `waitForSignal`, `memo`, `step`, `shell`, `merge`, `log`, `phase`, โ€ฆ) is injected as module-scope bindings; the same file is what the composer produces (`lua workflows export`) and what a template freezes.
6
6
 
7
7
  ```js
8
- export const meta = { name: 'judge-panel', description: 'Three drafts, three judges, synthesize the winner',
9
- phases: [{ title: 'Draft' }, { title: 'Judge' }, { title: 'Synthesize' }] };
10
- const drafts = await parallel([agent('Draft, technical angle', { phase: 'Draft' }), agent('Draft, market angle', { phase: 'Draft' })]);
8
+ export const meta = {
9
+ name: 'judge-panel',
10
+ description: 'Three drafts, three judges, synthesize the winner',
11
+ phases: [{ title: 'Draft' }, { title: 'Judge' }, { title: 'Synthesize' }],
12
+ };
13
+ const drafts = await parallel([
14
+ agent('Draft, technical angle', { phase: 'Draft' }),
15
+ agent('Draft, market angle', { phase: 'Draft' }),
16
+ ]);
11
17
  const scores = await foreach(drafts, (d) => agent(`Score this draft: ${d}`, { phase: 'Judge' }));
12
18
  const picked = await step('pick-winner', () => ({ winner: drafts[scores.indexOf(Math.max(...scores))] }));
13
19
  return picked;
@@ -27,17 +33,21 @@ Run in this order โ€” the same order lua-api re-validates on push (one implement
27
33
 
28
34
  `lua compile` discovers every `src/workflows/*.workflow.script.js` (outside the TypeScript project โ€” never bundled) and emits `ManifestWorkflow{ form:'script', script, scriptHash, meta, journalProtocolVersion: 1, scriptSurface, roleBindings }` โ€” the graph members are absent. `lua push` sends `{ form:'script', script, scriptHash, meta, journalProtocolVersion, roleBindings, schedule?, scheduleInput?, budget? }` and skips the bundle upload. The stored row is `WorkflowVersion{ form:'script', dynamic:false }` โ€” identical to a composed script minus `dynamic:true`. `WORKFLOW_SCRIPT_FORM_NOT_PUSHABLE` is struck (never emitted). An org whose `workflowPolicy.composeForm` is `'graph'` refuses the push with `compose-form-forbidden`, exactly as it refuses a composed script.
29
35
 
36
+ **The parent's form (LUA-717).** `lua push` creates a workflow the server does not have with `form` taken from the compiled definition (`POST /developer/workflows/:agentId { name, description, form:'script' }`). A workflow's form is fixed by its first version: a version whose form differs from the parent's is refused `400 WORKFLOW_FORM_MISMATCH { workflowForm, versionForm }` โ€” rename the script, or delete the workflow (`lua workflows delete`) and push again. The one exception is an **empty** parent (no version of any kind, sandbox included), which adopts the form of its first pushed version: a lua-cli older than the LUA-717 release sent no `form` on create, so its script workflows were left as graph shells, and the first script push is what makes such a shell usable.
37
+
38
+ **Script workflows are pushed from the CLI only.** Chat compose (`composeWorkflow`, `POST โ€ฆ/compose`) composes **graph** workflows; a `form:'script'` compose is refused `script-validation-tick-unavailable`, because the WF-323 validation tick is not bound on the production server (by design โ€” a script is never persisted un-validated). A script that should exist on the server goes through `src/workflows/<name>.workflow.script.js` + `lua push workflow`.
39
+
30
40
  ## `lua test workflow <name>` on a script
31
41
 
32
42
  Runs the file through the runner's own replay wrapper (parity context) in an offline tick loop: every tick re-runs the script against the journal so far, the wrapper parks with its async intents, the driver settles each with a **fake completion** (no API call) and re-ticks until `done`/`bailed`/`failed`. Steering:
33
43
 
34
- | Flag | Effect |
35
- | --- | --- |
36
- | `--input <json|@file>` | the script's `args` (default `meta.sampleArgs ?? {}`) |
37
- | `--step-output <label|seq>=<json>` | the completion for the intent with that `narration.label` (an `agent()`'s `label`/`phase`) or journal seq |
38
- | `--max-ticks <n>` | default 64 โ€” `SCRIPT_TICK_LIMIT` (exit 4) beyond it |
39
- | `--ledger-out <file>` | the local journal (`{ form:'script', journal[] }`) |
40
- | `--json` | the result envelope |
44
+ | Flag | Effect |
45
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
46
+ | `--input <json\|@file>` | the script's `args` (default `meta.sampleArgs ?? {}`) |
47
+ | `--step-output <label\|seq>=<json>` | the completion for the intent with that `narration.label` (an `agent()`'s `label`/`phase`) or journal seq |
48
+ | `--max-ticks <n>` | default 64 โ€” `SCRIPT_TICK_LIMIT` (exit 4) beyond it |
49
+ | `--ledger-out <file>` | the local journal (`{ form:'script', journal[] }`) |
50
+ | `--json` | the result envelope |
41
51
 
42
52
  Exit 0 `done`/`bailed`; 4 `failed` (`SCRIPT_THREW`, `SCRIPT_DEADLOCK` โ€” parked with zero intents, `SCRIPT_TICK_LIMIT`); 3 when `@lua/sandbox-runtime` is not resolvable from the project (`REPLAY_RUNTIME_UNAVAILABLE`, see `replay-local.md`); 2 on flag grammar.
43
53
 
@@ -8,25 +8,26 @@ _Source of truth: workflows-spec 03 ยง3.9 (WF-204/WF-225/WF-332). This page is t
8
8
 
9
9
  ## Steering flags โ€” fully scriptable, no stdin
10
10
 
11
- | Flag | Effect |
12
- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `--input @file\|<json>` | Run input (validated against `inputSchema`) |
14
- | `--step-output <id>=@file\|<json>` (rep.) | Completes the row **without** running it โ€” validated against the step's `outputSchema`. This is how a predicate over an agent output gets BOTH truth values under `--agents fake` |
15
- | `--approve <id>[=@payload]` ยท `--deny <id>[=@reason]` | Pre-answer approvals (edited payloads use the server's `matchesEditablePath` + `editedPayloadSchema`) |
16
- | `--signal <name>=@file\|<json>` (rep.) | Pre-supply `waitForSignal` payloads in order of arrival |
17
- | `--fixtures <dir>` / `--record <dir>` | Replay / record agent+tool outputs (`<stepId>.<attempt>.json`); a missing fixture is exit **5** `FIXTURE_MISSING`, never a silent fake; `--step-output` wins over a fixture |
18
- | `--from-run <runId>` | Seed every `completed` step from a real run (same math as repair runs); graph drift asks for `--force` |
19
- | `--park <id>` (rep.) | Simulate a platform-fault park of a `sideEffects:'external'` step โ€” then `retry / skip / complete / fail`, the production verbs (see [When a step parks](./recovery.md)) |
20
- | `--fast-retries` | Collapse retry backoff waits to 0 |
21
- | `--real-time` | Actually wait on `sleep`/backoff instead of fast-forwarding the virtual clock |
22
- | `--now <iso>` | Pin the virtual clock start (resolves `sleepUntil` dateFrom, business-hours deadlines) |
23
- | `--artefacts-dir <dir>` | Back `ctx.artefacts.*` on disk (`<dir>/<artefactId>` + `<artefactId>.meta.json`, ids `local-<n>`) |
24
- | `--env KEY=value` (rep.) | The local `env.template()` overlay โ€” a missing key is exit 2 `env-template-missing`; values never reach `--ledger-out` |
25
- | `--step-wall <s>` | Per-step wall, default 600 |
26
- | `--max-foreach-items <n>` | Override the local foreach cap |
27
- | `--agents fake\|live` | Fake stubs (default) or the dev API |
28
- | `--ledger-out <file>` | Write the in-memory ledger (steps, outputs, attempts, effects, events) โ€” the input of `replay --local` |
29
- | `--json` | Machine-readable result envelope |
11
+ | Flag | Effect |
12
+ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `--input @file\|<json>` | Run input (validated against `inputSchema`) |
14
+ | `--step-output <id>=@file\|<json>` (rep.) | Completes the row **without** running it โ€” validated against the step's `outputSchema`. This is how a predicate over an agent output gets BOTH truth values under `--agents fake` |
15
+ | `--approve <id>[=@payload]` ยท `--deny <id>[=@reason]` | Pre-answer approvals (edited payloads use the server's `matchesEditablePath` + `editedPayloadSchema`) |
16
+ | `--signal <name>=@file\|<json>` (rep.) | Pre-supply `waitForSignal` payloads in order of arrival |
17
+ | `--fixtures <dir>` / `--record <dir>` | Replay / record agent+tool outputs (`<stepId>.<attempt>.json`); a missing fixture is exit **5** `FIXTURE_MISSING`, never a silent fake; `--step-output` wins over a fixture |
18
+ | `--from-run <runId>` | Seed every `completed` step from a real run (same math as repair runs); graph drift asks for `--force` |
19
+ | `--park <id>` (rep.) | Simulate a platform-fault park of a `sideEffects:'external'` step โ€” then `retry / skip / complete / fail`, the production verbs (see [When a step parks](./recovery.md)) |
20
+ | `--fast-retries` | Collapse retry backoff waits to 0 |
21
+ | `--real-time` | Actually wait on `sleep`/backoff instead of fast-forwarding the virtual clock |
22
+ | `--now <iso>` | Pin the virtual clock start (resolves `sleepUntil` dateFrom, business-hours deadlines) |
23
+ | `--artefacts-dir <dir>` | Back `ctx.artefacts.*` on disk (`<dir>/<artefactId>` + `<artefactId>.meta.json`, ids `local-<n>`) |
24
+ | `--env KEY=value` (rep.) | The local `env.template()` overlay โ€” a missing key is exit 2 `env-template-missing`; values never reach `--ledger-out` |
25
+ | `--step-wall <s>` | The cap on every step wall, default 600 โ€” a node's own `timeoutSeconds` (or its step definition's) applies below it; a step past its wall fails `TIMEOUT` and its retry policy applies; the attempt's `ctx.signal` aborts at the wall โ€” the driver cannot kill in-process code, so a step that ignores the signal keeps running in the background until it settles (LUA-689) |
26
+ | `--max-foreach-items <n>` | Override the local foreach cap |
27
+ | `--agents fake\|live` | Fake stubs (default) or the dev API |
28
+ | `--ledger-out <file>` | Write the in-memory ledger (steps, outputs, attempts, effects, events) โ€” the input of `replay --local` |
29
+ | `--json` | Machine-readable result envelope |
30
+ | `--workspace <dir>` | The checkout Job-tier **code** steps run in: gives them `ctx.workspace` and `ctx.exec` / `ctx.$` against that directory โ€” the pod's allowlist (`git`, `gh`, `npm`, โ€ฆ, argv only, no shell) on your own PATH and git credentials. Without it a Job-tier step's `$` rejects `ExecError{code:'EXEC_UNAVAILABLE'}` naming the flag |
30
31
 
31
32
  ## Semantics worth knowing
32
33
 
@@ -34,7 +35,8 @@ _Source of truth: workflows-spec 03 ยง3.9 (WF-204/WF-225/WF-332). This page is t
34
35
  - **Retry backoff** runs on the virtual clock: `backoffSeconds ยท 2^(attempt-1)` capped at `maxBackoffSeconds`, printed as `[retry:<id>] backoff <n>s`.
35
36
  - **`foreach` `rateLimit`** is an in-memory token bucket โ€” delayed starts print `[throttle:<id>] resumeAt=<iso>`; loop `intervalSeconds` prints `kind=loop_interval`.
36
37
  - **`suspend`** re-runs `execute` from the top with `resumeData` set โ€” the documented Mastra semantics, exercised offline.
37
- - **`ctx.once`** settles against the in-memory effects map: a re-drive over the same ledger sends nothing twice.
38
+ - **`ctx.once`** settles against the in-memory effects map: a re-drive over the same ledger sends nothing twice. (On the platform it is served to worker-tier steps; the Job pod does not serve `ctx.once` yet โ€” LUA-706 โ€” so a Job-tier step dedupes on `occurrenceId` itself.)
39
+ - **`ctx.getInitData()`** is `null` on a run without `--input` and **`ctx.runtime`** is `{ trigger:'sdk', principalKind:'user' }` โ€” the same shapes the pod and the worker serve, so a step that branches on them behaves offline as it does on the platform.
38
40
  - **Not emulated offline**: the org pacing policy (no org context โ€” `pacing_deferred` never appears locally), the runner mid-run kill (Ctrl-C is the local analogue), `backfillOnEnable`, and the `startWorkflowRunBatch` tool.
39
41
 
40
42
  ## Example
@@ -6,7 +6,7 @@ _Source of truth: workflows-spec 05 ยง5.17 (D19-r1/r2). This page is the develop
6
6
 
7
7
  ## Workspaces
8
8
 
9
- `workspace` on `createWorkflow` declares the checkout: `{ kind:'git', repo, ref, credentialsRef, sizeGb?, verify? }` or `{ kind:'empty' }`. Steps mount it with `workspace: { mount: 'rw'|'ro', isolation?: 'worktree' }` and read `ctx.workspace = { path, mount, branch }`.
9
+ `workspace` on `createWorkflow` declares the checkout: `{ kind:'git', repo, ref, credentialsRef, sizeGb?, verify? }` or `{ kind:'empty' }`. Steps mount it with `workspace: { mount: 'rw'|'ro', isolation?: 'worktree' }` and read `ctx.workspace = { root, mount, branch, headSha, isolation, baseSha?, arm?, backend? }` โ€” `root` is the absolute directory of the checkout (`/workspace`); `baseSha` is the commit the run's base ref resolved to at provision (`headSha` is where this step's checkout is), `arm` the worktree arm id under `isolation:'worktree'`, `backend` the volume backend โ€” the three are present when the pod hands the run's stamps to the step and absent otherwise (an `empty` workspace, a shared mount). The pre-3.33 name `path` is served by the Job pod as a deprecated alias for one minor (it warns once per step) and then removed; new code reads `root`.
10
10
 
11
11
  `credentialsRef` is a connection id **or a key you declare under `connections: [{ key, integrationType }]`** โ€” declare a key; it resolves against the owner agent's own connections on any agent at run time (see [Git credentials](./git-credentials.md)), so the definition never freezes a production id.
12
12
 
@@ -14,6 +14,40 @@ _Source of truth: workflows-spec 05 ยง5.17 (D19-r1/r2). This page is the develop
14
14
 
15
15
  **Worktree arms:** parallel arms never share a filesystem; each gets its own clone and branch and the `merge` step integrates them โ€” put `verify` on the workspace so an agent-resolved conflict must pass your tests. `onConflict:'agent'` lets one resolver turn fix conflicts.
16
16
 
17
+ ## Commands from a code step โ€” `ctx.exec` / `ctx.$`
18
+
19
+ A Job-tier **code** step has no `child_process` โ€” an import of it fails `lua compile` with `node-capability-unavailable` โ€” and it builds no code from strings and compiles no WebAssembly (see the note at the end of this section). It runs commands through `ctx.exec` / `ctx.$`, which the Job pod executes on its behalf:
20
+
21
+ ```ts
22
+ const runTests = createStep({
23
+ id: 'runTests',
24
+ tier: 'job',
25
+ workspace: { mount: 'rw' },
26
+ inputSchema: z.any(),
27
+ outputSchema: z.object({ passed: z.boolean() }),
28
+ async execute({ $, exec, inputData, log }) {
29
+ await $!.strict`npm ci`; // `.strict` throws ExecError on a non-zero exit
30
+ const r = await $!`npm test -- --maxWorkers=2`; // { code, stdout, stderr, durationMs, truncated, timedOut }
31
+ const sha = (await exec!(['git', 'rev-parse', 'HEAD'], { cwd: 'packages/app' })).stdout.trim();
32
+ await $!.strict`git add -- ${'CHANGELOG.md'}`; // each ${โ€ฆ} is ONE argument; `--` keeps a data value out of git's options
33
+ log(`tests ${r.code === 0 ? 'passed' : 'failed'} at ${sha}`);
34
+ return { passed: r.code === 0 };
35
+ },
36
+ });
37
+ ```
38
+
39
+ - **Argv only, never a shell.** `$` splits its literal text on whitespace (with `'โ€ฆ'` / `"โ€ฆ"` quoting) and passes every `${value}` as exactly one argument, never re-parsed โ€” a title with spaces or a body with newlines is one argument. There is no `cd`, `&&`, `|`, glob or `$VAR`: a second command is a second call, and the cwd is the workspace root (`{ cwd: 'packages/app' }` for a directory inside it).
40
+ - **Allowlist.** `git`, `gh`, `pnpm`, `npm`, `npx`, `node`, `yarn`, `python3`, `pytest`, `make` โ€” bare names, resolved from the pod image's own PATH. `sh -c`, `bash`, `curl` or a path is refused before anything runs (`ExecError{code:'EXEC_REFUSED'}`, `reason` says why). **The allowlist is not the image's inventory:** today's Job image ships `git`, `gh`, `node`, `npm`, `npx` (plus `corepack`, `gitleaks`, `claude`, `curl` for the harness); `pnpm`, `yarn`, `python3`, `pytest` and `make` are on the list for the local driver and a future image but fail `EXEC_REFUSED/binary_not_found` in the pod today โ€” run them offline, or use `npx` / `npm exec` for what npm can fetch.
41
+ - **git rules.** The same as a coding turn's git tool: no `-C`, `--git-dir`, `--work-tree`, `--exec-path` (the repository is the workspace and the binary is the host's โ€” use `{ cwd }`); no `-c` over what git would execute or route through (`core.hooksPath`, `core.fsmonitor`, `protocol.*`, `credential.*`, `url.*`, proxies, editors, filters); no `git config` (the commit identity and every setting a step needs come from the host); no `credential*`, `push-mirror`, `daemon`. **`git push`** is refused on an `ro` mount and with `--force` / `-f` / `--force-with-lease` / `--delete` / `--mirror` / `--prune` / `--all` / `--tags`, and every push first runs the same pre-push secret scan the harness's own push runs (05 ยง5.17.6) โ€” a finding refuses it (`EXEC_REFUSED/git_push_refused`). Put `--` before positional values that come from data: `$\`git add -- ${file}\``, `$\`git log -- ${path}\``.
42
+ - **Budget.** `timeoutMs` (default 10 min) is capped by the step's remaining wall; on expiry the command's whole process tree is killed and the call rejects `EXEC_TIMEOUT`. stdout and stderr are each kept to 1 MiB (`truncated`). One command runs at a time.
43
+ - **Env.** The command sees a scrubbed env โ€” no token, no `LUA_WF_*`, no cloud credential. git runs exactly as the harness's own git does: through the credential-proxy remote, hooks and other repo-configured commands off, `GIT_TERMINAL_PROMPT=0`, the run's commit identity; `gh` and the package managers run non-interactive. A step that declares `jobTools: ['gh']` gives its `gh` (and only `gh`) the sidecar's gh proxy env, exactly what a coding turn's `gh` tool gets โ€” the proxy admits `gh pr create` / `gh pr edit` / `gh pr comment` on the pinned repository and nothing else: **no `gh pr merge`** (a merge stays with a person), no `/graphql`. `env: { NODE_ENV: 'test' }` adds variables; `PATH`, `HOME`, `GIT_*`, `GH_*`, `LUA_*`, `NODE_OPTIONS` cannot be overridden.
44
+ - **Errors.** A non-zero exit is data (`result.code`). `exec.strict` / `$.strict` throw an `ExecError` โ€” `name === 'ExecError'`, `code` one of `EXEC_FAILED` | `EXEC_TIMEOUT` | `EXEC_REFUSED` | `EXEC_UNAVAILABLE`, with `exitCode`, `stdout`, `stderr`. Check `name` / `code`, not `instanceof` (the pod runs a step in its own realm). `undefined` interpolated into `$` is refused, never stringified.
45
+ - **Tier.** Worker-tier steps have neither `exec` nor `$`. Shell work with a model in the loop is a Job-tier `agentStep` (its `shell` tool, below). The command line, exit code and duration are logged to the run's job-logs; the output is not โ€” `log()` what matters.
46
+
47
+ An `ro` mount is read-only at the volume (the PVC is mounted `readOnly`): a command that writes fails `EROFS`, and `git push` is refused on it.
48
+
49
+ **No code from strings, no WebAssembly โ€” Job tier only.** Inside a Job-tier code step, `eval`, `new Function` (the `Function` constructor reached any way, `Buffer.constructor` included) and `new WebAssembly.Module` / `WebAssembly.compile` / `WebAssembly.instantiate` throw โ€” `EvalError` / `CompileError`, surfaced as `USER_CODE_ERROR` with the reason named โ€” because the pod runs the step with code generation off in both realms (`--disallow-code-generation-from-strings` on the child process, `codeGeneration: { strings: false, wasm: false }` on the vm context). `WebAssembly.validate` still answers (it compiles nothing). A library that builds code from strings at import time fails the same way. The worker tier has neither restriction: a step that needs either stays `tier: 'worker'`.
50
+
17
51
  ## Coding turns
18
52
 
19
53
  A Job-tier `agentStep` is a coding turn (Claude Code or the generic harness โ€” see [Coding harness](./coding-harness.md)) with the `WORKFLOW_JOB_TOOLS` set (`shell`, `read`, `write`, `edit`, `glob`, `grep`, `git`, โ€ฆ). MCP connections mount via `toolScope.connectionIds`: your connections are mounted through a local proxy; the model never sees a token, and a tool that needs approval is refused inside the turn โ€” hand it to a worker-tier step.
@@ -26,4 +60,4 @@ GitHub review loops: the PR body carries `<!-- lua-run:<runId> -->`; the GitHub
26
60
 
27
61
  ## Offline
28
62
 
29
- The local driver emulates the tier: `--workspace <dir>`, `--job-wall <s>`, worktree arms as literal `git worktree add`, `--segment-wall` for segment rehearsal. `credentialsRef` is printed and never resolved offline.
63
+ The local driver emulates the tier: `--workspace <dir>` gives Job-tier code steps `ctx.workspace` and `ctx.exec` / `ctx.$` against that directory (the pod's allowlist, your own PATH and git credentials; without the flag a Job-tier step's `$` rejects `EXEC_UNAVAILABLE`), `--job-wall <s>` rehearses the segment wall for agent steps. `credentialsRef` is printed and never resolved offline.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.32.1",
3
+ "version": "3.32.2",
4
4
  "description": "Build, test, and deploy AI agents with custom tools, webhooks, and scheduled jobs. Features LuaAgent unified configuration, streaming chat, and batch deployment.",
5
5
  "readmeFilename": "README.md",
6
6
  "main": "dist/api-exports.js",
@@ -114,9 +114,9 @@
114
114
  "ts-node": "^10.9.2",
115
115
  "tsup": "^8.5.1",
116
116
  "@lua/shared-sandbox": "0.0.1",
117
- "@lua/workflow-graph": "0.0.1",
118
117
  "@lua/shared-source-sync": "0.0.1",
119
- "@lua/shared-types": "0.0.1"
118
+ "@lua/shared-types": "0.0.1",
119
+ "@lua/workflow-graph": "0.0.1"
120
120
  },
121
121
  "scripts": {
122
122
  "clean": "rm -rf dist temp",
@@ -13,9 +13,13 @@ const pushFix = createStep({
13
13
  tier: 'job',
14
14
  workspace: { mount: 'rw' },
15
15
  timeoutSeconds: 600,
16
- async execute({ workspace }) {
17
- // git runs through the credential proxy โ€” no token in this container (05 ยง5.17.3)
18
- const sha = (await $`git -C ${workspace!.path} rev-parse HEAD`).stdout.trim(); // `$` = the Job image's shell helper (`@lua/coding-harness/shell`); a plain child_process spawn works too โ€” the Job site has no REQUIRE_BLOCKLIST for `child_process`
16
+ async execute({ $, workspace, log }) {
17
+ // `ctx.$` (LUA-682) runs an allow-listed binary in the checkout โ€” argv only, no shell โ€” with git's remote on the
18
+ // credential proxy: no token in this container (05 ยง5.17.3). `child_process` is NOT available to a code step
19
+ // (`lua compile` refuses it: node-capability-unavailable); a coding turn's `shell` tool is the other way to run
20
+ // commands. The harness pushes the branch at the checkpoint / terminal; this step reports what it pushed.
21
+ const sha = (await $!.strict`git rev-parse HEAD`).stdout.trim();
22
+ log(`pushed ${sha} on ${workspace!.branch ?? 'the run branch'}`);
19
23
  return { headSha: sha };
20
24
  },
21
25
  });
@@ -1,4 +1,4 @@
1
- // Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR -> review loop -> approval -> merge.
1
+ // Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR (coding turn) -> review loop -> approval -> the decision (the merge stays human).
2
2
  // From workflows-spec 03 ยง3.2 (f) (WF-215 / WF-223 โ€” the spec is normative). LUA-635: `openPr` is placed by `.then(openPr)` on
3
3
  // both test outcomes โ€” the listing's `otherwise: 'openPr'` named a `.then(createStep)` placement, which never declares an id
4
4
  // (03 ยง3.2.0); a string arm names an agentStep / specialistStep / toolStep / map / workflow declaration. `mergeGate.title` is a
@@ -16,49 +16,43 @@ const runTests = createStep({
16
16
  workspace: { mount: 'rw' },
17
17
  timeoutSeconds: 3600,
18
18
  jobResources: 'large', // a 15โ€“40 min monorepo suite fits one 4 h segment (D19-r1); up to 86 400 s is legal since D19-r2 โ€” see ยง3.2 (h) for a step that crosses the segment boundary
19
- async execute({ workspace, log }) {
20
- const r = await $`cd ${workspace!.path} && npm ci && npm test -- --maxWorkers=2`.nothrow(); // untracked node_modules were rebuilt on restore (05 ยง5.17.5, E12); `--maxWorkers=2` keeps a mongodb-memory-server-per-file suite inside `large`'s 8 GiB โ€” over it the container is OOM-killed and the step fails `job_oom_killed` (customer fault, 05 ยง5.17.4; ยง3.12 note)
19
+ async execute({ $, log }) {
20
+ // `ctx.$` runs ONE allow-listed binary per call, argv only, in the checkout (LUA-682): no `cd`, no `&&` โ€” the
21
+ // cwd is the workspace root and a second command is a second call. `.strict` throws on a non-zero exit; the
22
+ // plain form returns it, which is what a test run wants.
23
+ await $!.strict`npm ci`; // untracked node_modules were rebuilt on restore (05 ยง5.17.5, E12)
24
+ const r = await $!`npm test -- --maxWorkers=2`; // `--maxWorkers=2` keeps a mongodb-memory-server-per-file suite inside `large`'s 8 GiB โ€” over it the container is OOM-killed and the step fails `job_oom_killed` (customer fault, 05 ยง5.17.4; ยง3.12 note)
21
25
  log(r.stdout.slice(-4000));
22
- return { passed: r.exitCode === 0, summary: r.stdout.slice(-2000) };
26
+ return { passed: r.code === 0, summary: r.stdout.slice(-2000) };
23
27
  },
24
28
  });
25
29
 
26
- const openPr = createStep({
27
- id: 'openPr',
28
- inputSchema: z.object({ ticketId: z.string(), title: z.string() }),
29
- outputSchema: z.object({ prNumber: z.number(), url: z.string() }),
30
- tier: 'job',
31
- workspace: { mount: 'ro' },
32
- sideEffects: 'external',
33
- onError: 'park', // a PR is an external effect: a platform fault PARKS (06 ยง6.3.5); `gh` is idempotent on the branch anyway
34
- async execute({ workspace, inputData, runId }) {
35
- const out =
36
- await $`cd ${workspace!.path} && gh pr create --fill --title ${inputData.title} --body ${`Closes ${inputData.ticketId}\n\n<!-- lua-run:${runId} -->`}`; // the run id in the PR body is what the GitHub webhook uses to route review signals (ยง3.7)
37
- const url = out.stdout.trim();
38
- return { prNumber: Number(url.split('/').pop()), url };
39
- },
40
- });
30
+ // `openPr` is a CODING TURN (05 ยง5.17.6) โ€” the pilot's real pattern: the harness's own `gh` tool talks to the sidecar's
31
+ // gh proxy. A Job-tier code step can run `gh` too (`ctx.$`, with `jobTools:['gh']` on the step), but the proxy admits
32
+ // `gh pr create` / `gh pr edit` / `gh pr comment` on the pinned repo only โ€” no merge, no /graphql (T17-D-SE25) โ€” and the
33
+ // end-to-end path is the sidecar's to prove; the coding turn is what ships PRs today.
41
34
 
42
- const mergePr = createStep({
43
- id: 'mergePr',
44
- inputSchema: z.object({ approved: z.boolean(), input: z.object({ prNumber: z.number() }) }),
45
- outputSchema: z.object({ merged: z.boolean() }),
46
- tier: 'job',
47
- workspace: { mount: 'ro' },
48
- jobTools: ['gh'],
49
- sideEffects: 'external',
50
- onError: 'park', // `jobTools:['gh']` is what mints GH_TOKEN for a code step (11 ยง11.5.6: pull_requests:write + contents:write + issues:read, repo-scoped โ€” merge is a contents write on GitHub)
51
- async execute({ workspace, inputData }) {
52
- if (!inputData.approved) return { merged: false };
53
- await $`cd ${workspace!.path} && gh pr merge ${inputData.input.prNumber} --squash --delete-branch`; // succeeds only if the base branch's ruleset lets the App merge โ€” human-only-merge orgs keep contents:write off the App (11 T21)
54
- return { merged: true };
35
+ const recordDecision = createStep({
36
+ id: 'recordDecision',
37
+ inputSchema: z.object({ approved: z.boolean(), prNumber: z.number() }), // projected by the `mergeInput` map below โ€” an approval's output carries `approved` (and `decidedBy`, `editRevision`โ€ฆ), never the payload it was shown (docs/workflows/approvals.md), so the PR number is read back from `openPr`
38
+ outputSchema: z.object({ approved: z.boolean(), prNumber: z.number(), merged: z.boolean() }),
39
+ async execute({ inputData, log }) {
40
+ // The merge itself stays with a person: `gh pr merge` is refused by the sidecar BY DESIGN (a contents write; human-only-merge
41
+ // orgs keep contents:write off the App, 11 T21), so the workflow ends at the decision โ€” `merged` is always false here and
42
+ // the approver merges when ready.
43
+ log(
44
+ inputData.approved
45
+ ? `PR #${inputData.prNumber} approved โ€” merge when ready`
46
+ : `PR #${inputData.prNumber} not approved`
47
+ );
48
+ return { approved: inputData.approved, prNumber: inputData.prNumber, merged: false };
55
49
  },
56
50
  });
57
51
 
58
52
  export const ticketToPr = createWorkflow({
59
53
  name: 'ticket-to-pr',
60
54
  description:
61
- 'Linear label โ†’ implement/tests/docs in parallel worktrees โ†’ merge โ†’ test suite โ†’ PR โ†’ review rounds โ†’ approval โ†’ merge',
55
+ 'Linear label โ†’ implement/tests/docs in parallel worktrees โ†’ merge โ†’ test suite โ†’ PR โ†’ review rounds โ†’ approval โ†’ decision (a human merges)',
62
56
  inputSchema: z.object({
63
57
  ticketId: z.string(),
64
58
  title: z.string(),
@@ -66,7 +60,7 @@ export const ticketToPr = createWorkflow({
66
60
  repo: z.string(),
67
61
  baseRef: z.string().default('main'),
68
62
  }),
69
- outputSchema: z.object({ merged: z.boolean(), prUrl: z.string().optional(), rounds: z.number() }),
63
+ outputSchema: z.object({ approved: z.boolean(), prNumber: z.number(), merged: z.boolean() }), // the decision; the merge is a human act (see `recordDecision`)
70
64
  connections: [{ key: 'github', integrationType: 'github', required: true }], // the GitHub connection this workflow acts through, declared ONCE by key โ€” resolved on the owner agent at run time (an agent-scoped GitHub connection first, then org-scoped; LUA-623), so the same definition runs on any agent: no frozen connection id in source, no per-agent build
71
65
  workspace: {
72
66
  kind: 'git',
@@ -117,7 +111,19 @@ export const ticketToPr = createWorkflow({
117
111
  'The test suite failed:\n${stepResults.runTests.summary}\nFix the code (not the tests unless they are wrong) and re-run `npm test` until green.'
118
112
  ),
119
113
  })
120
- .then(openPr)
114
+ .agentStep('openPr', {
115
+ // a CODING TURN with `gh` (05 ยง5.17.6): opens the PR from the run branch and reports it as typed output
116
+ agentId: 'swe-implementer',
117
+ tier: 'job',
118
+ workspace: { mount: 'ro' },
119
+ timeoutSeconds: 600,
120
+ jobResources: 'small',
121
+ prompt: template(
122
+ 'Open a pull request from the current branch against ${initData.baseRef} for ticket ${initData.ticketId}, titled "${initData.title}". Put the marker the harness gives you for this run in the PR body (the GitHub webhook routes review signals by it). Reply with the PR number and URL.'
123
+ ),
124
+ toolScope: { jobTools: ['gh', 'git', 'read', 'glob', 'grep'] }, // `gh` here is the harness's tool through the sidecar's gh proxy (create / edit / comment on this repo only)
125
+ outputSchema: z.object({ prNumber: z.number(), url: z.string() }),
126
+ })
121
127
  .dowhile('reviewRound', eq(step('reviewRound').path('state'), lit('changes_requested')), { maxIterations: 8 }) // โ‰ค 8 review rounds (the SWE_REVIEW_MAX_ROUNDS lesson)
122
128
  .workflow(
123
129
  'reviewRound',
@@ -133,5 +139,6 @@ export const ticketToPr = createWorkflow({
133
139
  timeoutHours: 72,
134
140
  onTimeout: 'deny',
135
141
  })
136
- .then(mergePr)
142
+ .map({ approved: fromStep('mergeGate', 'approved'), prNumber: fromStep('openPr', 'prNumber') }, { id: 'mergeInput' }) // a `.then(step)` receives the PREVIOUS node's output โ€” the approval's, which has `approved` but not the PR number โ€” so the fields `recordDecision` declares are projected explicitly; without this map the row fails `input_schema_invalid` before dispatch (LUA-679, found by the LUA-669 builder)
143
+ .then(recordDecision)
137
144
  .commit();
@@ -20,7 +20,7 @@
20
20
  "inquirer": "^12.9.6",
21
21
  "stripe": "^17.5.0",
22
22
  "js-yaml": "^4.1.0",
23
- "lua-cli": "^3.32.1",
23
+ "lua-cli": "^3.32.2",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"