lua-cli 3.32.3 → 3.32.5

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.
@@ -102,10 +102,14 @@ lua compile [options]
102
102
 
103
103
  **Options:**
104
104
 
105
- - `--debug` - Show detailed compilation output
105
+ - `--verbose` - Show detailed progress output
106
+ - `--debug` - Show detailed compilation output (verbose esbuild, temp files kept)
107
+ - `--sync` - Check for drift against the server before compiling (reads only — nothing is written)
106
108
 
107
109
  **Description:**
108
- Compiles TypeScript to JavaScript bundles, validates primitives (tools, skills, webhooks, jobs, processors), and generates manifest.
110
+ Compiles TypeScript to JavaScript bundles, validates primitives (tools, skills, webhooks, jobs, processors, workflows), and generates the manifest.
111
+
112
+ `lua compile` is a **local build**: it reads no credential and makes no API call — with or without a credential configured, nothing is created or changed on the agent. The write is `lua push`: a skill or workflow the agent does not have yet is registered there (the push writes its server id into `lua.skill.yaml`), then the version is uploaded. `lua test`, `lua test workflow` and `lua workflows run` build the same way. (LUA-790 — earlier releases registered every new skill / workflow on the agent during a bare compile whenever a credential was configured.)
109
113
 
110
114
  **Output:**
111
115
 
package/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # lua-cli v3.32.3
1
+ # lua-cli v3.32.5
2
2
 
3
- Welcome to the comprehensive API documentation for lua-cli v3.32.3. This guide covers every API, class, and function exported by the package.
3
+ Welcome to the comprehensive API documentation for lua-cli v3.32.5. This guide covers every API, class, and function exported by the package.
4
4
 
5
5
  ## 📚 Documentation Index
6
6
 
