lua-cli 3.31.0 → 3.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/api-exports.d.ts +416 -103
  2. package/dist/api-exports.js +1992 -299
  3. package/dist/api-exports.js.map +1 -1
  4. package/dist/index.js +5262 -1914
  5. package/dist/index.js.map +1 -1
  6. package/dist/voice/test/index.d.ts +54 -54
  7. package/dist/workflow-builder.d.ts +257 -44
  8. package/dist/workflow-builder.js +1382 -265
  9. package/dist/workflow-builder.js.map +1 -1
  10. package/docs/README.md +2 -2
  11. package/docs/api/LuaWorkflow.md +44 -28
  12. package/docs/api/Workflows.md +12 -1
  13. package/docs/workflows/approvals.md +14 -1
  14. package/docs/workflows/connections-in-coding-turns.md +1 -0
  15. package/docs/workflows/correlation-keys.md +1 -0
  16. package/docs/workflows/git-credentials.md +22 -1
  17. package/docs/workflows/goals.md +46 -0
  18. package/docs/workflows/limits.md +6 -0
  19. package/docs/workflows/recovery.md +6 -2
  20. package/docs/workflows/replay-local.md +10 -10
  21. package/docs/workflows/schedules.md +15 -0
  22. package/docs/workflows/script-form.md +20 -10
  23. package/docs/workflows/testing-offline.md +25 -20
  24. package/docs/workflows/workspaces-and-long-steps.md +38 -2
  25. package/package.json +2 -2
  26. package/template/examples/workflows/CLAUDE.md +16 -13
  27. package/template/examples/workflows/pr-review-round.ts +61 -20
  28. package/template/examples/workflows/provision-tenant.ts +25 -8
  29. package/template/examples/workflows/refund-approval.ts +30 -17
  30. package/template/examples/workflows/support-triage.ts +59 -22
  31. package/template/examples/workflows/ticket-to-pr.ts +125 -46
  32. package/template/examples/workflows/vendor-invoices.ts +69 -16
  33. package/template/package.json +1 -1
@@ -1,5 +1,8 @@
1
- // Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR -> review loop -> approval -> merge.
2
- // Verbatim from workflows-spec 03 §3.2 (f) (WF-215 / WF-223 — the spec is normative).
1
+ // Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR (coding turn) -> review loop -> approval -> the decision (the merge stays human).
2
+ // From workflows-spec 03 §3.2 (f) (WF-215 / WF-223 — the spec is normative). LUA-635: `openPr` is placed by `.then(openPr)` on
3
+ // both test outcomes — the listing's `otherwise: 'openPr'` named a `.then(createStep)` placement, which never declares an id
4
+ // (03 §3.2.0); a string arm names an agentStep / specialistStep / toolStep / map / workflow declaration. `mergeGate.title` is a
5
+ // plain string — the grammar is `title: string` (03 §3.1) and bindings ride `details`.
3
6
  // src/workflows/ticket-to-pr.ts — Linear label → clone → parallel worktree arms → merge → test suite → PR → review loop → approval → merge
4
7
  import { z } from 'zod';
5
8
  import { createStep, createWorkflow, step, fromInit, fromStep, template, eq, lit } from 'lua-cli';
@@ -7,59 +10,135 @@ import { prReviewRound } from './pr-review-round';
7
10
 
