lua-cli 3.30.0 → 3.32.1

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.
Files changed (62) hide show
  1. package/dist/api-exports.d.ts +1096 -41
  2. package/dist/api-exports.js +5544 -137
  3. package/dist/api-exports.js.map +1 -1
  4. package/dist/index.js +21255 -8412
  5. package/dist/index.js.map +1 -1
  6. package/dist/voice/test/index.d.ts +4 -4
  7. package/dist/workflow-builder.d.ts +800 -0
  8. package/dist/workflow-builder.js +6273 -0
  9. package/dist/workflow-builder.js.map +1 -0
  10. package/docs/API_INDEX.md +2 -0
  11. package/docs/README.md +27 -9
  12. package/docs/api/Jobs.md +10 -10
  13. package/docs/api/LuaWorkflow.md +89 -0
  14. package/docs/api/Workflows.md +111 -0
  15. package/docs/workflows/approvals.md +41 -0
  16. package/docs/workflows/artefacts-and-datasets.md +19 -0
  17. package/docs/workflows/coding-harness.md +12 -0
  18. package/docs/workflows/compliance-gates.md +16 -0
  19. package/docs/workflows/connections-in-coding-turns.md +10 -0
  20. package/docs/workflows/correlation-keys.md +11 -0
  21. package/docs/workflows/env-overlays.md +12 -0
  22. package/docs/workflows/evidence-bundles.md +11 -0
  23. package/docs/workflows/exports.md +5 -0
  24. package/docs/workflows/external-content-and-toolscope.md +11 -0
  25. package/docs/workflows/git-credentials.md +32 -0
  26. package/docs/workflows/goals.md +46 -0
  27. package/docs/workflows/knowledge-bindings.md +13 -0
  28. package/docs/workflows/limits.md +11 -0
  29. package/docs/workflows/long-steps-and-checkpoints.md +13 -0
  30. package/docs/workflows/migrating-cloud-tasks.md +9 -0
  31. package/docs/workflows/migrating-runs.md +13 -0
  32. package/docs/workflows/output-visibility.md +9 -0
  33. package/docs/workflows/per-item-approvals.md +9 -0
  34. package/docs/workflows/private-network-sources.md +12 -0
  35. package/docs/workflows/recovery.md +32 -0
  36. package/docs/workflows/replay-local.md +35 -0
  37. package/docs/workflows/reply-channels.md +11 -0
  38. package/docs/workflows/retention-and-archival.md +82 -0
  39. package/docs/workflows/roles.md +12 -0
  40. package/docs/workflows/schedules.md +26 -0
  41. package/docs/workflows/script-form.md +50 -0
  42. package/docs/workflows/testing-offline.md +49 -0
  43. package/docs/workflows/workspace-backends.md +11 -0
  44. package/docs/workflows/workspaces-and-long-steps.md +29 -0
  45. package/package.json +8 -3
  46. package/scripts/run-api-extractor.mjs +1 -1
  47. package/template/.gitignore +2 -0
  48. package/template/examples/workflows/CLAUDE.md +27 -0
  49. package/template/examples/workflows/adversarial-verify.workflow.script.js +48 -0
  50. package/template/examples/workflows/github-review.webhook.ts +19 -0
  51. package/template/examples/workflows/linear-ready.trigger.ts +21 -0
  52. package/template/examples/workflows/outreach.ts +55 -0
  53. package/template/examples/workflows/pr-review-round.ts +75 -0
  54. package/template/examples/workflows/provision-tenant.ts +35 -0
  55. package/template/examples/workflows/refund-approval.ts +57 -0
  56. package/template/examples/workflows/research-brief.ts +42 -0
  57. package/template/examples/workflows/reviewed-brief.ts +19 -0
  58. package/template/examples/workflows/support-triage.ts +81 -0
  59. package/template/examples/workflows/ticket-to-pr.ts +137 -0
  60. package/template/examples/workflows/vendor-invoices.ts +83 -0
  61. package/template/lua.skill.yaml +1 -0
  62. package/template/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ # Exports on terminal state