@@ -61,7 +61,7 @@ Config: `name`, `description?`, `inputSchema`, `outputSchema?`, `budget?` (`maxC
61
61
  | `.specialistStep(id, { role, prompt, … })` | Ephemeral role on the owning agent (D25) |
62
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
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) |
64
+ | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm — the step runs with the map as its own `input` (the arm node carries `input`, like a `toolStep` with one). Not a `foreach` / loop body: those receive their item / the previous output (`mapping-placement` at `.commit()`) — map the items before the container instead |
65
65
  | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
66
  | `.foreach(step, { concurrency?, maxItems?, chunk?, rateLimit? })` / `.foreach({ items })` | Fan-out over an upstream array |
67
67
  | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
@@ -50,6 +50,8 @@ Offline, `lua workflows run --approve <id>` completes the node with `decidedBy:
50
50
 
51
51
  `editable: true` + `editablePaths: ['amount', 'drafts[*].body']` (the `a.b[0].c` / `[*]` grammar) lets the approver patch the payload. The sequence is **fetch → patch → approve-with-fingerprint**: the approve call echoes the fingerprint of the revision the approver saw, so a concurrent edit forces a re-read. Out-of-grammar paths are refused; `editedPayloadSchema` validates the result.
52
52
 
53
+ From the CLI: `lua workflows approval-payload <runId> --approval <wfa_…>` prints the current payload with its `payloadFingerprint`, `editRevision` and the editable paths (a large payload answers the arrays to page — `--path drafts [--limit n] [--cursor c]` reads one page); then `lua workflows approve <runId> --approval <wfa_…> --edit @edited.json --fingerprint <payloadFingerprint>`. A stale fingerprint is `409 PAYLOAD_MISMATCH` — refetch and retry.
54
+
53
55
  ## Where approvals surface
54
56
 
55
57
  The desktop inbox and run detail carry the full card (per-item rows, edits, escalation state). **Text channels (WhatsApp/SMS/email) approve or deny the whole batch only** — no per-item decisions, no edits.
@@ -8,6 +8,10 @@ Per-run: `budget.maxCredits` (over the org's `maxCreditsPerRun` default 250 ⇒
8
8
 
9
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
10
 
11
+ ### Payment required — the billing gate
12
+
13
+ The run's `maxCredits` is the run's own ceiling; the **workspace's wallet** is a separate gate. When the wallet refuses an agent step's reserve (an inline step's turn, or a Job-tier attempt's reserve — taken **before** any pod is spawned), the run parks `suspended{gate.kind:'billing', code:'insufficient_credits' | 'out_of_actions'}` and the step reads `suspended` (`suspend.kind:'billing'`) on `lua workflows status --steps`. `nextAction` is `top_up`: nothing on the run raises it — top up the workspace (or ask a workspace admin to). The engine re-checks the wallet every few minutes and re-arms the held step on its own; `lua workflows retry-step <runId> --step <stepId>` forces that re-check right away. A run still unpaid after the org's `billingGateExpiryHours` (default 168) fails with `billing_gate_expired`. The inbox card reads "Needs you — payment required" while it waits.
14
+
11
15
  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
16
 
13
17
  ## Org pacing
@@ -19,7 +19,7 @@ A parked step waits for exactly one of:
19
19
 
20
20
  `onError:'continue'` never parks: the step is `failed` on the ledger, the run goes on, and every successor sees the step's **continued-failure value** `{ __lua_workflow:'continued_failure', failed:true, error:{code,message}, text:'' }` under `stepResults.<id>` (the default input, `${stepResults.<id>.text}` → `''`, `getStepResult(id)`, a `conditional` on `stepResults.<id>.failed`, and the parallel / foreach join entry). A `retry-step` on such a row while the run is still running re-arms it, but a successor that already consumed the value is not re-run.
21
21
 
22
- `fail` (or cancelling the run) applies the step's failure as-is. From the CLI: `lua workflows status <runId>` shows the park, `lua workflows resume` / the desktop inbox carry the verbs.
22
+ `fail` (or cancelling the run) applies the step's failure as-is. From the CLI: `lua workflows status <runId>` shows the park and names the verbs; `lua workflows retry-step <runId> --step <id>` re-runs, `lua workflows resolve-step <runId> --step <id> --outcome skip|complete|fail [--output <json|@file>] [--note …]` decides (R37 — `complete` needs `--output`, validated against the step's `outputSchema`; a second decision on the same park is a 200 no-op that names who decided). The desktop inbox carries the same three, plus a repair run.
23
23
 
24
24
  ## Repair runs
25
25
 
@@ -2,25 +2,34 @@
2
2
 
3
3
  _Source of truth: workflows-spec 07 §7.1. This page is the developer summary; the spec is normative._
4
4
 
5
- `schedule: { type:'cron', expression: '0 9 * * 1', timezone: 'Europe/London' }` on `createWorkflow` fires runs on the cron grid; `scheduleInput` is the run input. `concurrencyPolicy:'forbid'` skips a fire while a run is in flight (the skip is recorded, never queued). Manage with `lua workflows activate` / `deactivate`; `lua workflows backfill` starts missed occurrences by hand (deduplicated on `backfill:<workflowId>:<occurrenceIso>`).
5
+ `schedule: { type:'cron', expression: '0 9 * * 1', timezone: 'Europe/London' }` on `createWorkflow` fires runs on the cron grid; `scheduleInput` is the run input. `concurrencyPolicy:'forbid'` skips a fire while a run is in flight (the skip is recorded, never queued). Manage with `lua workflows activate` / `deactivate`, or create and drive a schedule from the CLI without a `schedule:` block — `lua workflows schedules create|pause|resume|patch|delete` below; a re-enable with `--backfill-now` starts the missed occurrences (deduplicated on `backfill:<workflowId>:<occurrenceIso>`).
6
6
 
7
7
  ## Re-enabling a schedule
8
8
 
9
- A paused or auto-disabled schedule never replays missed fires by itself; opt in with `schedule.backfillOnEnable: { maxOccurrences }` (or `lua workflows activate --backfill` for one re-enable) and the most recent misses start as one batch, deduplicated against `lua workflows backfill` by the shared `backfill:<workflowId>:<occurrenceIso>` key, capped, and summarised in your inbox.
9
+ A paused or auto-disabled schedule never replays missed fires by itself; opt in with `schedule.backfillOnEnable: { maxOccurrences }` (or `lua workflows schedules resume <jobId> --backfill-now` for one re-enable, `--backfill-on-enable <n>` to persist the opt-in) and the most recent misses start as one batch, deduplicated by the shared `backfill:<workflowId>:<occurrenceIso>` key, capped, and summarised in your inbox.
10
10
 
11
11
  Offline, schedules do not fire (`backfillOnEnable` is not emulated) — start runs with `lua test workflow` / `lua workflows start`.
12
12
 
13
13
  ## From the CLI
14
14
 
15
- Schedules are `Job{kind:'workflow'}` rows on the agent; the CLI reads them through `GET /workflows/:agentId/schedules` and removes one through R28.
15
+ Schedules are `Job{kind:'workflow'}` rows on the agent; the CLI reads them through `GET /workflows/:agentId/schedules`, creates one through R27, pauses / resumes it through R56 and removes it through R28.
16
16
 
17
17
  ```bash
18
18
  lua workflows schedules list -i outreach # this workflow's schedule Jobs (omit -i for every workflow of the agent)
19
19
  lua workflows schedules list --json # { success, data: { items: [{ jobId, workflowId, trigger, paused, nextRunAt, lastFiredAt?, consecutiveFailures, autoDisabled?, goalId? }] } }
20
+ lua workflows schedules create outreach --cadence '0 9 * * 1' --timezone Europe/London --input '{"segment":"trial"}'
21
+ # R27 — the goals cadence grammar: --cadence (cron | JSON | @file, ≤ 5) [--timezone] or --every 30m;
22
+ # [--tag] [--notify emailApp|email|app|off] [-v <version>] [--budget-credits <n>] [--backfill-on-enable <n>].
23
+ # Create-or-REPLACE: a second create swaps the workflow's schedule.
24
+ lua workflows schedules pause <jobId> # R56 { paused:true }
25
+ lua workflows schedules resume <jobId> [--backfill-now] [--backfill-on-enable <n|none>] # R56 { paused:false, … } — the one-shot backfill rides a re-enable only
26
+ lua workflows schedules patch <jobId> --paused true|false [--backfill-on-enable <n|none>] [--backfill-now]
20
27
  lua workflows schedules delete <jobId> [--yes] # R28 — asks first unless --yes
21
28
  lua workflows view outreach # the "Schedules:" line + table; --json carries the same rows under `schedules`
22
29
  ```
23
30
 
31
+ Every verb prints one `{ success, data | error }` envelope under `--json`; exit codes are the usual ones (`2` usage before any call, `3` an unknown workflow / `SCHEDULE_NOT_FOUND`, `1` a refusal — `SCHEDULE_CAP`, a `VALIDATION_FAILED` whose issues are printed, `goal_schedule`).
32
+
24
33
  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
34
 
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).
35
+ **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). `schedules pause|resume|patch` on such a row refuses (`goal_schedule`, exit 1, nothing called) whatever the goal's state — a goal's Job follows its goal (`goals pause` / `goals resume`), never the other way round. 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).
@@ -4,7 +4,7 @@ _Source of truth: workflows-spec 03 §3.9 (WF-204/WF-225/WF-332). This page is t
4
4
 