8
11
  const runTests = createStep({
9
12
  id: 'runTests',
10
- inputSchema: z.any(), outputSchema: z.object({ passed: z.boolean(), summary: z.string() }),
11
- tier: 'job', workspace: { mount: 'rw' }, timeoutSeconds: 3600, jobResources: 'large', // a 15–40 min monorepo suite fits one 4 h segment (D19-r1); up to 86 400 s is legal since D19-r2 — see §3.2 (h) for a step that crosses the segment boundary
12
- async execute({ workspace, log }) {
13
- const r = await $`cd ${workspace!.path} && npm ci && npm test -- --maxWorkers=2`.nothrow(); // untracked node_modules were rebuilt on restore (05 §5.17.5, E12); `--maxWorkers=2` keeps a mongodb-memory-server-per-file suite inside `large`'s 8 GiB — over it the container is OOM-killed and the step fails `job_oom_killed` (customer fault, 05 §5.17.4; §3.12 note)
13
+ inputSchema: z.any(),
14
+ outputSchema: z.object({ passed: z.boolean(), summary: z.string() }),
15
+ tier: 'job',
16
+ workspace: { mount: 'rw' },
17
+ timeoutSeconds: 3600,
18
+ jobResources: 'large', // a 15–40 min monorepo suite fits one 4 h segment (D19-r1); up to 86 400 s is legal since D19-r2 — see §3.2 (h) for a step that crosses the segment boundary
19
+ async execute({ $, log }) {
20
+ // `ctx.$` runs ONE allow-listed binary per call, argv only, in the checkout (LUA-682): no `cd`, no `&&` — the
21
+ // cwd is the workspace root and a second command is a second call. `.strict` throws on a non-zero exit; the
22
+ // plain form returns it, which is what a test run wants.
23
+ await $!.strict`npm ci`; // untracked node_modules were rebuilt on restore (05 §5.17.5, E12)
24
+ const r = await $!`npm test -- --maxWorkers=2`; // `--maxWorkers=2` keeps a mongodb-memory-server-per-file suite inside `large`'s 8 GiB — over it the container is OOM-killed and the step fails `job_oom_killed` (customer fault, 05 §5.17.4; §3.12 note)
14
25
  log(r.stdout.slice(-4000));
15
- return { passed: r.exitCode === 0, summary: r.stdout.slice(-2000) };
26
+ return { passed: r.code === 0, summary: r.stdout.slice(-2000) };
16
27
  },
17
28
  });
18
29
 
19
- const openPr = createStep({
20
- id: 'openPr',
21
- inputSchema: z.object({ ticketId: z.string(), title: z.string() }), outputSchema: z.object({ prNumber: z.number(), url: z.string() }),
22
- tier: 'job', workspace: { mount: 'ro' },
23
- sideEffects: 'external', onError: 'park', // a PR is an external effect: a platform fault PARKS (06 §6.3.5); `gh` is idempotent on the branch anyway
24
- async execute({ workspace, inputData, runId }) {
25
- const out = await $`cd ${workspace!.path} && gh pr create --fill --title ${inputData.title} --body ${`Closes ${inputData.ticketId}\n\n<!-- lua-run:${runId} -->`}`; // the run id in the PR body is what the GitHub webhook uses to route review signals (§3.7)
26
- const url = out.stdout.trim(); return { prNumber: Number(url.split('/').pop()), url };
27
- },
28
- });
30
+ // `openPr` is a CODING TURN (05 §5.17.6) — the pilot's real pattern: the harness's own `gh` tool talks to the sidecar's
31
+ // gh proxy. A Job-tier code step can run `gh` too (`ctx.$`, with `jobTools:['gh']` on the step), but the proxy admits
32
+ // `gh pr create` / `gh pr edit` / `gh pr comment` on the pinned repo only — no merge, no /graphql (T17-D-SE25) — and the
33
+ // end-to-end path is the sidecar's to prove; the coding turn is what ships PRs today.
29
34
 
30
- const mergePr = createStep({
31
- id: 'mergePr', inputSchema: z.object({ approved: z.boolean(), input: z.object({ prNumber: z.number() }) }), outputSchema: z.object({ merged: z.boolean() }),
32
- tier: 'job', workspace: { mount: 'ro' }, jobTools: ['gh'], sideEffects: 'external', onError: 'park', // `jobTools:['gh']` is what mints GH_TOKEN for a code step (11 §11.5.6: pull_requests:write + contents:write + issues:read, repo-scoped merge is a contents write on GitHub)
33
- async execute({ workspace, inputData }) {
34
- if (!inputData.approved) return { merged: false };
35
- await $`cd ${workspace!.path} && gh pr merge ${inputData.input.prNumber} --squash --delete-branch`; // succeeds only if the base branch's ruleset lets the App merge — human-only-merge orgs keep contents:write off the App (11 T21)
36
- return { merged: true };
35
+ const recordDecision = createStep({
36
+ id: 'recordDecision',
37
+ inputSchema: z.object({ approved: z.boolean(), prNumber: z.number() }), // projected by the `mergeInput` map below an approval's output carries `approved` (and `decidedBy`, `editRevision`…), never the payload it was shown (docs/workflows/approvals.md), so the PR number is read back from `openPr`
38
+ outputSchema: z.object({ approved: z.boolean(), prNumber: z.number(), merged: z.boolean() }),
39
+ async execute({ inputData, log }) {
40
+ // The merge itself stays with a person: `gh pr merge` is refused by the sidecar BY DESIGN (a contents write; human-only-merge
41
+ // orgs keep contents:write off the App, 11 T21), so the workflow ends at the decision — `merged` is always false here and
42
+ // the approver merges when ready.
43
+ log(
44
+ inputData.approved
45
+ ? `PR #${inputData.prNumber} approved — merge when ready`
46
+ : `PR #${inputData.prNumber} not approved`
47
+ );
48
+ return { approved: inputData.approved, prNumber: inputData.prNumber, merged: false };
37
49
  },
38
50
  });
39
51
 
40
52
  export const ticketToPr = createWorkflow({
41
53
  name: 'ticket-to-pr',
42
- description: 'Linear label → implement/tests/docs in parallel worktrees → merge → test suite → PR → review rounds → approval → merge',
43
- inputSchema: z.object({ ticketId: z.string(), title: z.string(), spec: z.string(), repo: z.string(), baseRef: z.string().default('main') }),
44
- outputSchema: z.object({ merged: z.boolean(), prUrl: z.string().optional(), rounds: z.number() }),
45
- workspace: { kind: 'git', repo: template('${initData.repo}'), ref: template('${initData.baseRef}'), credentialsRef: 'github-app', sizeGb: 20, verify: 'npm test' }, // `github-app` = the org's GitHub connection id (a template would use `connections[].key`, §13 §13.3.3)
46
- budget: { maxCredits: 400, maxDurationSeconds: 21 * 24 * 3600 }, // review rounds may wait a week each. Job seconds (wall, claim → terminal) worst case: 7200+5400+1800 arms + 3600 merge + 3600 tests + 3600 fixTests + 3600 openPr + 8 × (7200+600) rounds + 3600 merge ≈ 94 800 s — fits the org default maxJobSecondsPerRun 172 800 (48 h) with margin; the push-time `job-seconds-exceed-cap` BLOCKER (10 §10.7.6) refuses this file on an org that lowered the cap below the sum, and prints the R23 raise path. Declare `maxJobSeconds` explicitly only to narrow.
54
+ description:
55
+ 'Linear label implement/tests/docs in parallel worktrees merge → test suite → PR → review rounds → approval → decision (a human merges)',
56
+ inputSchema: z.object({
57
+ ticketId: z.string(),
58
+ title: z.string(),
59
+ spec: z.string(),
60
+ repo: z.string(),
61
+ baseRef: z.string().default('main'),
62
+ }),
63
+ outputSchema: z.object({ approved: z.boolean(), prNumber: z.number(), merged: z.boolean() }), // the decision; the merge is a human act (see `recordDecision`)
64
+ connections: [{ key: 'github', integrationType: 'github', required: true }], // the GitHub connection this workflow acts through, declared ONCE by key — resolved on the owner agent at run time (an agent-scoped GitHub connection first, then org-scoped; LUA-623), so the same definition runs on any agent: no frozen connection id in source, no per-agent build
65
+ workspace: {
66
+ kind: 'git',
67
+ repo: template('${initData.repo}'),
68
+ ref: template('${initData.baseRef}'),
69
+ credentialsRef: 'github',
70
+ sizeGb: 20,
71
+ verify: 'npm test',
72
+ }, // `credentialsRef` names the declared key above; an undeclared key fails `lua compile` / `lua push` with connection-key-undeclared
73
+ budget: { maxCredits: 400, maxDurationSeconds: 21 * 24 * 3600 }, // review rounds may wait a week each. Job seconds (wall, claim → terminal) worst case: 7200+5400+1800 arms + 3600 merge + 3600 tests + 3600 fixTests + 3600 openPr + 8 × (7200+600) rounds + 3600 merge ≈ 94 800 s — fits the org default maxJobSecondsPerRun 172 800 (48 h) with margin; the push-time `job-seconds-exceed-cap` BLOCKER (10 §10.7.6) refuses this file on an org that lowered the cap below the sum, and prints the R23 raise path. Declare `maxJobSeconds` explicitly only to narrow.
47
74
  })
48
- .parallel(['implement', 'writeTests', 'updateDocs'], { merge: { strategy: 'rebase', onConflict: 'agent' } }) // three coding turns on three clone volumes + branches; the merge row rebases them onto the run branch and lets one resolver turn fix conflicts (05 §5.17.5)
49
- .agentStep('implement', { agentId: 'swe-implementer', tier: 'job', workspace: { mount: 'rw', isolation: 'worktree' }, timeoutSeconds: 7200,
50
- prompt: template('Implement ${initData.spec} for ticket ${initData.ticketId}. Do not touch tests or docs; commit as you go.'),
51
- toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] } })
52
- .agentStep('writeTests', { agentId: 'swe-tester', tier: 'job', workspace: { mount: 'rw', isolation: 'worktree' }, timeoutSeconds: 5400,
53
- prompt: template('Write tests for the behaviour described in ${initData.spec}; they may fail until implementation lands. Only touch test files.') })
54
- .agentStep('updateDocs', { agentId: 'swe-writer', tier: 'job', workspace: { mount: 'rw', isolation: 'worktree' }, timeoutSeconds: 1800, jobResources: 'small',
55
- prompt: template('Update docs/ for ${initData.spec}. Only touch markdown files.') })
56
- .then(runTests) // shared mount, sequential — the merged run branch
57
- .switch([[eq(step('runTests').path('passed'), lit(false)), 'fixTests']], 'openPr')
58
- .agentStep('fixTests', { agentId: 'swe-implementer', tier: 'job', workspace: { mount: 'rw' }, timeoutSeconds: 3600,
59
- prompt: template('The test suite failed:\n${stepResults.runTests.summary}\nFix the code (not the tests unless they are wrong) and re-run `npm test` until green.') })
60
- .then(openPr)
61
- .dowhile('reviewRound', eq(step('reviewRound').path('state'), lit('changes_requested')), { maxIterations: 8 }) // ≤ 8 review rounds (the SWE_REVIEW_MAX_ROUNDS lesson)
62
- .workflow('reviewRound', prReviewRound, { prNumber: fromStep('openPr', 'prNumber'), repo: fromInit('repo') }, { workspace: 'inherit' }) // each round is a child run on the SAME workspace; the parent's PVC is released while a round waits for the webhook (E12)
63
- .approval('mergeGate', { title: template('Merge PR ${stepResults.openPr.url}?'), approver: { role: 'eng-lead' }, excludeInitiator: true, timeoutHours: 72, onTimeout: 'deny' })
64
- .then(mergePr)
75
+ .parallel(['implement', 'writeTests', 'updateDocs'], { merge: { strategy: 'rebase', onConflict: 'agent' } }) // three coding turns on three clone volumes + branches; the merge row rebases them onto the run branch and lets one resolver turn fix conflicts (05 §5.17.5)
76
+ .agentStep('implement', {
77
+ agentId: 'swe-implementer',
78
+ tier: 'job',
79
+ workspace: { mount: 'rw', isolation: 'worktree' },
80
+ timeoutSeconds: 7200,
81
+ prompt: template(
82
+ 'Implement ${initData.spec} for ticket ${initData.ticketId}. Do not touch tests or docs; commit as you go.'
83
+ ),
84
+ toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] },
85
+ })
86
+ .agentStep('writeTests', {
87
+ agentId: 'swe-tester',
88
+ tier: 'job',
89
+ workspace: { mount: 'rw', isolation: 'worktree' },
90
+ timeoutSeconds: 5400,
91
+ prompt: template(
92
+ 'Write tests for the behaviour described in ${initData.spec}; they may fail until implementation lands. Only touch test files.'
93
+ ),
94
+ })
95
+ .agentStep('updateDocs', {
96
+ agentId: 'swe-writer',
97
+ tier: 'job',
98
+ workspace: { mount: 'rw', isolation: 'worktree' },
99
+ timeoutSeconds: 1800,
100
+ jobResources: 'small',
101
+ prompt: template('Update docs/ for ${initData.spec}. Only touch markdown files.'),
102
+ })
103
+ .then(runTests) // shared mount, sequential — the merged run branch
104
+ .switch([[eq(step('runTests').path('passed'), lit(false)), 'fixTests']]) // red ⇒ one fix turn (placed inside this arm by the string ref below); green falls through — no `otherwise`, `openPr` follows either way
105
+ .agentStep('fixTests', {
106
+ agentId: 'swe-implementer',
107
+ tier: 'job',
108
+ workspace: { mount: 'rw' },
109
+ timeoutSeconds: 3600,
110
+ prompt: template(
111
+ 'The test suite failed:\n${stepResults.runTests.summary}\nFix the code (not the tests unless they are wrong) and re-run `npm test` until green.'
112
+ ),
113
+ })
114
+ .agentStep('openPr', {
115
+ // a CODING TURN with `gh` (05 §5.17.6): opens the PR from the run branch and reports it as typed output
116
+ agentId: 'swe-implementer',
117
+ tier: 'job',
118
+ workspace: { mount: 'ro' },
119
+ timeoutSeconds: 600,
120
+ jobResources: 'small',
121
+ prompt: template(
122
+ 'Open a pull request from the current branch against ${initData.baseRef} for ticket ${initData.ticketId}, titled "${initData.title}". Put the marker the harness gives you for this run in the PR body (the GitHub webhook routes review signals by it). Reply with the PR number and URL.'
123
+ ),
124
+ toolScope: { jobTools: ['gh', 'git', 'read', 'glob', 'grep'] }, // `gh` here is the harness's tool through the sidecar's gh proxy (create / edit / comment on this repo only)
125
+ outputSchema: z.object({ prNumber: z.number(), url: z.string() }),
126
+ })
127
+ .dowhile('reviewRound', eq(step('reviewRound').path('state'), lit('changes_requested')), { maxIterations: 8 }) // ≤ 8 review rounds (the SWE_REVIEW_MAX_ROUNDS lesson)
128
+ .workflow(
129
+ 'reviewRound',
130
+ prReviewRound,
131
+ { prNumber: fromStep('openPr', 'prNumber'), repo: fromInit('repo') },
132
+ { workspace: 'inherit' }
133
+ ) // declared here, placed as the loop body by the string ref above; each round is a child run on the SAME workspace; the parent's PVC is released while a round waits for the webhook (E12)
134
+ .approval('mergeGate', {
135
+ title: 'Merge the PR?',
136
+ details: template('Merge ${stepResults.openPr.url} into ${initData.baseRef}?'),
137
+ approver: { role: 'eng-lead' },
138
+ excludeInitiator: true,
139
+ timeoutHours: 72,
140
+ onTimeout: 'deny',
141
+ })
142
+ .map({ approved: fromStep('mergeGate', 'approved'), prNumber: fromStep('openPr', 'prNumber') }, { id: 'mergeInput' }) // a `.then(step)` receives the PREVIOUS node's output — the approval's, which has `approved` but not the PR number — so the fields `recordDecision` declares are projected explicitly; without this map the row fails `input_schema_invalid` before dispatch (LUA-679, found by the LUA-669 builder)
143
+ .then(recordDecision)
65
144
  .commit();