2
+
3
+ _Source of truth: workflows-spec (Cluster K B45). This page is the developer summary; the spec is normative._
4
+
5
+ `workflowPolicy.export.onTerminal` (org policy) writes an evidence bundle for every terminal run to the configured destination — the always-on sibling of the manual [evidence bundles](./evidence-bundles.md). `lua workflows exports` lists what was written (destination, run, status); failures surface in the org inbox, and the run itself is never blocked by an export failure.
@@ -0,0 +1,11 @@
1
+ # External content and `toolScope` — the taint rule
2
+
3
+ _Source of truth: workflows-spec (Cluster E, P1-16). This page is the developer summary; the spec is normative._
4
+
5
+ A step is **external-content tainted** when anything untrusted can reach its prompt: customer-channel input (`${initData.message}` on a customer-started run), `{knowledge}` bindings, or output flowing from a tool of the external kinds (`web-search`, `web-fetch`, `email-read`, `http`, `scrape`). Taint **propagates along bindings** — a step consuming a tainted step's output is tainted too (`computeExternalContentTaint` walks the plan).
6
+
7
+ **A tainted `agentStep` must declare `toolScope`** — `{}` means "no tools" (and `defaultToolScopeMode:'deny'` implies it when absent). The point: prompt-injected content must not be able to steer an agent that holds tools.
8
+
9
+ Walk-through — `support-triage` (§3.2 (g)): `classify` renders the customer message AND a knowledge binding ⇒ tainted ⇒ `toolScope: {}`; the downstream `refundGate` approval routes to `org-admins` (never `'creator'` — on a customer-channel run that is `approver-is-run-principal`).
10
+
11
+ Push refuses a tainted step without `toolScope` (`external-content-needs-toolscope`); the desktop run detail marks tainted steps with the provenance chain (`via`).
@@ -0,0 +1,32 @@
1
+ # Git credentials in workflows
2
+
3
+ _Source of truth: workflows-spec 11 §11.5.6 / 05 §5.17.3. This page is the developer summary; the spec is normative._
4
+
5
+ `workspace.credentialsRef` names a **git-host connection** (e.g. your org's GitHub App install). Per Job, the platform mints a **short-lived token held by a sidecar** — git operations in the step go through the credential proxy; the token never enters the agent container and never appears in `env`.
6
+
7
+ > **Never put a PAT in `env`.** Rotation or revocation of the underlying connection makes the next mint fail: the step fails `credentials_revoked{git_token_mint_failed}` and — with `onError:'park'` — enters the park/`retry-step` loop, so a re-grant resumes the run without losing work.
8
+
9
+ ## Declare a key; it resolves on any agent
10
+
11
+ Don't freeze a connection id in source. Declare the connection once on `createWorkflow` and reference it by key:
12
+
13
+ ```ts
14
+ createWorkflow({
15
+ name: 'ticket-pilot',
16
+ connections: [{ key: 'github', integrationType: 'github', required: true }],
17
+ workspace: { kind: 'git', repo: 'acme/app', credentialsRef: 'github' },
18
+ // …
19
+ });
20
+ ```
21
+
22
+ At run time the engine resolves `github` against the **owner agent's own connections** — an agent-scoped GitHub connection first, then an org-scoped one; a user's personal connection is never picked. The same definition therefore runs unchanged on a template install (where the install binding still wins), on a hand-built agent and on a duplicated agent — no "two builds of one workflow", no compile-time env var. `requiredConnections` on a step accepts declared keys the same way.
23
+
24
+ - The resolution is recorded once per run as `connection.resolved {key, connectionId, scope}`; `lua workflows status <runId> --steps --json` shows it under `connections.resolved`, beside `connections.declared` (the keys the version declares) — a declared key missing from `resolved` never resolved on that run.
25
+ - A step that fails before the key resolves carries the reason on `error` (`message`, `providerStatus`, `providerMessage` and the engine's `detail` bag — e.g. `detail.connectionId` names the ref the git mint was asked for).
26
+ - Two connections of the same type on the agent are ambiguous — label the one to use with the key (its display name) or the step fails `credentials_unresolved` listing the candidates.
27
+ - No connection of that type at all ⇒ `credentials_unresolved` (non-retryable, no turn billed) and a notice naming the key and the integration to connect; with `onError:'park'` the run resumes through `retry-step` once it is connected. A revoked or unhealthy connection still fails `credentials_revoked` exactly as before.
28
+ - A literal connection id keeps working exactly as it does today. A key that is not declared — with or without a `connections` block — fails `lua compile` and `lua push` with `connection-key-undeclared`, which names the declaration to add.
29
+
30
+ - `jobTools:['gh']` is what mints `GH_TOKEN` for a code step (`pull_requests:write` + `contents:write` + `issues:read`, repo-scoped). Merge is a contents write — human-only-merge orgs keep `contents:write` off the App.
31
+ - Templates declare `connections[].key` too: an install binds the key to the installer's connection; a definition installed without a binding falls back to the run-time resolution above.
32
+ - Offline, `credentialsRef` is printed and **not** resolved (no token mint) — `--credentials-ref-ok` suppresses the notice; the local driver uses your own git credentials.
@@ -0,0 +1,46 @@
1
+ # Goals
2
+
3
+ _Source of truth: workflows-spec 05 §5.20 / 09 §9.5.9. This page is the developer summary; the spec is normative._
4
+
5
+ A goal runs a workflow again and again until a judge says it is done: `objective` is what "done" means, `judge` decides it after every iteration (a deterministic `predicate` over the run's `{ output, state, steps.<stepId>, iteration, runStatus }`, or a judge agent answering a JsonSchema with a boolean `done`), `cadence` is when the next iteration starts (a schedule Job the goal owns; none ⇒ the next iteration starts immediately), and `maxRuns` caps the loop. A goal is `active`, `paused` (by you, or by the platform on `budget` / `max_runs` / `strikes`), `done` (the judge said so) or `closed` (by you — final).
6
+
7
+ Goals are created from chat (`setWorkflowGoal`), from the SDK (`Workflows.setGoal`) and from the CLI; the CLI is the audit surface for all of them.
8
+
9
+ ## Verbs
10
+
11
+ ```bash
12
+ lua workflows goals list [<workflow>] [-i <workflow>] [--status active|paused|done|closed] [--limit n] [--cursor c] [--json]
13
+ lua workflows goals get <goalId> [--json] # the goal card + its iteration runs with verdicts
14
+ lua workflows goals pause <goalId> # R60 — the cadence stays with the goal
15
+ lua workflows goals resume <goalId> # R61 — no backfill of missed fires
16
+ lua workflows goals close <goalId> [--note <text>] # R62 — final
17
+ lua workflows goals create <workflow> --objective <text> --max-runs <n> [-v <semver|latest|versionId>] \
18
+ (--judge-predicate '<path> <op> [value]' | --judge-agent <agentId|'$self'> [--judge-role <json|@file>] --schema <json|@file>) \
19
+ [--cadence <cron|json|@file> …] [--timezone <tz>] [--input <json|@file>] \
20
+ [--budget-credits <n>] [--max-total-credits <n>] [--idempotency-key <key>]
21
+ ```
22
+
23
+ `lua workflows view <workflow>` prints the workflow's goals (and its schedules); `--json` carries them under `goals` and `schedules`.
24
+
25
+ ### `goals create`
26
+
27
+ - `--judge-predicate` is the deterministic judge: `output.signups gte 50`, `output.done truthy`, `state.phase eq "shipped"`. The value is parsed as JSON when it parses (`50` → number, `true` → boolean) and kept as a string otherwise; ops are `exists · truthy · eq · neq · gt · gte · lt · lte`. A JSON object (`{"path":…,"op":…,"value":…}`) or `@file` works too.
28
+ - `--judge-agent` runs a judge turn instead: another agent by id, or `'$self'` — the workflow's own agent; quote it, an unquoted `$self` is expanded by the shell to an empty string (`self` is accepted as the same thing) — then `--judge-role` with the D25 `{ name, instructions, tools }` is required. `--schema` is required for an agent judge and must declare a boolean `done` at the root; the judge answers it and `done` ends the goal.
29
+ - `--cadence` is repeatable (≤ 5): a bare cron expression (`'0 9 * * 1'`, `--timezone Europe/London` applies to it) or a JSON slot / array for `interval` (`{"type":"interval","seconds":3600}`) and `once` entries — a timezone for those goes inside the entry (`--timezone` beside JSON-only cadences is an error). `-v` pins the iteration version (a semver, `latest` or a version id — resolved like `start -v`); without it every iteration runs the active version. Without a cadence the goal runs `immediate`: each iteration starts as soon as the previous one is judged.
30
+ - `--input` seeds every iteration's run input; `--budget-credits` is the per-run budget; `--max-total-credits` is the lineage-wide gate that pauses the goal (`budget`) when the sum of its runs reaches it.
31
+
32
+ Example:
33
+
34
+ ```bash
35
+ lua workflows goals create -i outreach \
36
+ --objective "Reach 50 trial signups from the March cohort" \
37
+ --judge-predicate 'output.signups gte 50' \
38
+ --cadence '0 9 * * 1' --timezone Europe/London \
39
+ --max-runs 12 --input '{"segment":"trial-march"}'
40
+ ```
41
+
42
+ ## Errors and exit codes
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`.
45
+
46
+ `lua workflows schedules delete` on a goal's cadence Job is refused (`goal_schedule`) — use `goals pause` / `goals close`; see [schedules.md](./schedules.md).
@@ -0,0 +1,13 @@
1
+ # Knowledge bindings
2
+
3
+ _Source of truth: workflows-spec (P1-11; Cluster K B41). This page is the developer summary; the spec is normative._
4
+
5
+ `fromKnowledge({ source, query, maxChars?, topK? })` as a map descriptor retrieves at render time (S2): each chunk arrives under a **provenance header** — `[knowledge source=<source> doc=<doc> chunk=<n> score=<s>]` — so the model can cite and the audit trail can trace. `maxChars` caps the rendered block; a retrieval timeout renders the EMPTY block plus a `knowledge: empty` warning (the run continues).
6
+
7
+ ## Connection sources (B41)
8
+
9
+ `source:'connection'` with `connectionId` retrieves from Drive / Notion / Confluence through the org's connection. Requirements: the step must carry `toolScope` (connection knowledge is external content — see [External content](./external-content-and-toolscope.md)); the compiler's overlay pass adds the knowledge connection to the step's mounted set.
10
+
11
+ Any knowledge binding taints the step (P1-16).
12
+
13
+ Offline: knowledge retrieval is not emulated — a knowledge binding fails closed (`binding_unresolved`) unless you `--step-output` the consuming step. (`--knowledge <key>=@file` substitution is specified in 03 §3.9 step 7 and not yet wired in this build.)
@@ -0,0 +1,11 @@
1
+ # Limits and caps
2
+
3
+ _Source of truth: workflows-spec 10 / 16. This page is the developer summary; the spec is normative._
4
+
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
+
7
+ ## Org pacing
8
+
9
+ `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.
10
+
11
+ Offline, the driver has no org context: a `tool`/`http` step dispatches immediately and `pacing_deferred` never appears locally. Customer-side pacing of a `foreach` (`rateLimit: { perSecond | perMinute }`) IS emulated — an in-memory token bucket printing `[throttle:<id>] resumeAt=<iso>`.
@@ -0,0 +1,13 @@
1
+ # Long steps and checkpoints (Cluster I)
2
+
3
+ _Source of truth: workflows-spec 05 §5.17.4 (D19-r2). This page is the developer summary; the spec is normative._
4
+
5
+ A Job step may run up to **24 h** (`timeoutSeconds` ≤ 86 400). The platform runs it as **segments** of ≤ 4 h: at every 4 h boundary — and every 30 min in between — your branch is committed and pushed and the coding session is saved; the next segment resumes from that commit on the same volume with the same attempt and the same billing epoch; a code step is re-entered from the top with `LUA_WF_JOB_SEGMENT` set, so **make it idempotent on the tree**.
6
+
7
+ - A step past 14 400 s requires a workspace (`long-job-requires-workspace`) — the checkpoint needs somewhere to commit.
8
+ - **Eviction** (`JOB_EVICTED`) resumes from the last checkpoint — at most 30 min of work is redone; no attempt bump.
9
+ - `lua workflows jobs` / `job-logs --segment <n>` read a segmented step; the timeline shows `s<n>/<total> · resumed from checkpoint`.
10
+
11
+ ## Rehearsing offline
12
+
13
+ `--segment-wall <s>` (default 14 400) shrinks the segment wall — set it to `30` to exercise a 24 h step in a minute: the driver commits the checkpoint literally (`[lua-wf] checkpoint <stepId> s<n>`), saves the session under `<workspace>/.home/.lua-wf/`, and re-enters the step with `LUA_WF_JOB_SEGMENT` bumped. `--evict-at <stepId>=<s>` simulates an eviction; `--checkpoint-interval <s>` makes interval checkpoints visible.
@@ -0,0 +1,9 @@
1
+ # Migrating cloud tasks (jobs) to workflows
2
+
3
+ _Source of truth: workflows-spec (Cluster J B9, D12-r2). This page is the developer summary; the spec is normative._
4
+
5
+ `lua workflows import-job <jobName>` writes a one-step workflow file wrapping the job's `execute` (schedule + input carried over) into `src/workflows/`. It **writes a file** — it does not deploy anything.
6
+
7
+ **Deactivate the agent job yourself** (D12-r2: the platform never migrates a job) — after you push and activate the workflow, run `lua jobs`-side deactivation, or both will fire.
8
+
9
+ The unified read: `lua workflows list --all` / the desktop cloud-tasks view show jobs and workflows side by side so nothing is lost mid-migration.
@@ -0,0 +1,13 @@
1
+ # Migrating runs between versions
2
+
3
+ _Source of truth: workflows-spec (Cluster D R46/R47, D7-r2). This page is the developer summary; the spec is normative._
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.
6
+
7
+ ## Edited inputs
8
+
9
+ `--resume-override <path>=<json>` patches the seeded input where the target version's schema requires it — validated against the TARGET schema before anything moves.
10
+
11
+ ## Running runs
12
+
13
+ A **running** run is scheduled and moves at its next step boundary or park — never mid-step; after `LUA_WF_MIGRATION_DRAIN_MAX_HOURS` it is refused, not forced. Suspended/gated runs move immediately; incompatible runs are reported per-run with the incompatibility, never skipped silently.
@@ -0,0 +1,9 @@
1
+ # Output visibility
2
+
3
+ _Source of truth: workflows-spec (Cluster K B44). This page is the developer summary; the spec is normative._
4
+
5
+ `outputVisibility: { roles: [...] }` on `createWorkflow` **restricts who can READ what a workflow produced; it never restricts who can run it.** A reader outside the roles gets `restricted: true` and no `output` from `Workflows.get`/`list` — never an error.
6
+
7
+ - ANDs with the org's `workflowPolicy.artefactAcl` — the narrower set wins.
8
+ - **Owner bypass** is on by default; every bypass read leaves an audit line.
9
+ - The ACL is hashed into `run.aclHash` at start: **changing roles affects new runs only.**
@@ -0,0 +1,9 @@
1
+ # Per-item approvals
2
+
3
+ _Source of truth: workflows-spec 06 (Cluster K B20, WF-544). This page is the developer summary; the spec is normative._
4
+
5
+ `itemsPath: 'invoices'` fans one approval node out to **one decision per item**; `itemApprover: { fromItem: 'approverEmail' }` routes each item to ITS approver (the parent `approver` keeps the overview card); `itemTimeout: { timeoutHours }` expires unanswered items as `expired`. The node resumes with `resumeData.items[]` (+ `approved = every item approved`) — feed approved rows to a `foreach` (see the `vendor-invoices` example).
6
+
7
+ > **T50 caution:** `fromItem` trusts the payload to NAME the approver; membership + `workflows:execute` is what makes it safe — never derive the approver from external content.
8
+
9
+ Offline: `--approve-item <id>=<index>:approve|deny[=@payload]` (repeatable); `--approve <id>` on an items approval is exit 2 ("per-item approval: use --approve-item"). Text channels approve/deny the whole batch only.
@@ -0,0 +1,12 @@
1
+ # Private-network sources
2
+
3
+ _Source of truth: workflows-spec (ICP P1-20). This page is the developer summary; the spec is normative._
4
+
5
+ **The boundary:** the sandbox reaches the public internet on any port and Lua services; it does **not** reach `10/8`, `172.16/12`, `192.168/16`, `169.254/16`, `100.64/10` — a VPC database, an on-prem ERP, a peered network.
6
+
7
+ **The exact error:** a step that tries gets `EGRESS_DENIED { host, ip, port }` within seconds — never a `WALL_TIMEOUT`. If you see a hang instead, you are not hitting this boundary.
8
+
9
+ ## The two supported patterns
10
+
11
+ 1. **Expose the source behind a public TLS endpoint** (or an API gateway) with an allow-list — the sandbox reaches any public host/port.
12
+ 2. **Ask for a partner carve-out** — a platform-team lua-iac PR adding your source's `/32` + port to the runner and Job NetworkPolicies (`platforms/aws/prod/INTEGRATIONS.md`; carve-outs are CI-guarded by `policy/check-partner-egress.py`). Budget lead time, and supply the exact `/32` and port.
@@ -0,0 +1,32 @@
1
+ # When a step parks
2
+
3
+ _Source of truth: workflows-spec 01 §1.6-r1 / 06 §6.3.5. This page is the developer summary; the spec is normative._
4
+
5
+ A **park** is the platform refusing to guess. Two things park a step:
6
+
7
+ 1. **`sideEffects:'external'` + a platform fault.** A worker crash, an eviction or a network partition while an external-effect step was in flight means the platform cannot know whether the effect happened (`EFFECT_IN_DOUBT`). **The platform never re-runs an external step on its own** — the step parks and the run gates `exception`.
8
+ 2. **`onError:'park'`.** A final (retries exhausted, or non-retryable) failure parks instead of failing the run — for steps where an operator should decide. The row carries `park: { reason: 'onerror_park' }`, the run is `suspended` with `gate.kind: 'exception'`, and the events show `step.parked` + `run.gated`; nothing downstream is skipped. A failure that happened **before dispatch** (`binding_unresolved`, `input_schema_invalid`, `credentials_unresolved`, `credentials_revoked`, `TOOL_REF_UNRESOLVED`, `BUNDLE_UNRESOLVED`) parks the same way — but a plain `retry-step` re-renders the same input against the same definition and connections and parks again unless one of those was fixed in between; the retry response carries `detail.hint` saying so, and `resolve-step` is the way past it.
9
+
10
+ If a **sibling** step fails under `onError:'fail'` while the run is parked, the run keeps the park but is marked `failing`: after `retry-step` the parked step still re-runs, and the run then terminalizes `failed` (never `completed`) once nothing is in flight.
11
+
12
+ ## The three verbs
13
+
14
+ A parked step waits for exactly one of:
15
+
16
+ - **`retry-step`** — re-runs `execute` with the **same `occurrenceId`** (`${lineageId}:${stepId}`), so every `ctx.once(key, fn)` effect that already settled returns its stored result and is never re-sent.
17
+ - **`resolve-step`** — you supply the step's output (validated against `outputSchema`); the run continues as if the step had returned it.
18
+ - **`skip`** — marks the row skipped; downstream bindings render `status="skipped"`.
19
+
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
+
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.
23
+
24
+ ## Repair runs
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.
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}`).
29
+
30
+ ## Rehearsing offline
31
+
32
+ `lua test workflow <name> --park <stepId>` simulates the platform-fault reclaim locally and offers the same verbs (`retry / skip / complete / fail`) on stdin — see [Testing offline](./testing-offline.md).
@@ -0,0 +1,35 @@
1
+ # `lua workflows replay <runId> --local` — runtime packaging
2
+
3
+ _Source of truth: workflows-spec 03 §3.10, 04 §4.3.5, 05 §5.7.1, 12 §12.9.11 (WF-324 / WF-332, WB-09). This page records the packaging decision for the script-form replay wrapper; the spec sections are normative._
4
+
5
+ ## The decision (wave 5, WB-09)
6
+
7
+ `replay --local` for a **script** run re-runs the runner's own replay wrapper (`buildWorkflowScriptWrapper`, `@lua/sandbox-runtime`) against the R6 `?format=replay` bundle and diffs the re-issued `callHash`es against the server journal. `lua test workflow <name>` on a `*.workflow.script.js` file drives the same wrapper through an offline tick loop.
8
+
9
+ Two options were on the table:
10
+
11
+ | Option | Shape | Verdict |
12
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | **A — project-local runtime, resolved lazily** | `@lua/sandbox-runtime` is NOT bundled into the published CLI. The verb resolves it with `createRequire(<project>/package.json)` at call time; a miss is the typed error `REPLAY_RUNTIME_UNAVAILABLE` (exit 3), never a silent "stable". | **Chosen.** |
14
+ | B — a published replay runtime (`@lua/workflow-replay`) | Extract the wrapper source + `createSandboxContext` into a small published package the CLI depends on. | Deferred: it would fork the wrapper text away from the runner (the §2.2 parity rule says one implementation), and the wrapper is versioned by `journalProtocolVersion` (N-1 window, 04 §4.3.5) — a second package would have to track that window. Revisit at GA if partner projects outside the monorepo need `replay --local`. |
15
+
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
+
18
+ ## What it means for a project
19
+
20
+ - Inside the monorepo (or any project that lists `@lua/sandbox-runtime` in its devDependencies): `replay --local` and `lua test workflow <script>` work as-is.
21
+ - Elsewhere: `REPLAY_RUNTIME_UNAVAILABLE: replay --local needs @lua/sandbox-runtime resolvable from the project (…)` — add the devDependency, or run the verb from a monorepo checkout with `--project <dir>`.
22
+ - 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
+
24
+ ## Exit matrix (script form)
25
+
26
+ | Exit | Meaning |
27
+ | ---- | ------------------------------------------------------------------------------------------------------------ |
28
+ | 0 | stable — every async journal entry re-issued with the same `callHash` |
29
+ | 3 | runtime unavailable (`REPLAY_RUNTIME_UNAVAILABLE`) or the run/bundle could not be fetched |
30
+ | 4 | `LEDGER_DIVERGENCE` / `JOURNAL_DIVERGENCE` — at least one `hash_mismatch` / `journal_longer_than_script` row |
31
+
32
+ ## Related
33
+
34
+ - `docs/workflows/script-form.md` — authoring `*.workflow.script.js`, `lua push`, `lua test workflow` on a script.
35
+ - `packages/lua-cli/src/utils/workflow-script-replay.ts` — `loadLocalScriptRunner`, `compareScriptReplay` (pure; what the tests pin).
@@ -0,0 +1,11 @@
1
+ # Reply channels
2
+
3
+ _Source of truth: workflows-spec (Cluster K B42). This page is the developer summary; the spec is normative._
4
+
5
+ A run started from a conversation carries `replyTo: { channel, threadId }`; at terminal state the platform sends the outcome back through a **`ChannelAdapter`** — a new channel is one adapter file + one registry entry.
6
+
7
+ - Registered today: **whatsapp**. `sms` / `email` / `webchat` / `slack` are specified and land behind their E-10 pins; an unregistered channel fails loudly (`channel_unsupported`), never silently drops.
8
+ - Slack `threadId` format: `<channelId>:<thread_ts>`.
9
+ - A step may answer the customer itself mid-run (`Channels.<channel>.send` inside `ctx.once`) — the terminal reply then lands as a duplicate-safe fallback on the same thread.
10
+
11
+ Offline: `--reply-to <channel>:<threadId>` prints the would-be reply on the terminal.
@@ -0,0 +1,82 @@
1
+ # Workflows — retention and archival
2
+
3
+ _Source of truth: workflows-spec 02 §2.14, 03 §3.10, 11 §11.11.1 (WF-428 / WF-443, WB-01). This page is the operator-facing summary; the spec sections are normative._
4
+
5
+ ## Retention model
6
+
7
+ | Data | Clock | Default | Org control |
8
+ | --- | --- | --- | --- |
9
+ | `workflow_runs` and every child row (`workflow_step_runs`, `workflow_run_events`, `workflow_run_signals`, `workflow_artefacts`) | `expiresAt = completedAt + retentionDays` stamped by the run's terminal write; children tightened to the run's stamp | 90 d (`LUA_WF_RETENTION_DAYS_DEFAULT`) | `workflowPolicy.retentionDays` (R23), **7..90**, clamp-down only |
10
+ | Hidden `wf-` threads (agent-step + loopback threads) | `payloadsExpireAt` (else `expiresAt`) — sweep #31 `hidden-thread-gc` | same | same |
11
+ | `workflow_migrations` rows | `finishedAt + 400 d` — never purged with a run | fixed | none |
12
+ | Audit rows (lua-auth) | audit retention | fixed | none |
13
+
14
+ **Clamp-down only.** Lowering `retentionDays` restamps every terminal row of the org whose `expiresAt` sits past `completedAt + newDays` (the R23 write flags `retentionRestampPending`; sweep #13 `ttl-stamp` lowers the run, then steps/events/signals/artefacts with a `{expiresAt:{$gt}}` guard). Raising it restamps **nothing** — an `expiresAt` already stamped is never raised, and the platform 90 d stays the ceiling (`RETENTION_OUT_OF_RANGE` 400 outside 7..90). Every change emits audit `workflow.policy.retention_changed{from, to, restampPending}`.
15
+
16
+ Runs the org still needs past their retention must be **archived out** before the TTL fires — that is the `archive-runs` hop below.
17
+
18
+ ## `lua workflows archive-runs`
19
+
20
+ Pages the agent's terminal runs (R2, `status ∈ completed|failed|cancelled|timed_out|abandoned`, oldest first), requests an export bundle per run (R50 `POST /runs/:id/export` → R51 `GET /runs/:id/export`) at `--concurrency` (default 4), and writes each bundle plus an `archive-index.ndjson` to the sink.
21
+
22
+ ```
23
+ lua workflows archive-runs \
24
+ --agent <agentId> \
25
+ --since 2026-06-01 --until 2026-08-01 \
26
+ --sink s3://acme-workflow-archive/prod/ \
27
+ --connection <orgStorageConnectionId> \
28
+ --concurrency 4
29
+ ```
30
+
31
+ - **Sinks:** `s3://` and `gs://` through an org storage connection (`--connection`); the CLI never holds cloud credentials itself. A local directory sink is accepted for dry runs.
32
+ - **Skip rule:** a run is skipped (`skipped:'already_exported'` in the index) when `run.exportedAt ≥ run.completedAt` **and** the sink already holds the bundle whose `manifest.sha256` matches — re-running the hop is idempotent and cheap.
33
+ - **Index:** one line per run — `{runId, status, completedAt, exportedAt, sha256, objectKey, bytes, skipped?, error?}` (`WorkflowArchiveIndexRow` in `@lua/shared-types`).
34
+ - **Window guard:** a run whose `completedAt` is older than `retentionDays − 7 d` may be reaped mid-export; the hop refuses the whole window with `ARCHIVE_WINDOW_TOO_OLD` (exit 4) rather than produce a partial archive. Narrow `--since` or run the hop more often.
35
+
36
+ ### Exit matrix
37
+
38
+ | Exit | Meaning |
39
+ | --- | --- |
40
+ | 0 | every selected run exported or skipped |
41
+ | 1 | ≥ 1 run failed to export — listed in the index with `error`; re-run to retry only those |
42
+ | 3 | sink or storage connection unavailable — nothing written |
43
+ | 4 | `ARCHIVE_WINDOW_TOO_OLD` — the window reaches past `retentionDays − 7 d` |
44
+
45
+ ## Weekly cron template
46
+
47
+ Run the hop weekly per agent, one day inside the window guard. The CronJob manifest lives in lua-iac (`workload-cronjob-*.tf`) — this repo carries no k8s manifests. Every cronjob **must** heartbeat (repo `CLAUDE.md`): create the Betterstack heartbeat (period 7 d, grace ≈ 1 d), store its URL in Secrets Manager under `<service>/prod/heartbeat-url-wf-archive-<agent>`, and wrap the command:
48
+
49
+ ```yaml
50
+ command:
51
+ - /bin/sh
52
+ - -c
53
+ - |
54
+ set +e
55
+ lua workflows archive-runs --agent "$AGENT_ID" --since "$(date -u -d '-14 days' +%F)" \
56
+ --sink "$ARCHIVE_SINK" --connection "$STORAGE_CONNECTION_ID" --concurrency 4
57
+ EXIT=$?
58
+ if [ $EXIT -eq 0 ]; then
59
+ curl -fsS --max-time 10 --retry 2 "$HEARTBEAT_URL" || true
60
+ else
61
+ curl -fsS --max-time 10 --retry 2 --data-urlencode "exit=$EXIT" "$HEARTBEAT_URL/fail" || true
62
+ fi
63
+ exit $EXIT
64
+ env:
65
+ - name: HEARTBEAT_URL
66
+ valueFrom:
67
+ secretKeyRef:
68
+ name: lua-cli-archive-secrets
69
+ key: HEARTBEAT_URL_WF_ARCHIVE
70
+ optional: true
71
+ ```
72
+
73
+ The three invariants: the hop's own non-zero exit propagates (`exit $EXIT`), heartbeat pings use `|| true`, and `optional: true` on the secret ref keeps staging (no heartbeat) runnable.
74
+
75
+ ## Related sweeps
76
+
77
+ | # | name | what it does |
78
+ | --- | --- | --- |
79
+ | 13 | `ttl-stamp` | backstop stamp + clamp-down restamp + 400 d migration counter + orphan artefact reap |
80
+ | 15 | `wf-thread-ttl` | 90 d backstop for hidden threads minted before `payloadsExpireAt` existed |
81
+ | 19 | `org-purge` | offboarding / agent-delete cascade (fence → drain → done) |
82
+ | 31 | `hidden-thread-gc` | hidden `wf-` threads past the payload clock, through the conversation scrub |
@@ -0,0 +1,12 @@
1
+ # Roles — the workflow role library
2
+
3
+ _Source of truth: workflows-spec (Cluster J B18, D25-r1). This page is the developer summary; the spec is normative._
4
+
5
+ `lua roles` manages the owner's `workflowRoles[]` library (`list · view · save · delete`). Both forms bind a saved role:
6
+
7
+ - Builder: `specialistStep(id, { role: { ref: '<roleName>' }, … })` — or an inline `{ name, instructions, tools }` role (see the `reviewed-brief` example).
8
+ - Script form: `role: { ref: '<roleName>' }` in an `agent(...)` call — the CLI collects it into `roleBindings` with `status:'deferred'` (it never reads the library); resolution happens at push.
9
+
10
+ **A library edit never changes a published version — push again.** Versions pin the resolved role bytes; `lua workflows versions` marks a version whose role source has drifted with `⚠ stale`.
11
+
12
+ Role tools are a subset of the owning agent's toolset; delegation tools (`agent-*`) are never allowlistable. Offline, `--agents fake` prints `[fake:<id> as <role.name>]`; a role block under `--agents live` is a client-side approximation — the allowlist is enforced server-side only.
@@ -0,0 +1,26 @@
1
+ # Schedules
2
+
3
+ _Source of truth: workflows-spec 07 §7.1. This page is the developer summary; the spec is normative._
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>`).
6
+
7
+ ## Re-enabling a schedule
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.
10
+
11
+ Offline, schedules do not fire (`backfillOnEnable` is not emulated) — start runs with `lua test workflow` / `lua workflows start`.
12
+
13
+ ## From the CLI
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.
16
+
17
+ ```bash
18
+ lua workflows schedules list -i outreach # this workflow's schedule Jobs (omit -i for every workflow of the agent)
19
+ lua workflows schedules list --json # { success, data: { items: [{ jobId, workflowId, trigger, paused, nextRunAt, lastFiredAt?, consecutiveFailures, autoDisabled?, goalId? }] } }
20
+ lua workflows schedules delete <jobId> [--yes] # R28 — asks first unless --yes
21
+ lua workflows view outreach # the "Schedules:" line + table; --json carries the same rows under `schedules`
22
+ ```
23
+
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
+
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). 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 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). The server refuses it too: R28 answers `409 GOAL_SCHEDULE {goalId}` for every caller (SDK, desktop, curl), and the CLI renders that with the same message. See [goals.md](./goals.md).
@@ -0,0 +1,50 @@
1
+ # Script-form workflows — `src/workflows/<name>.workflow.script.js`
2
+
3
+ _Source of truth: workflows-spec 04 §4.3 (D15-r2, Cluster J B12 — WF-527, WB-09 push half / WB-03 script host). This page is the developer summary; the spec is normative._
4
+
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
+
7
+ ```js
8
+ export const meta = { name: 'judge-panel', description: 'Three drafts, three judges, synthesize the winner',
9
+ phases: [{ title: 'Draft' }, { title: 'Judge' }, { title: 'Synthesize' }] };
10
+ const drafts = await parallel([agent('Draft, technical angle', { phase: 'Draft' }), agent('Draft, market angle', { phase: 'Draft' })]);
11
+ const scores = await foreach(drafts, (d) => agent(`Score this draft: ${d}`, { phase: 'Judge' }));
12
+ const picked = await step('pick-winner', () => ({ winner: drafts[scores.indexOf(Math.max(...scores))] }));
13
+ return picked;
14
+ ```
15
+
16
+ ## Rules the CLI checks before anything is sent
17
+
18
+ Run in this order — the same order lua-api re-validates on push (one implementation, decision §2.2):
19
+
20
+ 1. **`meta` (04 §4.3.1)** — the FIRST statement, a pure object literal (strings/numbers/booleans/null/arrays/objects only): `name` `/^[a-z][a-z0-9-]{0,63}$/` **equal to the file stem** (`SCRIPT_META_INVALID{name-file-mismatch}`), `description` ≤ 500, optional `phases[]` (≤ 64 × `{title ≤ 80, detail? ≤ 300, model?}`), `whenToUse`, `concurrency` 1..32, `sampleArgs` (the `args` of the validation tick and of `lua test workflow`), ≤ 4 KB.
21
+ 2. **Determinism lint (04 §4.3.3)** — the banned identifier set is DERIVED from the sandbox's own lists (`WORKFLOW_SCRIPT_REMOVED_GLOBALS` + trapped timers + forbidden platform keys), plus `Date.now`, `new Date()` with no argument, `Math.random`, `eval`, `new Function`, `with`, `globalThis`, and any `import`/`export` other than `meta`. Every hit is `file:line:col — SCRIPT_NONDETERMINISM{<kind>} …`. **The closure passed to `step(label, fn)` is exempt** (its result is journaled once and memoised on replay); `fn` must be a function literal or a module-scope function (`step-fn-not-literal`), `timeoutSeconds` a literal 1..600 (`step-timeout-invalid`).
22
+ 3. **`role: { ref }`** collection — literal refs become `roleBindings[{ref, status:'deferred'}]` (the CLI never reads the role library; the server fills them at push); a non-literal `ref` is `role-ref-unknown`, `ref` mixed with inline members is `role-ref-and-inline`.
23
+ 4. **`scriptSurface`** — `{ hostCalls: {member: count}, hasExternalSideEffects }` (`tool(..., {sideEffects:'external'})`, `shell()`, `merge()`, Job-tier `agent()`), shown on the consent card instead of the text.
24
+ 5. **`scriptHash`** = `sha256-cj1:` + sha256(canonicalJson({ script, meta })).
25
+
26
+ ## `lua push workflow`
27
+
28
+ `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
+
30
+ ## `lua test workflow <name>` on a script
31
+
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:
33
+
34
+ | Flag | Effect |
35
+ | --- | --- |
36
+ | `--input <json|@file>` | the script's `args` (default `meta.sampleArgs ?? {}`) |
37
+ | `--step-output <label|seq>=<json>` | the completion for the intent with that `narration.label` (an `agent()`'s `label`/`phase`) or journal seq |
38
+ | `--max-ticks <n>` | default 64 — `SCRIPT_TICK_LIMIT` (exit 4) beyond it |
39
+ | `--ledger-out <file>` | the local journal (`{ form:'script', journal[] }`) |
40
+ | `--json` | the result envelope |
41
+
42
+ Exit 0 `done`/`bailed`; 4 `failed` (`SCRIPT_THREW`, `SCRIPT_DEADLOCK` — parked with zero intents, `SCRIPT_TICK_LIMIT`); 3 when `@lua/sandbox-runtime` is not resolvable from the project (`REPLAY_RUNTIME_UNAVAILABLE`, see `replay-local.md`); 2 on flag grammar.
43
+
44
+ ## `step(label, fn, { timeoutSeconds? })`
45
+
46
+ Journals as `kind:'code'` with `callHash = hash(label, fn.toString())`; replay memoises the recorded result and never re-executes the closure; more than 64 `step()` closures in one tick is `script-step-too-many`; a throw is `SCRIPT_STEP_THREW{message}` (catchable); the wall is `SCRIPT_STEP_TIMEOUT`. Every `step()` site makes the script non-representable as a graph (`lua workflows export --form graph` → `script-not-graph-representable{inline-code}`).
47
+
48
+ ## Example
49
+
50
+ `template/examples/workflows/adversarial-verify.workflow.script.js` — a finder/verifier loop with a `step()` reduction.
@@ -0,0 +1,49 @@
1
+ # Testing offline — the local reference driver
2
+
3
+ _Source of truth: workflows-spec 03 §3.9 (WF-204/WF-225/WF-332). This page is the developer summary; the spec is normative._
4
+
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
+
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).
8
+
9
+ ## Steering flags — fully scriptable, no stdin
10
+
11
+ | Flag | Effect |
12
+ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `--input @file\|<json>` | Run input (validated against `inputSchema`) |
14
+ | `--step-output <id>=@file\|<json>` (rep.) | Completes the row **without** running it — validated against the step's `outputSchema`. This is how a predicate over an agent output gets BOTH truth values under `--agents fake` |
15
+ | `--approve <id>[=@payload]` · `--deny <id>[=@reason]` | Pre-answer approvals (edited payloads use the server's `matchesEditablePath` + `editedPayloadSchema`) |
16
+ | `--signal <name>=@file\|<json>` (rep.) | Pre-supply `waitForSignal` payloads in order of arrival |
17
+ | `--fixtures <dir>` / `--record <dir>` | Replay / record agent+tool outputs (`<stepId>.<attempt>.json`); a missing fixture is exit **5** `FIXTURE_MISSING`, never a silent fake; `--step-output` wins over a fixture |
18
+ | `--from-run <runId>` | Seed every `completed` step from a real run (same math as repair runs); graph drift asks for `--force` |
19
+ | `--park <id>` (rep.) | Simulate a platform-fault park of a `sideEffects:'external'` step — then `retry / skip / complete / fail`, the production verbs (see [When a step parks](./recovery.md)) |
20
+ | `--fast-retries` | Collapse retry backoff waits to 0 |
21
+ | `--real-time` | Actually wait on `sleep`/backoff instead of fast-forwarding the virtual clock |
22
+ | `--now <iso>` | Pin the virtual clock start (resolves `sleepUntil` dateFrom, business-hours deadlines) |
23
+ | `--artefacts-dir <dir>` | Back `ctx.artefacts.*` on disk (`<dir>/<artefactId>` + `<artefactId>.meta.json`, ids `local-<n>`) |
24
+ | `--env KEY=value` (rep.) | The local `env.template()` overlay — a missing key is exit 2 `env-template-missing`; values never reach `--ledger-out` |
25
+ | `--step-wall <s>` | Per-step wall, default 600 |
26
+ | `--max-foreach-items <n>` | Override the local foreach cap |
27
+ | `--agents fake\|live` | Fake stubs (default) or the dev API |
28
+ | `--ledger-out <file>` | Write the in-memory ledger (steps, outputs, attempts, effects, events) — the input of `replay --local` |
29
+ | `--json` | Machine-readable result envelope |
30
+
31
+ ## Semantics worth knowing
32
+
33
+ - **`sleep(ms)`** advances the virtual clock by exactly `ms` **milliseconds** — the SDK's unit and the engine's (`.sleep(3000)` is 3 s, printed as `<id> · sleep 3000 ms (3 s) (virtual clock)`); `sleepUntil` jumps to the resolved instant. `--real-time` waits the same `ms`.
34
+ - **Retry backoff** runs on the virtual clock: `backoffSeconds · 2^(attempt-1)` capped at `maxBackoffSeconds`, printed as `[retry:<id>] backoff <n>s`.
35
+ - **`foreach` `rateLimit`** is an in-memory token bucket — delayed starts print `[throttle:<id>] resumeAt=<iso>`; loop `intervalSeconds` prints `kind=loop_interval`.
36
+ - **`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
+ - **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
+
40
+ ## Example
41
+
42
+ ```bash
43
+ lua test workflow outreach --input @leads.json \
44
+ --step-output draftEmail=@fixtures/draft.json \
45
+ --approve reviewDrafts=@edited.json \
46
+ --ledger-out out.json
47
+ ```
48
+
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).
@@ -0,0 +1,11 @@
1
+ # Workspace backends — `ebs` / `efs` / `s3`
2
+
3
+ _Source of truth: workflows-spec 05 §5.17 (D27). This page is the developer summary; the spec is normative._
4
+
5
+ `workspace.backend`:
6
+
7
+ - **`ebs`** — the default and what every org has today: one zonal volume, one clone per parallel arm, merged through your remote.
8
+ - **`efs`** — arms get one shared checkout and a local merge (shared `mount:'ro'` arms are allowed in one `parallel`; merge without fetch).
9
+ - **`s3`** — no volume; a prefix is synced at every checkpoint — larger workspaces, slower steps.
10
+
11
+ `efs` and `s3` are **refused, not downgraded**, until the platform opens them for your org (`workspace-backend-unavailable`, gate `EXT-EFS` — see [Compliance gates](./compliance-gates.md)). The local driver prints the declared backend, always uses a local dir, and applies the `efs` validator differences so an offline run matches what an open gate would do.
@@ -0,0 +1,29 @@
1
+ # Workspaces and long steps — the Job tier
2
+
3
+ _Source of truth: workflows-spec 05 §5.17 (D19-r1/r2). This page is the developer summary; the spec is normative._
4
+
5
+ `tier:'job'` runs a step as a **Kubernetes Job per attempt** — up to **24 h** as a chain of ≤ 4 h segments (see [Long steps and checkpoints](./long-steps-and-checkpoints.md)), with `jobResources: 'small'|'medium'|'large'`, a real filesystem and real cancel.
6
+
7
+ ## Workspaces
8
+
9
+ `workspace` on `createWorkflow` declares the checkout: `{ kind:'git', repo, ref, credentialsRef, sizeGb?, verify? }` or `{ kind:'empty' }`. Steps mount it with `workspace: { mount: 'rw'|'ro', isolation?: 'worktree' }` and read `ctx.workspace = { path, mount, branch }`.
10
+
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
+
13
+ **The release-at-suspension rule:** when your run waits for a person or a signal, its volume is released: committed work is on the run branch `lua/wf-<lineageId>` on your remote; untracked files — `node_modules`, build output — are rebuilt when the next step restores the checkout.
14
+
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
+
17
+ ## Coding turns
18
+
19
+ 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.
20
+
21
+ GitHub review loops: the PR body carries `<!-- lua-run:<runId> -->`; the GitHub webhook routes `pull_request_review` to `Workflows.signal(runId, 'github.review', …)` — see the `ticket-to-pr` example.
22
+
23
+ ## Memory on the Job tier
24
+
25
+ > `jobResources:'large'` (4 CPU / 8 GiB) is the ceiling. A container that exceeds its class's memory limit is OOM-killed and the step fails `job_oom_killed` — that is your fault, not a platform fault: it is not retried unless you declare `retry`, and the error's `sizeClassHint` tells you whether a bigger class exists. Test suites are the usual culprit: jest spawns a worker per CPU and `mongodb-memory-server` boots a `mongod` per test file, so run `npm test -- --maxWorkers=2` (or set `maxWorkers` in `jest.config`) inside a Job step, keep `runInBand` for suites with in-memory databases, and split a monorepo suite across `parallel` worktree arms rather than sizing up.
26
+
27
+ ## Offline
28
+
29
+ The local driver emulates the tier: `--workspace <dir>`, `--job-wall <s>`, worktree arms as literal `git worktree add`, `--segment-wall` for segment rehearsal. `credentialsRef` is printed and never resolved offline.