5
5
  `lua test workflow <name>` (alias `lua workflows run <name>`) drives a compiled workflow **offline**: the same `compilePlan`, predicates and mappings the engine uses, code steps executed in the `lua test` sandbox, agent steps faked by default, waits fast-forwarded on a virtual clock. Exit codes: `0` completed · `2` usage/validation · `4` failed · `5` missing fixture.
6
6
 
7
- **No API call.** The verb compiles first, but that compile skips the server ↔ YAML sync every other `lua compile` performs — nothing is created on the agent and no run exists on the server. Only `lua push workflow` persists a definition; `lua workflows start` creates a run. The exceptions are the flags that ask for the platform: `--from-run <runId>` (reads the seed run) and `--agents live` (calls the dev API for agent steps).
7
+ **No API call.** The verb compiles first, and that compile is local like every `lua compile` (LUA-790) — nothing is created on the agent and no run exists on the server. Only `lua push workflow` persists a definition; `lua workflows start` creates a run. The exceptions are the flags that ask for the platform: `--from-run <runId>` (reads the seed run) and `--agents live` (calls the dev API for agent steps).
8
8
 
9
9
  ## Steering flags — fully scriptable, no stdin
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.32.3",
3
+ "version": "3.32.5",
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,11 +113,11 @@
113
113
  "stripe": "^19.2.0",
114
114
  "ts-node": "^10.9.2",
115
115
  "tsup": "^8.5.1",
116
+ "@lua/sandbox-runtime": "0.0.1",
116
117
  "@lua/shared-source-sync": "0.0.1",
118
+ "@lua/shared-sandbox": "0.0.1",
117
119
  "@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
+ "@lua/workflow-graph": "0.0.1"
121
121
  },