@@ -1,30 +1,83 @@
1
1
  // Per-item review with an env overlay, a restricted output pane and a slack reply (Cluster K).
2
- // Verbatim from workflows-spec 03 §3.2 (j) (WF-215 / WF-223 — the spec is normative).
2
+ // From workflows-spec 03 §3.2 (j) (WF-215 / WF-223 — the spec is normative). LUA-635: the approval `title` is a plain string
3
+ // (03 §3.1 `approval(id, { title: string; details?: TemplateBinding })`); the listing's `template(…)` title moved into `details`.
4
+ // The listing's `.foreach('pay', 'payInvoice', …)` is not the builder's `foreach(step, opts)` (03 §3.1) and declared no `payInvoice`
5
+ // anywhere — the body is the `payInvoice` code step below.
3
6
  import { z } from 'zod';
4
7
  import { createStep, createWorkflow, env, fromInit, fromKnowledge, fromStep, template } from 'lua-cli';
5
8
 
6
- const invoiceSchema = z.object({ vendorId: z.string(), approverEmail: z.string().email(), amount: z.number(), pdf: z.object({ __artefactRef: z.string() }) });
9
+ const invoiceSchema = z.object({
10
+ vendorId: z.string(),
11
+ approverEmail: z.string().email(),
12
+ amount: z.number(),
13
+ pdf: z.object({ __artefactRef: z.string() }),
14
+ });
7
15
 
