lua-cli 3.32.1 → 3.32.3
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/dist/api-exports.d.ts +414 -103
- package/dist/api-exports.js +2216 -622
- package/dist/api-exports.js.map +1 -1
- package/dist/index.js +5374 -2188
- package/dist/index.js.map +1 -1
- package/dist/voice/test/index.d.ts +54 -54
- package/dist/workflow-builder.d.ts +228 -45
- package/dist/workflow-builder.js +1945 -1025
- package/dist/workflow-builder.js.map +1 -1
- package/docs/CLI_REFERENCE.md +126 -4
- package/docs/README.md +2 -2
- package/docs/api/LuaWorkflow.md +16 -16
- package/docs/api/Workflows.md +10 -0
- package/docs/workflows/approvals.md +24 -6
- package/docs/workflows/correlation-keys.md +1 -0
- package/docs/workflows/goals.md +2 -2
- package/docs/workflows/limits.md +6 -0
- package/docs/workflows/replay-local.md +9 -3
- package/docs/workflows/schedules.md +1 -1
- package/docs/workflows/script-form.md +22 -12
- package/docs/workflows/testing-offline.md +35 -21
- package/docs/workflows/workspaces-and-long-steps.md +36 -2
- package/package.json +5 -4
- package/template/examples/workflows/CLAUDE.md +17 -11
- package/template/examples/workflows/adversarial-verify.workflow.script.js +20 -16
- package/template/examples/workflows/outreach.ts +31 -15
- package/template/examples/workflows/pr-review-round.ts +7 -3
- package/template/examples/workflows/refund-approval.ts +26 -12
- package/template/examples/workflows/ticket-to-pr.ts +43 -36
- package/template/package.json +1 -1
package/docs/api/LuaWorkflow.md
CHANGED
|
@@ -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()
|
|
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
|
|
package/docs/api/Workflows.md
CHANGED
|
@@ -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).
|
|
@@ -2,20 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
_Source of truth: workflows-spec 06 §6.4. This page is the developer summary; the spec is normative._
|
|
4
4
|
|
|
5
|
-
`.approval(id, cfg)` parks the run until a person decides. The node completes with the decision as data — branchable, never an exception.
|
|
5
|
+
`.approval(id, cfg)` parks the run until a person decides. The node completes with the decision as data — branchable, never an exception. The output is **one** shape everywhere — the engine, `lua workflows run --approve/--deny` offline and the shipped examples — `WorkflowApprovalOutput` (`@lua/shared-types`, LUA-751):
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
{
|
|
9
|
-
approved: boolean;
|
|
9
|
+
approved: boolean; // false on a denial AND on a timeout under onTimeout:'deny'
|
|
10
|
+
decision: 'approved' | 'denied' | 'timed_out';
|
|
11
|
+
text: string; // the approver's note when one was left, else the decision word — `${stepResults.<id>.text}`
|
|
10
12
|
note?: string; // the approver's note, when one was left
|
|
11
13
|
editedPayload?: unknown; // only when the approver edited (see below)
|
|
12
|
-
editRevision
|
|
13
|
-
decidedBy
|
|
14
|
+
editRevision?: number; // 0 when the payload was never edited; absent on a timeout
|
|
15
|
+
decidedBy?: { id?: string; kind?: string }; // who decided (the resumer's actor); absent on a timeout — the surface it came through is on the approval link / event, never here
|
|
14
16
|
evidence?: string[]; // decision artefact ids frozen with the decision
|
|
17
|
+
timedOut?: boolean; escalations?: number; // the timeout leg (below)
|
|
18
|
+
items?: Array<{ index; itemKey?; decision; decidedBy?; payload }>; // per-item approvals
|
|
15
19
|
}
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
A timeout that ends the chain in `'deny'` completes it with the sweep's
|
|
22
|
+
A timeout that ends the chain in `'deny'` completes it with the sweep's envelope — `{ approved: false, decision: 'timed_out', text: 'timed_out', timedOut: true, escalations: <hops> }` (no `decidedBy`, `editRevision` or `evidence`); `'continue'` is treated as `'deny'` for approvals; `'cancel-run'` cancels the run. **The original payload is not echoed back** — read it from the step's own input (`getInitData()`, `getStepResult('<id>')`, `${stepResults.<id>}`), not from the output; `editedPayload` replaces it only when the approver edited. `onDeny` defaults to `'continue'` (a denial is data — the next step runs with `{ approved: false, … }`); `onDeny: 'fail'` fails the step `approval_denied` on denials and timeouts alike.
|
|
23
|
+
|
|
24
|
+
The step after an approval declares the members it reads and lets the rest ride (`.passthrough()` — the engine validates a step's input with `additionalProperties: false` otherwise):
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
inputSchema: z.object({ approved: z.boolean(), editedPayload: refundRequest.optional() }).passthrough(),
|
|
28
|
+
async execute({ inputData, getStepResult }) {
|
|
29
|
+
if (!inputData.approved) return { refundId: null };
|
|
30
|
+
const r = inputData.editedPayload ?? getStepResult<RefundRequest>('refundRequest'); // the edited details, else what the approver saw
|
|
31
|
+
…
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Map the payload the approver should see ahead of the approval (`.map({ ticketId: fromInit('ticketId'), amount: fromInit('amount') }, { id: 'refundRequest' })`) when it is not the previous step's whole output: `editedPayloadSchema` describes THAT payload, and an edit is judged on the pointers that changed against it.
|
|
35
|
+
|
|
36
|
+
Offline, `lua workflows run --approve <id>` completes the node with `decidedBy: { id: 'local', kind: 'user' }` (exactly the resumer's `{ id, kind }` — no `via`: the engine never writes a surface into a step output) and `editRevision: 0`; `--approve <id>=@edited.json` carries the FULL edited payload as `editedPayload` with `editRevision: 1`; `--deny <id>[=<reason>]` makes the reason the `note` (and the `text`). `packages/shared-types/src/__fixtures__/workflow-approval-output.fixture.json` is the one fixture the engine, the driver and the examples are pinned to.
|
|
19
37
|
|
|
20
38
|
## Approver specs
|
|
21
39
|
|
|
@@ -38,4 +56,4 @@ The desktop inbox and run detail carry the full card (per-item rows, edits, esca
|
|
|
38
56
|
|
|
39
57
|
Per-item approvals (`itemsPath` / `itemApprover` / `itemTimeout`) fan one node out to one decision per item — see [Per-item approvals](./per-item-approvals.md).
|
|
40
58
|
|
|
41
|
-
Offline: `--approve <id>[=@payload]` / `--deny <id>[=@reason]` pre-answer the prompt; an edited payload is checked
|
|
59
|
+
Offline: `--approve <id>[=@payload]` / `--deny <id>[=@reason]` pre-answer the prompt; an edited payload is the FULL payload, checked the way the server checks an inline edit — only the pointers that CHANGED against the payload the approval showed must sit inside `editablePaths` (`changedPointers` + `matchesEditablePath` from `@lua/workflow-graph`), then `editedPayloadSchema`. `editable: true` with no `editablePaths` is whole-root rights — any member may change — as on the server. Because the change set is judged against the payload the approval actually showed, an edit outside `editablePaths` fails exit 2 `edit-path-not-allowed` **when the approval's turn comes**, i.e. after the steps before it have run, not before the run starts (`editedPayloadSchema` and `approval-not-editable` are still checked up-front).
|
|
@@ -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.
|
package/docs/workflows/goals.md
CHANGED
|
@@ -41,6 +41,6 @@ lua workflows goals create -i outreach \
|
|
|
41
41
|
|
|
42
42
|
## Errors and exit codes
|
|
43
43
|
|
|
44
|
-
The usual `lua workflows` codes: 0 ok · 1 API refusal · 2 usage · 3 not found. The goal routes' refusals are printed with their hint: `GOAL_NOT_ACTIVE {status}` (only an active goal pauses, only a paused one resumes, a done/closed goal stays closed), `GOAL_CAP {cap}` (close or finish a goal first), `VALIDATION_FAILED {issues}` (each issue with its path — `goal-judge-schema-missing-done`, `goal-cadence-invalid`, `goal-objective-too-long`, `ephemeral-role-required`, …), `GOAL_MAX_RUNS_INVALID`, `WORKFLOW_NOT_ON_AGENT`.
|
|
44
|
+
The usual `lua workflows` codes: 0 ok · 1 API refusal (a 4xx other than 404) · 2 usage · 3 not found · 11 unavailable (5xx / connection refused / timeout); an escaped 401 / 403 is 9 / 10 — `lua --help` prints the full table. The goal routes' refusals are printed with their hint: `GOAL_NOT_ACTIVE {status}` (only an active goal pauses, only a paused one resumes, a done/closed goal stays closed), `GOAL_CAP {cap}` (close or finish a goal first), `VALIDATION_FAILED {issues}` (each issue with its path — `goal-judge-schema-missing-done`, `goal-cadence-invalid`, `goal-objective-too-long`, `ephemeral-role-required`, …), `GOAL_MAX_RUNS_INVALID`, `WORKFLOW_NOT_ON_AGENT`.
|
|
45
45
|
|
|
46
|
-
`lua workflows schedules delete` on a goal's cadence Job is refused (`goal_schedule`) — use `goals pause` / `goals close
|
|
46
|
+
`lua workflows schedules delete` on a goal's cadence Job is refused (`goal_schedule`) while the goal is active or paused — use `goals pause` / `goals close` (closing retires the Job). Once the goal is done or closed (`goals close` then answers `GOAL_NOT_ACTIVE`), a cadence Job that still lingers is removed with `schedules delete <jobId>`; see [schedules.md](./schedules.md).
|
package/docs/workflows/limits.md
CHANGED
|
@@ -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.
|
|
@@ -15,10 +15,16 @@ Two options were on the table:
|
|
|
15
15
|
|
|
16
16
|
Why A: the wrapper drags the whole runtime plane (`@lua/sandbox-runtime` → `@lua/shared-sandbox` polyfills, artefact/dataset contexts) into the CLI bundle otherwise; every project that can run a workflow-script replay today is a monorepo/dev checkout that already has the runtime in `node_modules`; and resolving from the **project** rather than the CLI guarantees the wrapper version matches the runner the project deploys.
|
|
17
17
|
|
|
18
|
+
## Update — LUA-751: the wrapper text is bundled, the project's runtime still wins
|
|
19
|
+
|
|
20
|
+
Option A left every plain `lua init` project unable to run a script offline (`lua workflows run adversarial-verify` → exit 3 `REPLAY_RUNTIME_UNAVAILABLE`: the runtime is neither a template dependency nor on npm). The fix keeps A's ordering and adds the missing fallback without option B's fork: `@lua/sandbox-runtime` now ships a **dependency-free subpath entry**, `@lua/sandbox-runtime/workflow-script-wrapper` (the replay-round source, `buildWorkflowScriptWrapper`, the protocol window), which lua-cli inlines through its usual `noExternal: [/^@lua\//]` — the same file the runner builds, versioned with the CLI release, none of the platform-API clients of the root index. The context is the `workflow-script` surface of `@lua/shared-sandbox` (`buildWorkflowScriptGlobals` + the runtime handle), i.e. what `createSandboxContext({ siteType: 'workflow-script' })` builds.
|
|
21
|
+
|
|
22
|
+
`loadLocalScriptRunner(projectDir, { runtime })`: `'auto'` (default) resolves the project's `@lua/sandbox-runtime` first and falls back to the bundled wrapper; `'project'` keeps the strict behaviour (`REPLAY_RUNTIME_UNAVAILABLE`, never a silent "stable" against a different wrapper); `'bundled'` never looks. The report carries `runtime: 'project' | 'bundled'` and the printed report names it. The `journalProtocolVersion` gate applies to the bundled wrapper exactly as on the runner (`runtime_incompatible` outside its window).
|
|
23
|
+
|
|
18
24
|
## What it means for a project
|
|
19
25
|
|
|
20
|
-
- Inside the monorepo (or any project that lists `@lua/sandbox-runtime` in its devDependencies): `replay --local` and `lua test workflow <script>`
|
|
21
|
-
- Elsewhere:
|
|
26
|
+
- Inside the monorepo (or any project that lists `@lua/sandbox-runtime` in its devDependencies): `replay --local` and `lua test workflow <script>` run the project's runtime — parity with the runner it deploys.
|
|
27
|
+
- Elsewhere: both verbs run the wrapper bundled with the CLI; the replay report says `wrapper: bundled with this lua-cli`. A divergence found on the bundled wrapper is worth re-running from a monorepo checkout with `--project <dir>` before it is triaged as a runner NDE.
|
|
22
28
|
- The graph-form `replay --local` (R4/R5 → `replayLedger`) has no runtime dependency and always works. It re-evaluates every derived row over the same ancestor context the engine used — including a `failed` ancestor under `onError:'continue'`, which binds its continued-failure value (`{ __lua_workflow:'continued_failure', failed:true, error, text:'' }`) exactly as the engine did, so a mapping over one replays stable rather than `binding_unresolved`.
|
|
23
29
|
|
|
24
30
|
## Exit matrix (script form)
|
|
@@ -26,7 +32,7 @@ Why A: the wrapper drags the whole runtime plane (`@lua/sandbox-runtime` → `@l
|
|
|
26
32
|
| Exit | Meaning |
|
|
27
33
|
| ---- | ------------------------------------------------------------------------------------------------------------ |
|
|
28
34
|
| 0 | stable — every async journal entry re-issued with the same `callHash` |
|
|
29
|
-
| 3 |
|
|
35
|
+
| 3 | the run/bundle could not be fetched (`REPLAY_RUNTIME_UNAVAILABLE` only under `{ runtime: 'project' }`) |
|
|
30
36
|
| 4 | `LEDGER_DIVERGENCE` / `JOURNAL_DIVERGENCE` — at least one `hash_mismatch` / `journal_longer_than_script` row |
|
|
31
37
|
|
|
32
38
|
## Related
|
|
@@ -23,4 +23,4 @@ lua workflows view outreach # the "Schedules:" line + tabl
|
|
|
23
23
|
|
|
24
24
|
Each row shows the trigger (`cron 0 9 * * 1 (Europe/London)` · `every 3600s` · `once <iso>`), the next fire, the last fire, its status (`active` · `paused` · `auto-disabled` after the PRO-726 strike limit) and the strike count.
|
|
25
25
|
|
|
26
|
-
**Goal-owned schedules.** A goal's cadence is a schedule Job too; it is tagged with the goal's id (`goalId` in `--json`, the `Goal` column in the table).
|
|
26
|
+
**Goal-owned schedules.** A goal's cadence is a schedule Job too; it is tagged with the goal's id (`goalId` in `--json`, the `Goal` column in the table). While the goal is **active or paused** the CLI refuses to delete it — `schedules delete` on such a row answers `goal_schedule` (exit 1) without calling the API, the same refusal the chat tool gives — because unscheduling a live goal's job is recorded as a failure of the goal, never as stopping it. Stop the goal instead: `lua workflows goals pause <goalId>` (it can come back) or `lua workflows goals close <goalId>` (final; closing retires the cadence Job with it). The server refuses it too while the goal is live: R28 answers `409 GOAL_SCHEDULE {goalId}` for every caller (SDK, desktop, curl), and the CLI renders that with the same message. Once the goal has **ended** (`done` / `closed`, or its row is gone) a cadence Job that still lingers — one left behind before LUA-760, or one whose retirement did not land — is R28's to remove: `schedules delete <jobId>` goes through and retires it (the engine's sweep does the same on its own within one interval). A goal the CLI cannot read refuses (fail closed). See [goals.md](./goals.md).
|
|
@@ -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 = {
|
|
9
|
-
|
|
10
|
-
|
|
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' }), // thunks — `parallel` starts each arm itself (04 §4.3.2)
|
|
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,19 +33,23 @@ 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
|
-
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:
|
|
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`. The wrapper is **bundled with the CLI** (LUA-751 — the dependency-free `@lua/sandbox-runtime/workflow-script-wrapper` entry over the `workflow-script` context of `@lua/shared-sandbox`, the same surface the runner builds), so a plain `lua init` project runs scripts offline; a project that lists `@lua/sandbox-runtime` in its devDependencies (a monorepo checkout) runs that copy instead. A fake `approval()` resolves the same `WorkflowApprovalOutput` the graph form's `--approve` completes with. Steering:
|
|
33
43
|
|
|
34
|
-
| Flag
|
|
35
|
-
|
|
|
36
|
-
| `--input <json
|
|
37
|
-
| `--step-output <label
|
|
38
|
-
| `--max-ticks <n>`
|
|
39
|
-
| `--ledger-out <file>`
|
|
40
|
-
| `--json`
|
|
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
|
-
Exit 0 `done`/`bailed`; 4 `failed` (`SCRIPT_THREW`, `SCRIPT_DEADLOCK` — parked with zero intents, `SCRIPT_TICK_LIMIT`); 3
|
|
52
|
+
Exit 0 `done`/`bailed`; 4 `failed` (`SCRIPT_THREW`, `SCRIPT_DEADLOCK` — parked with zero intents, `SCRIPT_TICK_LIMIT`); 2 on flag grammar. (Exit 3 `REPLAY_RUNTIME_UNAVAILABLE` is reserved for `replay --local --runtime project` on a project without `@lua/sandbox-runtime`, see `replay-local.md`.)
|
|
43
53
|
|
|
44
54
|
## `step(label, fn, { timeoutSeconds? })`
|
|
45
55
|
|
|
@@ -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
|
|
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>` |
|
|
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 — the node completes with the engine's own envelope, `WorkflowApprovalOutput` (`{ approved, decision, text, note?, editedPayload?, editRevision?, decidedBy? }`, see [Approvals](./approvals.md)); an edited payload is the FULL payload, judged like the server's inline edit (`changedPointers` against the payload the approval showed + `matchesEditablePath`, then `editedPayloadSchema`); `onDeny` defaults to `'continue'` as on the engine (LUA-751). `editable: true` with no `editablePaths` is whole-root rights (any member may change), as on the server. Because the change set is judged against the payload the approval actually showed, a change outside `editablePaths` fails exit 2 `edit-path-not-allowed` **when the approval's turn comes — after the steps before it have run** — not before the run starts; `editedPayloadSchema` is still checked up-front |
|
|
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. An agent fixture's `output` is either the `{ text, object? }` reply envelope `--record` writes (the `object` wins, else the text is parsed under the `outputSchema`) or the structured value itself, taken as-is (LUA-751) — a value that is exactly `{ text }` reads as a text reply; record it as `{ object: { text } }` |
|
|
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,12 @@ _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.
|
|
40
|
+
- **Agent steps complete with the OBJECT, never a `{text, object}` wrapper** (LUA-751 — the engine's structured-output rule): under `--agents fake` a node with an `outputSchema` completes with a schema-shaped stub object (`{ summary: '', confidence: 0 }`), one without completes with `{ text: '[fake:<id>] …' }`; under `--agents live` / `--fixtures` the reply's `object` wins, else the single JSON value in its text is validated against the schema (`OUTPUT_SCHEMA_INVALID` names the miss). The next step's `inputData` is that value — a plain agent → code step declares `{ text: string }`.
|
|
41
|
+
- **A `foreach`'s output is readable under its body id** — `fromStep('draftEmail')`, `${stepResults.draftEmail}`, `getStepResult('draftEmail')` after `.foreach(draftEmail)` are the ordered item outputs, and a container's `.join` is readable under the container id (`foreach@2`, `conditional@1`) — as the engine publishes them (LUA-639).
|
|
42
|
+
- **`env.template('KEY')`** at a workflow file's module scope resolves to its placeholder when `lua workflows run` loads the artifact; the `--env KEY=value` overlay substitutes it on the graph.
|
|
43
|
+
- **Script-form workflows** run on the replay wrapper bundled with the CLI (`@lua/sandbox-runtime/workflow-script-wrapper`) over the `workflow-script` parity context; a project that lists `@lua/sandbox-runtime` in its devDependencies runs that copy instead — see [Script form](./script-form.md).
|
|
38
44
|
- **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
45
|
|
|
40
46
|
## Example
|
|
@@ -46,4 +52,12 @@ lua test workflow outreach --input @leads.json \
|
|
|
46
52
|
--ledger-out out.json
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
runs §3.2 (b) end to end with no prompt and no model call; `sendEmails` still runs (`ctx.once` against the effects map, so a second invocation with the same ledger sends nothing)
|
|
55
|
+
runs §3.2 (b) end to end with no prompt and no model call; `sendEmails` still runs (`ctx.once` against the effects map, so a second invocation with the same ledger sends nothing) — its input is the approval's `WorkflowApprovalOutput`, the drafts come from `editedPayload` (the edit above) or `getStepResult('drafts')`.
|
|
56
|
+
|
|
57
|
+
The three approval / typed-agent examples `lua init --with-examples` ships are pinned to run this way — `tests/__tests__/compile.workflow.examples-offline.test.ts`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
lua workflows run refund-approval --input '{"ticketId":"t1","amount":5,"requesterId":"u1"}' --approve approveRefund # exit 0 (a stubbed Stripe passthrough in the test; `--step-output postRefund=…` on a machine without one)
|
|
61
|
+
lua workflows run outreach --input '{"leads":[{"email":"a@b.co","name":"A"}]}' --approve reviewDrafts --step-output 'draftEmail={"to":"a@b.co","body":"hi"}' --step-output 'sendEmails={"sent":1}'
|
|
62
|
+
lua workflows run research-brief --input '{"topic":"x"}' --step-output 'fetchSources={"urls":["https://a.example/1"]}' # the fake agents take the lowConfidence arm
|
|
63
|
+
```
|
|
@@ -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 = {
|
|
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
|
|
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.
|
|
3
|
+
"version": "3.32.3",
|
|
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",
|
|
@@ -113,10 +113,11 @@
|
|
|
113
113
|
"stripe": "^19.2.0",
|
|
114
114
|
"ts-node": "^10.9.2",
|
|
115
115
|
"tsup": "^8.5.1",
|
|
116
|
-
"@lua/shared-sandbox": "0.0.1",
|
|
117
|
-
"@lua/workflow-graph": "0.0.1",
|
|
118
116
|
"@lua/shared-source-sync": "0.0.1",
|
|
119
|
-
"@lua/shared-types": "0.0.1"
|
|
117
|
+
"@lua/shared-types": "0.0.1",
|
|
118
|
+
"@lua/workflow-graph": "0.0.1",
|
|
119
|
+
"@lua/sandbox-runtime": "0.0.1",
|
|
120
|
+
"@lua/shared-sandbox": "0.0.1"
|
|
120
121
|
},
|
|
121
122
|
"scripts": {
|
|
122
123
|
"clean": "rm -rf dist temp",
|
|
@@ -5,17 +5,17 @@ Worked examples from workflows-spec 03 §3.2 (the spec is normative — do not r
|
|
|
5
5
|
it (a `.then(createStep)` placement used as a string-ref target; a `template(…)` approval title). The note says what
|
|
6
6
|
changed and why; `tests/__tests__/compile.workflow.examples.test.ts` compiles every file here, so a drift cannot return.
|
|
7
7
|
|
|
8
|
-
| File | Shows
|
|
9
|
-
| ------------------------------------------------------------------------------------------------- |
|
|
10
|
-
| `research-brief.ts` | (a) sequential + parallel research, typed predicates (`stepOf`/`gt`/`lit`), switch + otherwise
|
|
11
|
-
| `outreach.ts` | (b) `foreach` + editable approval + an exactly-once send (`ctx.once`), cron schedule, `concurrencyPolicy`
|
|
12
|
-
| `provision-tenant.ts` | (c) nested workflow + `sleep` + `waitForSignal`
|
|
13
|
-
| `reviewed-brief.ts` | (d) `specialistStep` — an ephemeral reviewer role on the owning agent (D25)
|
|
14
|
-
| `refund-approval.ts` | (e) maker-checker + four-eyes + business-hours escalation chain + a recoverable external step (`onError:'park'`)
|
|
15
|
-
| `ticket-to-pr.ts` + `pr-review-round.ts` + `linear-ready.trigger.ts` + `github-review.webhook.ts` | (f) the SWE headline: Job tier, worktree arms + merge, review loop over a child workflow, trigger `startWorkflow`, webhook → `Workflows.signal`
|
|
16
|
-
| `support-triage.ts` | (g) knowledge grounding, mandatory `toolScope` on external content, dataset rows, `ctx.artefacts`
|
|
17
|
-
| `adversarial-verify.workflow.script.js` | (i) script form — `step()` inline
|
|
18
|
-
| `vendor-invoices.ts` | (j) per-item approvals (`itemsPath`), `env.template()` overlays, `outputVisibility`
|
|
8
|
+
| File | Shows |
|
|
9
|
+
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
10
|
+
| `research-brief.ts` | (a) sequential + parallel research, typed predicates (`stepOf`/`gt`/`lit`), switch + otherwise |
|
|
11
|
+
| `outreach.ts` | (b) `foreach` + editable approval + an exactly-once send (`ctx.once`), cron schedule, `concurrencyPolicy` |
|
|
12
|
+
| `provision-tenant.ts` | (c) nested workflow + `sleep` + `waitForSignal` |
|
|
13
|
+
| `reviewed-brief.ts` | (d) `specialistStep` — an ephemeral reviewer role on the owning agent (D25) |
|
|
14
|
+
| `refund-approval.ts` | (e) maker-checker + four-eyes + business-hours escalation chain + a recoverable external step (`onError:'park'`) |
|
|
15
|
+
| `ticket-to-pr.ts` + `pr-review-round.ts` + `linear-ready.trigger.ts` + `github-review.webhook.ts` | (f) the SWE headline: Job tier, worktree arms + merge, review loop over a child workflow, trigger `startWorkflow`, webhook → `Workflows.signal` |
|
|
16
|
+
| `support-triage.ts` | (g) knowledge grounding, mandatory `toolScope` on external content, dataset rows, `ctx.artefacts` |
|
|
17
|
+
| `adversarial-verify.workflow.script.js` | (i) script form — `parallel()` over thunks, a `step()` inline reduction that returns (never mutates) — runs offline on the bundled wrapper (LUA-751) |
|
|
18
|
+
| `vendor-invoices.ts` | (j) per-item approvals (`itemsPath`), `env.template()` overlays, `outputVisibility` |
|
|
19
19
|
|
|
20
20
|
Recipes:
|
|
21
21
|
|
|
@@ -25,3 +25,9 @@ Recipes:
|
|
|
25
25
|
- Every doc rule worth repeating: _execute re-runs from the top after resume; execution is at-least-once — dedupe
|
|
26
26
|
on `occurrenceId`_ (`${lineageId}:${stepId}`); an effect unique across independent runs needs your own business
|
|
27
27
|
key (`refund:${ticketId}`).
|
|
28
|
+
- The step after an `.approval()` receives the approval node's output — `WorkflowApprovalOutput` (`@lua/shared-types`):
|
|
29
|
+
`{ approved, decision, text, note?, editedPayload?, editRevision?, decidedBy?, timedOut?, escalations? }`, the same shape
|
|
30
|
+
on the engine and under `lua workflows run --approve/--deny` (LUA-751; `tests/__tests__/compile.workflow.examples-offline.test.ts`
|
|
31
|
+
runs `refund-approval`, `outreach` and `research-brief` offline over the shared fixture). The original payload is not
|
|
32
|
+
echoed back — `refund-approval` reads it with `getStepResult('refundRequest')` (the map ahead of its approval: the
|
|
33
|
+
payload the approver sees and edits is exactly what `editedPayloadSchema` describes), `outreach` with `getStepResult('drafts')`.
|