122
122
  "scripts": {
123
123
  "clean": "rm -rf dist temp",
@@ -1,5 +1,6 @@
1
1
  // Sequential + parallel research, typed end to end.
2
- // Verbatim from workflows-spec 03 §3.2 (a) (WF-215 / WF-223 — the spec is normative).
2
+ // Verbatim from workflows-spec 03 §3.2 (a) (WF-215 / WF-223 — the spec is normative), with the workflow EXPORTED
3
+ // like every other example — a non-exported `const` cannot be registered on a LuaAgent without editing the file.
3
4
  import { z } from 'zod';
4
5
  import { createStep, createWorkflow, stepOf, fromStep, template, gt, lit } from 'lua-cli';
5
6
 
@@ -19,24 +20,36 @@ const fetchSources = createStep({
19
20
 
20
21
  const angle = z.object({ summary: z.string(), confidence: z.number() });
21
22
 
22
- const wf = createWorkflow({
23
+ export const researchBrief = createWorkflow({
23
24
  name: 'research-brief',
24
25
  description: 'Fetch sources, summarise from two angles in parallel, merge — or fall back when confidence is low.',
25
26
  inputSchema: z.object({ topic: z.string() }),
26
27
  outputSchema: z.object({ brief: z.string() }),
27
28
  budget: { maxCredits: 40 },
28
29
  })
29
- .then(fetchSources) // entry 1: step
30
- .parallel(['techAngle', 'marketAngle']) // entry 2: parallel — 2 arms, both declared BELOW (§3.2.0: "declare here, inside me")
31
- .agentStep('techAngle', { agentId: 'analyst', prompt: template('Summarise the technical angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
32
- outputSchema: angle }) // no top-level entry — placed by the parallel above
33
- .agentStep('marketAngle', { agentId: 'analyst', prompt: template('Summarise the market angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
34
- outputSchema: angle }) // same
35
- .switch( // entry 3: conditional{exclusive}
36
- [[gt(stepOf<typeof angle>('techAngle').path('confidence'), lit(0.6)), 'merge']], // TypedRef<number> vs Literal<number> — `lit('a')` would not compile
37
- 'lowConfidence')
38
- .map({ brief: fromStep('techAngle', 'summary'), market: fromStep('marketAngle', 'summary') }, { id: 'merge' }) // placed INSIDE the switch arm (string ref above) — one entry, not two
39
- .agentStep('lowConfidence', { agentId: 'analyst', // placed as the switch's `otherwise`
40
- prompt: template('Confidence was low. Write a cautious brief on ${initData.topic} from ${stepResults.techAngle.summary} and ${stepResults.marketAngle.summary}'),
41
- outputSchema: z.object({ brief: z.string() }) })
42
- .commit(); // the conditional is the last entry: the taken arm's output IS the run output
30
+ .then(fetchSources) // entry 1: step
31
+ .parallel(['techAngle', 'marketAngle']) // entry 2: parallel — 2 arms, both declared BELOW (§3.2.0: "declare here, inside me")
32
+ .agentStep('techAngle', {
33
+ agentId: 'analyst',
34
+ prompt: template('Summarise the technical angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
35
+ outputSchema: angle,
36
+ }) // no top-level entry placed by the parallel above
37
+ .agentStep('marketAngle', {
38
+ agentId: 'analyst',
39
+ prompt: template('Summarise the market angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
40
+ outputSchema: angle,
41
+ }) // same
42
+ .switch(
43
+ // entry 3: conditional{exclusive}
44
+ [[gt(stepOf<typeof angle>('techAngle').path('confidence'), lit(0.6)), 'merge']], // TypedRef<number> vs Literal<number> — `lit('a')` would not compile
45
+ 'lowConfidence'
46
+ )
47
+ .map({ brief: fromStep('techAngle', 'summary'), market: fromStep('marketAngle', 'summary') }, { id: 'merge' }) // placed INSIDE the switch arm (string ref above) — one entry, not two
48
+ .agentStep('lowConfidence', {
49
+ agentId: 'analyst', // placed as the switch's `otherwise`
50
+ prompt: template(
51
+ 'Confidence was low. Write a cautious brief on ${initData.topic} from ${stepResults.techAngle.summary} and ${stepResults.marketAngle.summary}'
52
+ ),
53
+ outputSchema: z.object({ brief: z.string() }),
54
+ })
55
+ .commit(); // the conditional is the last entry: the taken arm's output IS the run output
@@ -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.3",
23
+ "lua-cli": "^3.32.5",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"