lua-cli 3.34.0 โ†’ 3.36.0

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.34.0
1
+ # lua-cli v3.36.0
2
2
 
3
- Welcome to the comprehensive API documentation for lua-cli v3.34.0. This guide covers every API, class, and function exported by the package.
3
+ Welcome to the comprehensive API documentation for lua-cli v3.36.0. This guide covers every API, class, and function exported by the package.
4
4
 
5
5
  ## ๐Ÿ“š Documentation Index
6
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  The workflow primitive: a **durable, multi-step program** you author in TypeScript, compile with `lua compile`, and push with `lua push workflow`. The engine executes the compiled graph server-side โ€” steps survive restarts, waits hold no compute, and approvals/signals park the run until a person or a webhook answers.
4
4
 
5
- > **Execution model โ€” read this first.** _`execute` re-runs from the top after resume; execution is at-least-once โ€” dedupe on `occurrenceId`._ `occurrenceId` is `${lineageId}:${stepId}` โ€” the same key on retry, resume, `retry-step` and a repair run; an effect that must be unique across independent runs needs your own business key (`refund:${ticketId}`). Wrap external effects in `ctx.once(key, fn)`.
5
+ > **Execution model โ€” read this first.** _`execute` re-runs from the top after resume; execution is at-least-once โ€” dedupe on `occurrenceId`._ `occurrenceId` is `${lineageId}:${stepId}` โ€” the same key on retry, resume and `retry-step` (no repair run exists โ€” a terminal run is never re-run from its ledger); an effect that must be unique across independent runs needs your own business key (`refund:${ticketId}`). Wrap external effects in `ctx.once(key, fn)`.
6
6
 
7
7
  ## Import
8
8
 
@@ -15,7 +15,7 @@ Inside a sandbox (tools, jobs, webhooks, workflow code steps) `Workflows` is als
15
15
  - `start` **never awaits execution** โ€” it returns `{ runId, status }` immediately. `waitSeconds` (โ‰ค 55) asks the server to hold the response for early terminal state; it changes the response, never the execution model.
16
16
  - Runs are idempotent on `idempotencyKey` โ€” re-sending the same key returns the existing run instead of a duplicate.
17
17
  - A workflow with `concurrencyPolicy: 'forbid'` refuses a second start while one is in flight with a thrown `WorkflowApiError { code: 'RUNS_IN_FLIGHT', blockingRunId }` โ€” never a 429, never a gate.
18
- - `status: 'gated'` means the run is parked (quota, billing or consent) and holds no slot. Consent gates are cleared from the desktop or with `lua workflows approve`.
18
+ - `status: 'gated'` means the run is parked (quota, billing or consent) and holds no slot. A **consent** gate is cleared by a person resolving its approval โ€” from the desktop, or through the approvals API / inbox. `lua workflows approve` cannot clear one: it requires an `--approval wfa_โ€ฆ` id, which belongs to a workflow `approval` node, and a consent gate has no such id. `lua workflows status <runId>` reports a gated run, and exits 6 under `--strict`.
19
19
  - `nameOrId` resolves the workflow **name on the calling agent first**, then a workflow id.
20
20
  - `get` / `list` honour output visibility: a restricted run comes back with `restricted: true` and no `output` โ€” never an error.
21
21
  - From a workflow code step, `Workflows.start` creates a **detached** run (no parent link). Use the `.workflow(...)` builder node when the parent should wait.
@@ -2,7 +2,7 @@
2
2
 
3
3
  _Source of truth: workflows-spec (Cluster D R46/R47, D7-r2). This page is the developer summary; the spec is normative._
4
4
 
5
- `lua workflows migrate-runs <workflowId> --from <ver> --to <ver>` re-pins runs to a new version where compatible (`isResumeCompatible` โ€” same seeding math as repair runs); `cancel-by-version` recalls a bad version's fleet.
5
+ `lua workflows migrate-runs <workflowId> --from <ver> --to <ver>` re-pins runs to a new version where compatible (`isResumeCompatible` โ€” the same seeding math as `lua test workflow --from-run`); `cancel-by-version` recalls a bad version's fleet.
6
6
 
7
7
  ## Edited inputs
8
8
 
@@ -19,13 +19,13 @@ 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 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.
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 run page carries the same decisions.
23
23
 
24
- ## Repair runs
24
+ ## No repair run
25
25
 