8
- const loadInvoices = createStep({ id: 'loadInvoices', inputSchema: z.object({ batchId: z.string() }), outputSchema: z.object({ invoices: z.array(invoiceSchema) }),
16
+ const loadInvoices = createStep({
17
+ id: 'loadInvoices',
18
+ inputSchema: z.object({ batchId: z.string() }),
19
+ outputSchema: z.object({ invoices: z.array(invoiceSchema) }),
9
20
  async execute(ctx) {
10
- const src = await ctx.artefacts.get(ctx.inputData.batchId); // B40: a multi-GB NDJSON batch — page it, never buffer it
11
- const invoices = []; for (let offset = 0; ; offset += 1000) { const page = await src.rows({ offset, limit: 1000 }); invoices.push(...page.rows); if (page.nextOffset == null) break; }
21
+ const src = await ctx.artefacts.get(ctx.inputData.batchId); // B40: a multi-GB NDJSON batch — page it, never buffer it
22
+ const invoices = [];
23
+ for (let offset = 0; ; offset += 1000) {
24
+ const page = await src.rows({ offset, limit: 1000 });
25
+ invoices.push(...page.rows);
26
+ if (page.nextOffset == null) break;
27
+ }
12
28
  return { invoices };
13
- } });
29
+ },
30
+ });
31
+
32
+ const payInvoice = createStep({
33
+ id: 'payInvoice',
34
+ inputSchema: invoiceSchema,
35
+ outputSchema: z.object({ vendorId: z.string(), paid: z.boolean() }),
36
+ sideEffects: 'external',
37
+ onError: 'park', // a payment is an external effect: a platform-fault reclaim PARKS it, never auto-retries (06 §6.3.5)
38
+ async execute({ inputData: inv, once }) {
39
+ // one approved row per iteration — the foreach body receives the raw item
40
+ await once(`pay:${inv.vendorId}:${inv.amount}`, () =>
41
+ Payments.transfer({ vendorId: inv.vendorId, amount: inv.amount })
42
+ ); // exactly-once per {occurrenceId, key}; `Payments` stands for your payment rail
43
+ return { vendorId: inv.vendorId, paid: true };
44
+ },
45
+ });
14
46
 