26
- When a whole run is beyond per-step repair, a **repair run** re-drives the graph seeded from the ledger (the ยง05 ยง5.5.4.1 seeding math โ€” the same `seedLedgerFromRun` the CLI's `--from-run` uses): every `completed` step keeps its outputs, attempts and settled effects, and the frontier restarts at the first non-completed step. Because `occurrenceId` is stable across retry, resume, `retry-step` **and** a repair run, an external effect executes at most once per occurrence.
26
+ Nothing re-runs a run from its ledger today (LUA-709 retired the verb; BAC-601): there is no "repair run" in the CLI, the API, the chat tools or the desktop, and a **terminal run cannot be re-run**. `retry-step` and `resolve-step` are the ways past a park; a new run (`lua workflows start`) is the way past a terminal one โ€” offline, `lua test workflow <name> --from-run <runId>` can seed every `completed` step from the old ledger for a rehearsal. Because `occurrenceId` is stable across retry, resume and `retry-step`, an external effect executes at most once per occurrence within a run.
27
27
 
28
- > **The rule to remember:** _execution is at-least-once โ€” dedupe on `occurrenceId`_; an effect that must be unique across **independent** runs needs your own business key (`refund:${ticketId}`).
28
+ > **The rule to remember:** _execution is at-least-once โ€” dedupe on `occurrenceId`_; an effect that must be unique across **independent** runs (a new run, seeded or not, is one) needs your own business key (`refund:${ticketId}`).
29
29
 
30
30
  ## Rehearsing offline
31
31
 
@@ -15,7 +15,7 @@ _Source of truth: workflows-spec 03 ยง3.9 (WF-204/WF-225/WF-332). This page is t
15
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
16
  | `--signal <name>=@file\|<json>` (rep.) | Pre-supply `waitForSignal` payloads in order of arrival |
17
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` |
18
+ | `--from-run <runId>` | Seed every `completed` step from a real run (the ledger seeding `migrate-runs` also uses); graph drift asks for `--force` |
19
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
20
  | `--fast-retries` | Collapse retry backoff waits to 0 |
21
21
  | `--real-time` | Actually wait on `sleep`/backoff instead of fast-forwarding the virtual clock |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.34.0",
3
+ "version": "3.36.0",
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/shared-types": "0.0.1",
117
- "@lua/shared-source-sync": "0.0.1",
118
- "@lua/workflow-graph": "0.0.1",
119
116
  "@lua/sandbox-runtime": "0.0.1",
120
- "@lua/shared-sandbox": "0.0.1"
117
+ "@lua/shared-sandbox": "0.0.1",
118
+ "@lua/shared-source-sync": "0.0.1",
119
+ "@lua/shared-types": "0.0.1",
120
+ "@lua/workflow-graph": "0.0.1"
121
121
  },
122
122
  "scripts": {
123
123
  "clean": "rm -rf dist temp",
@@ -128,6 +128,7 @@
128
128
  "typecheck": "tsc -p tsconfig.typecheck.json",
129
129
  "test": "node ../../scripts/test-gate.mjs jest",
130
130
  "test:watch": "jest --watch",
131
- "test:coverage": "node ../../scripts/test-gate.mjs jest --coverage"
131
+ "test:coverage": "node ../../scripts/test-gate.mjs jest --coverage",
132
+ "test:compat-matrix": "node scripts/compat-matrix-previous-cli.mjs"
132
133
  }
133
134
  }
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * [WMC-E7] The CLI compatibility matrix's real leg (plan ยง6.9, "Older CLIs keep working").
4
+ *
5
+ * Installs the PREVIOUS MINOR RELEASE of this CLI from npm into a temporary directory and drives it, as a
6
+ * child process, against the mock API in `tests/helpers/workflow-compat-server.mjs`. It runs the matrix
7
+ * twice: once against a server serving definitions and runs WITHOUT any of the per-step model-selection
8
+ * fields, and once WITH all of them, including the run-event types the older CLI has never heard of.
9
+ *
10
+ * The verbs exercised are the three ยง6.9 names: **push**, **view** and **watch**.
11
+ *
12
+ * โ”€โ”€ Safety โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+ * Nothing here touches a real account or a real agent. The child CLI is pointed at `127.0.0.1` through
14
+ * `LUA_API_URL` and given a throwaway `LUA_API_KEY`, so there is no `lua auth`, no production host and no
15
+ * production verb against anything that exists. The temp directory is removed on the way out.
16
+ *
17
+ * โ”€โ”€ Why it is gated โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
+ * It needs the npm registry. Every property it proves is also asserted offline in
19
+ * `tests/__tests__/workflows.compat-matrix.test.ts`, which runs on every PR; this leg is the end-to-end
20
+ * confirmation and belongs in the nightly job and on demand:
21
+ *
22
+ * pnpm --filter lua-cli test:compat-matrix
23
+ * LUA_COMPAT_MATRIX_VERSION=3.33.0 pnpm --filter lua-cli test:compat-matrix
24
+ *
25
+ * Exit 0 = the previous release coped with both halves. Non-zero names the leg that failed.
26
+ */
27
+ import { execFileSync, spawn } from 'node:child_process';
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { AGENT_ID, RUN_ID, WORKFLOW_ID, startCompatServer } from '../tests/helpers/workflow-compat-server.mjs';
33
+
34
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
35
+ const PKG = JSON.parse(fs.readFileSync(path.join(HERE, '..', 'package.json'), 'utf8'));
36
+
37
+ /** The previous MINOR of the version in package.json โ€” 3.34.x โ‡’ 3.33.0 โ€” unless one is named. */
38
+ function previousMinor(current) {
39
+ const [major, minor] = current.split('.').map(Number);
40
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) throw new Error(`cannot read a version from "${current}"`);
41
+ if (minor === 0) throw new Error(`no previous minor for ${current} โ€” name one with LUA_COMPAT_MATRIX_VERSION`);
42
+ return `${major}.${minor - 1}.0`;
43
+ }
44
+
45
+ const TARGET = process.env.LUA_COMPAT_MATRIX_VERSION || previousMinor(PKG.version);
46
+
47
+ function say(msg) {
48
+ process.stdout.write(`${msg}\n`);
49
+ }
50
+
51
+ /**
52
+ * Run the installed previous CLI with the mock API as its whole world.
53
+ *
54
+ * ASYNC on purpose. `spawnSync` blocks this process's event loop, and the mock API runs in this process โ€”
55
+ * so a synchronous spawn freezes the server and every request the child makes hangs until the child is
56
+ * killed on its timeout. That failure looks exactly like a compatibility failure (empty output, no requests
57
+ * recorded) and is not one, which is why the harness records the signal as well as the exit code below.
58
+ */
59
+ function runOldCli(cliEntry, cwd, baseUrl, args, { timeoutMs = 60_000 } = {}) {
60
+ return new Promise((resolve) => {
61
+ const child = spawn(process.execPath, [cliEntry, ...args], {
62
+ cwd,
63
+ // stdin CLOSED, not inherited: an open pipe leaves any interactive prompt waiting for ever.
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ env: {
66
+ ...process.env,
67
+ LUA_API_URL: baseUrl,
68
+ LUA_AUTH_URL: baseUrl,
69
+ LUA_API_KEY: 'compat-matrix-not-a-real-key',
70
+ // Keep the child from phoning home for an update check or writing analytics during the matrix.
71
+ LUA_DISABLE_ANALYTICS: '1',
72
+ LUA_SKIP_UPDATE_CHECK: '1',
73
+ CI: '1',
74
+ NO_COLOR: '1',
75
+ },
76
+ });
77
+ let stdout = '';
78
+ let stderr = '';
79
+ child.stdout.on('data', (c) => (stdout += c));
80
+ child.stderr.on('data', (c) => (stderr += c));
81
+ const timer = setTimeout(() => child.kill('SIGTERM'), timeoutMs);
82
+ child.on('error', (error) => {
83
+ clearTimeout(timer);
84
+ resolve({ status: null, signal: null, stdout, stderr, error });
85
+ });
86
+ child.on('close', (status, signal) => {
87
+ clearTimeout(timer);
88
+ resolve({ status, signal, stdout, stderr });
89
+ });
90
+ });
91
+ }
92
+
93
+ /** One half of the matrix: a server serving old-only or new-bearing shapes, and the three verbs against it. */
94
+ async function runLeg({ withNewFields, cliEntry, projectDir }) {
95
+ const label = withNewFields ? 'WITH the new fields' : 'WITHOUT the new fields';
96
+ const server = await startCompatServer({ withNewFields });
97
+ const failures = [];
98
+ try {
99
+ const checks = [
100
+ { verb: 'view', args: ['workflows', 'view', 'compat', '--json'] },
101
+ { verb: 'list', args: ['workflows', 'list', '--json'] },
102
+ { verb: 'status', args: ['workflows', 'status', RUN_ID, '--steps', '--json'] },
103
+ // `watch` must reach the terminal frame and exit 0 even with event types it does not know.
104
+ { verb: 'watch', args: ['workflows', 'watch', RUN_ID, '--timeout', '20'] },
105
+ // The push half of ยง6.9: the old client compiles its own source and sends a body with none of the new
106
+ // members. `compile` is part of it on purpose โ€” a push body an older CLI cannot even BUILD is the same
107
+ // failure as one the server rejects.
108
+ { verb: 'compile', args: ['compile', '--ci'] },
109
+ { verb: 'push', args: ['push', 'workflow', '--force'] },
110
+ ];
111
+ for (const { verb, args } of checks) {
112
+ const out = await runOldCli(cliEntry, projectDir, server.baseUrl, args);
113
+ const ok = out.status === 0;
114
+ say(
115
+ ` ${ok ? 'โœ“' : 'โœ—'} ${TARGET} ยท ${verb} ยท ${label}${ok ? '' : ` (${out.status === null ? `signal ${out.signal}` : `exit ${out.status}`})`}`
116
+ );
117
+ if (!ok) {
118
+ // `status: null` means the child never exited on its own โ€” name the signal and the spawn error, or the
119
+ // failure is indistinguishable from a real incompatibility.
120
+ const how = out.status === null ? `killed by ${out.signal ?? 'an unknown signal'}` : `exit ${out.status}`;
121
+ failures.push(`${verb} (${label}) ${how}`);
122
+ if (out.error) say(` spawn error: ${out.error.message}`);
123
+ say(` stdout: ${(out.stdout || '').trim().slice(0, 2500)}`);
124
+ say(` stderr: ${(out.stderr || '').trim().slice(0, 2500)}`);
125
+ say(` routes: ${server.requests.join(' | ')}`);
126
+ }
127
+ }
128
+ // A leg that asked the server for nothing proves nothing โ€” the "No tests found" failure mode.
129
+ if (server.requests.length === 0) failures.push(`${label}: the old CLI made no requests at all`);
130
+
131
+ // The push must have actually reached the version route, and what it sent is the thing under test:
132
+ // a body an older CLI builds carries NONE of the new members and leaves the envelope marker at 1.
133
+ if (server.pushed.length === 0) {
134
+ failures.push(`${label}: the old CLI never sent a version body`);
135
+ } else {
136
+ for (const body of server.pushed) {
137
+ const marker = body?.graph?.luaWorkflow;
138
+ if (marker !== 1) failures.push(`${label}: the old CLI sent luaWorkflow ${marker}, expected 1`);
139
+ const nodes = body?.graph?.definition?.graph ?? [];
140
+ const leaked = JSON.stringify(nodes).match(/"(taskClass|requires|modelReason|effort)"/g);
141
+ if (leaked) failures.push(`${label}: the old CLI's body carried ${[...new Set(leaked)].join(', ')}`);
142
+ say(` โœ“ ${TARGET} ยท push body ยท ${label} ยท luaWorkflow ${marker}, no new members`);
143
+ }
144
+ }
145
+ } finally {
146
+ await server.close();
147
+ }
148
+ return failures;
149
+ }
150
+
151
+ async function main() {
152
+ say(`\n๐Ÿงญ CLI compatibility matrix โ€” driving lua-cli@${TARGET} against the current shapes\n`);
153
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-compat-'));
154
+ const failures = [];
155
+ try {
156
+ // 1. Install the previous release into a scratch directory of its own.
157
+ say(` โ€ฆ installing lua-cli@${TARGET} into ${tmp}`);
158
+ fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'compat-host', private: true }), 'utf8');
159
+ execFileSync('npm', ['install', '--no-audit', '--no-fund', '--silent', `lua-cli@${TARGET}`], {
160
+ cwd: tmp,
161
+ stdio: 'inherit',
162
+ timeout: 300_000,
163
+ });
164
+ const cliEntry = path.join(tmp, 'node_modules', 'lua-cli', 'dist', 'index.js');
165
+ if (!fs.existsSync(cliEntry)) throw new Error(`installed lua-cli@${TARGET} has no dist/index.js at ${cliEntry}`);
166
+
167
+ // 2. A project directory the old CLI recognises. No credential file: the key rides the environment.
168
+ const projectDir = path.join(tmp, 'project');
169
+ fs.mkdirSync(projectDir, { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(projectDir, 'lua.skill.yaml'),
172
+ [
173
+ 'agent:',
174
+ ` agentId: ${AGENT_ID}`,
175
+ ' orgId: org_compat',
176
+ ' name: compat',
177
+ 'workflows:',
178
+ ' - name: compat',
179
+ ` workflowId: ${WORKFLOW_ID}`,
180
+ ' version: 1.0.0',
181
+ '',
182
+ ].join('\n'),
183
+ 'utf8'
184
+ );
185
+ // A project the OLD CLI can compile: nothing in it uses anything this change added, which is the point.
186
+ fs.mkdirSync(path.join(projectDir, 'src', 'workflows'), { recursive: true });
187
+ fs.writeFileSync(
188
+ path.join(projectDir, 'package.json'),
189
+ JSON.stringify({ name: 'compat', private: true, type: 'module', dependencies: { 'lua-cli': TARGET } }, null, 2),
190
+ 'utf8'
191
+ );
192
+ fs.symlinkSync(path.join(tmp, 'node_modules'), path.join(projectDir, 'node_modules'), 'dir');
193
+ fs.writeFileSync(
194
+ path.join(projectDir, 'tsconfig.json'),
195
+ JSON.stringify(
196
+ {
197
+ compilerOptions: {
198
+ target: 'ES2022',
199
+ module: 'ESNext',
200
+ moduleResolution: 'bundler',
201
+ strict: true,
202
+ skipLibCheck: true,
203
+ esModuleInterop: true,
204
+ outDir: 'dist',
205
+ },
206
+ include: ['src/**/*'],
207
+ },
208
+ null,
209
+ 2
210
+ ),
211
+ 'utf8'
212
+ );
213
+ fs.writeFileSync(
214
+ path.join(projectDir, 'src', 'workflows', 'compat.ts'),
215
+ [
216
+ "import { createWorkflow } from 'lua-cli';",
217
+ "import { z } from 'zod';",
218
+ '',
219
+ 'export const compat = createWorkflow({',
220
+ " name: 'compat',",
221
+ " description: 'compatibility matrix fixture',",
222
+ ' inputSchema: z.object({}),',
223
+ ' outputSchema: z.object({ text: z.string() }),',
224
+ '})',
225
+ " .agentStep('summarise', { agentId: '$self', toolScope: {}, prompt: 'hi' })",
226
+ ' .commit();',
227
+ '',
228
+ ].join('\n'),
229
+ 'utf8'
230
+ );
231
+ fs.writeFileSync(
232
+ path.join(projectDir, 'src', 'index.ts'),
233
+ [
234
+ "import { LuaAgent } from 'lua-cli';",
235
+ "import { compat } from './workflows/compat';",
236
+ '',
237
+ 'export default new LuaAgent({',
238
+ " name: 'compat',",
239
+ " persona: 'compatibility matrix fixture',",
240
+ ' workflows: [compat],',
241
+ '});',
242
+ '',
243
+ ].join('\n'),
244
+ 'utf8'
245
+ );
246
+
247
+ // 3. Both halves.
248
+ for (const withNewFields of [false, true]) {
249
+ failures.push(...(await runLeg({ withNewFields, cliEntry, projectDir })));
250
+ }
251
+ } finally {
252
+ fs.rmSync(tmp, { recursive: true, force: true });
253
+ }
254
+
255
+ if (failures.length) {
256
+ say(`\nโœ— compatibility matrix: ${failures.length} failure(s)`);
257
+ for (const f of failures) say(` โ€ข ${f}`);
258
+ process.exit(1);
259
+ }
260
+ say(`\nโœ“ compatibility matrix: lua-cli@${TARGET} pushes, views and watches against the current shapes.\n`);
261
+ }
262
+
263
+ main().catch((err) => {
264
+ process.stderr.write(`compatibility matrix failed to run: ${err?.stack || err}\n`);
265
+ process.exit(1);
266
+ });
@@ -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.34.0",
23
+ "lua-cli": "^3.36.0",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"