15
47
  export const vendorInvoices = createWorkflow({
16
- name: 'vendor-invoices', inputSchema: z.object({ batchId: z.string() }),
17
- outputVisibility: { roles: ['finance', 'org-admin'] }, // B44: invoice payloads are readable by finance only; owner bypass on (default)
18
- schedule: { cron: '0 6 * * 1-5', timezone: env.template('FINANCE_TZ') }, // B33: one file, per-env timezone resolved at push
48
+ name: 'vendor-invoices',
49
+ inputSchema: z.object({ batchId: z.string() }),
50
+ outputVisibility: { roles: ['finance', 'org-admin'] }, // B44: invoice payloads are readable by finance only; owner bypass on (default)
51
+ schedule: { cron: '0 6 * * 1-5', timezone: env.template('FINANCE_TZ') }, // B33: one file, per-env timezone resolved at push
52
+ scheduleInput: { batchId: 'latest' }, // the 06:00 run reads the standing `latest` batch alias; a required input without a `scheduleInput` is `schedule-input-required`, a publish blocker. An ad-hoc run passes its own batch id
19
53
  })
20
54
  .then(loadInvoices, { batchId: fromInit('batchId') })
21
- .agentStep('policyCheck', { agentId: env.template('FINANCE_AGENT_ID'), // B33: staging and prod route to different sub-agents; same graphHash
55
+ .agentStep('policyCheck', {
56
+ agentId: env.template('FINANCE_AGENT_ID'), // B33: staging and prod route to different sub-agents; same graphHash
22
57
  prompt: template('Flag any invoice that breaks policy.\n${knowledge.policy}\n${stepResults.loadInvoices.invoices}'),
23
58
  outputSchema: z.object({ flagged: z.array(z.string()) }),
24
- input: { policy: fromKnowledge({ source: 'connection', connectionId: env.template('FINANCE_DRIVE_CONNECTION'), query: 'vendor payment policy', maxChars: 6000 }) }, // B41
25
- toolScope: { connectionIds: [] } }) // tainted by the connection knowledge ⇒ `toolScope` mandatory (P1-16); the knowledge connection is added by the compiler's overlay pass
26
- .approval('reviewInvoices', { title: template('${stepResults.loadInvoices.invoices.length} vendor invoices'), details: template('Flagged: ${stepResults.policyCheck.flagged}'),
27
- approver: { role: 'finance' }, itemsPath: 'invoices', itemApprover: { fromItem: 'approverEmail' }, // B20: every invoice goes to ITS approver; the parent link is the finance role's overview
28
- itemTimeout: { timeoutHours: 48 }, editable: true, editablePaths: ['invoices[*].amount'], timeoutHours: 96 })
29
- .foreach('pay', 'payInvoice', { items: fromStep('reviewInvoices', 'items'), concurrency: 4 }) // consumes `resumeData.items[]` — approved rows only
59
+ input: {
60
+ policy: fromKnowledge({
61
+ source: 'connection',
62
+ connectionId: env.template('FINANCE_DRIVE_CONNECTION'),
63
+ query: 'vendor payment policy',
64
+ maxChars: 6000,
65
+ }),
66
+ }, // B41
67
+ toolScope: { connectionIds: [] },
68
+ }) // tainted by the connection knowledge ⇒ `toolScope` mandatory (P1-16); the knowledge connection is added by the compiler's overlay pass
69
+ .approval('reviewInvoices', {
70
+ title: 'Vendor invoices to review',
71
+ details: template(
72
+ '${stepResults.loadInvoices.invoices.length} invoices; flagged: ${stepResults.policyCheck.flagged}'
73
+ ),
74
+ approver: { role: 'finance' },
75
+ itemsPath: 'invoices',
76
+ itemApprover: { fromItem: 'approverEmail' }, // B20: every invoice goes to ITS approver; the parent link is the finance role's overview
77
+ itemTimeout: { timeoutHours: 48 },
78
+ editable: true,
79
+ editablePaths: ['invoices[*].amount'],
80
+ timeoutHours: 96,
81
+ })
82
+ .foreach(payInvoice, { items: fromStep('reviewInvoices', 'items'), concurrency: 4 }) // `foreach(step, opts)`: consumes `resumeData.items[]` — approved rows only
30
83
  .commit();
@@ -20,7 +20,7 @@
20
20
  "inquirer": "^12.9.6",
21
21
  "stripe": "^17.5.0",
22
22
  "js-yaml": "^4.1.0",
23
- "lua-cli": "^3.31.0",
23
+ "lua-cli": "^3.32.2",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"