lua-cli 3.31.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.
- package/dist/api-exports.d.ts +81 -5
- package/dist/api-exports.js +686 -126
- package/dist/api-exports.js.map +1 -1
- package/dist/index.js +2003 -308
- package/dist/index.js.map +1 -1
- package/dist/workflow-builder.d.ts +37 -3
- package/dist/workflow-builder.js +658 -117
- package/dist/workflow-builder.js.map +1 -1
- package/docs/README.md +2 -2
- package/docs/api/LuaWorkflow.md +44 -28
- package/docs/api/Workflows.md +2 -1
- package/docs/workflows/approvals.md +14 -1
- package/docs/workflows/connections-in-coding-turns.md +1 -0
- package/docs/workflows/git-credentials.md +22 -1
- package/docs/workflows/goals.md +46 -0
- package/docs/workflows/recovery.md +6 -2
- package/docs/workflows/replay-local.md +10 -10
- package/docs/workflows/schedules.md +15 -0
- package/docs/workflows/testing-offline.md +22 -19
- package/docs/workflows/workspaces-and-long-steps.md +2 -0
- package/package.json +3 -3
- package/template/examples/workflows/CLAUDE.md +16 -13
- package/template/examples/workflows/pr-review-round.ts +57 -20
- package/template/examples/workflows/provision-tenant.ts +25 -8
- package/template/examples/workflows/refund-approval.ts +30 -17
- package/template/examples/workflows/support-triage.ts +59 -22
- package/template/examples/workflows/ticket-to-pr.ts +103 -31
- package/template/examples/workflows/vendor-invoices.ts +69 -16
- package/template/package.json +1 -1
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
// Nested workflow + sleep + signal.
|
|
2
|
-
//
|
|
2
|
+
// From workflows-spec 03 §3.2 (c) (WF-215 / WF-223 — the spec is normative). LUA-635: the switch arms are the
|
|
3
|
+
// createStep objects themselves — a STRING arm names an agentStep / specialistStep / toolStep / map / workflow
|
|
4
|
+
// declaration elsewhere in the chain (03 §3.2.0); `.then(createStep({ id }))` places a step, it never declares one.
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import { createStep, createWorkflow, step, eq, lit, fromInit } from 'lua-cli';
|
|
5
7
|
|
|
8
|
+
const finalize = createStep({
|
|
9
|
+
id: 'finalize',
|
|
10
|
+
inputSchema: z.any(),
|
|
11
|
+
outputSchema: z.object({ ready: z.boolean() }),
|
|
12
|
+
execute: async () => ({ ready: true }),
|
|
13
|
+
});
|
|
14
|
+
const rollback = createStep({
|
|
15
|
+
id: 'rollback',
|
|
16
|
+
inputSchema: z.any(),
|
|
17
|
+
outputSchema: z.object({ ready: z.boolean() }),
|
|
18
|
+
execute: async () => ({ ready: false }),
|
|
19
|
+
});
|
|
20
|
+
|
|
6
21
|
export const provision = createWorkflow({
|
|
7
22
|
name: 'provision-tenant',
|
|
8
23
|
inputSchema: z.object({ tenantId: z.string() }),
|
|
9
24
|
outputSchema: z.object({ ready: z.boolean() }),
|
|
10
25
|
})
|
|
11
|
-
.workflow('createResources', 'research-brief', { topic: fromInit('tenantId') })
|
|
12
|
-
.sleep(5 * 60 * 1000)
|
|
13
|
-
.waitForSignal('vendorReady', {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
.workflow('createResources', 'research-brief', { topic: fromInit('tenantId') }) // nested by name (same agent) or LuaWorkflow ref
|
|
27
|
+
.sleep(5 * 60 * 1000) // engine-side; holds no compute
|
|
28
|
+
.waitForSignal('vendorReady', {
|
|
29
|
+
signal: 'vendor.ready',
|
|
30
|
+
schema: z.object({ ok: z.boolean() }),
|
|
31
|
+
timeoutHours: 72,
|
|
32
|
+
acceptedSources: ['webhook', 'api'],
|
|
33
|
+
})
|
|
34
|
+
.switch([[eq(step('vendorReady').path('ok'), lit(true)), finalize]], rollback) // inline code steps; the last entry is a conditional, so the taken arm's output is the run output
|
|
18
35
|
.commit();
|
|
@@ -5,19 +5,26 @@ import { createStep, createWorkflow, template, Integrations } from 'lua-cli';
|
|
|
5
5
|
|
|
6
6
|
const postRefund = createStep({
|
|
7
7
|
id: 'postRefund',
|
|
8
|
-
inputSchema: z.object({
|
|
9
|
-
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
approved: z.boolean(),
|
|
10
|
+
editedPayload: z.object({ ticketId: z.string(), amount: z.number() }).optional(),
|
|
11
|
+
input: z.object({ ticketId: z.string(), amount: z.number() }),
|
|
12
|
+
}), // the approval node's output shape (04 §4.2.3)
|
|
10
13
|
outputSchema: z.object({ refundId: z.string().nullable() }),
|
|
11
|
-
sideEffects: 'external',
|
|
12
|
-
onError: 'park',
|
|
13
|
-
requiredConnections: ['stripe'],
|
|
14
|
+
sideEffects: 'external', // a platform-fault reclaim PARKS this step (never auto-retried); the run gates `exception` (§06 §6.3.5)
|
|
15
|
+
onError: 'park', // a final customer-fault failure parks too — the operator retries / skips / supplies the refundId / fails it
|
|
16
|
+
requiredConnections: ['stripe'], // the declared key below — resolved on the owner agent at run time (LUA-623); no connection of that type ⇒ failed{credentials_unresolved}, unmountable ⇒ failed{credentials_revoked, reason:'required_connection_unmountable'} → parked via onError:'park' (§11 §11.5.5)
|
|
14
17
|
async execute({ inputData, occurrenceId }) {
|
|
15
18
|
if (!inputData.approved) return { refundId: null };
|
|
16
19
|
const r = inputData.editedPayload ?? inputData.input;
|
|
17
20
|
// `occurrenceId` is `${lineageId}:${stepId}`: the same key on retry-step, on resume and on a REPAIR RUN — the refund collapses to one (§06 §6.3.3 (e)).
|
|
18
21
|
// A key that must hold across INDEPENDENT runs (two runs for the same ticket) is the caller's: `refund:${r.ticketId}` (§06 §6.3.3 (f)).
|
|
19
|
-
const res = await Integrations.passthrough('stripe', {
|
|
20
|
-
|
|
22
|
+
const res = await Integrations.passthrough('stripe', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
path: '/v1/refunds',
|
|
25
|
+
headers: { 'Idempotency-Key': `refund:${r.ticketId}` },
|
|
26
|
+
body: { charge: r.ticketId, amount: r.amount },
|
|
27
|
+
});
|
|
21
28
|
return { refundId: res.body.id };
|
|
22
29
|
},
|
|
23
30
|
});
|
|
@@ -26,18 +33,24 @@ export const refund = createWorkflow({
|
|
|
26
33
|
name: 'refund-approval',
|
|
27
34
|
inputSchema: z.object({ ticketId: z.string(), amount: z.number(), requesterId: z.string() }),
|
|
28
35
|
outputSchema: z.object({ refundId: z.string().nullable() }),
|
|
29
|
-
budget: { maxDurationSeconds: 14 * 24 * 3600 },
|
|
36
|
+
budget: { maxDurationSeconds: 14 * 24 * 3600 }, // long enough for two business-hours escalation hops (the compiler warns `deadline-clamped` otherwise)
|
|
37
|
+
connections: [{ key: 'stripe', integrationType: 'stripe', required: true }], // the Stripe connection the refund step acts through, declared once by key — never a frozen connection id (LUA-623)
|
|
30
38
|
})
|
|
31
39
|
.approval('approveRefund', {
|
|
32
|
-
title: 'Refund request',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
title: 'Refund request',
|
|
41
|
+
details: template('Refund ${initData.amount} for ticket ${initData.ticketId}'),
|
|
42
|
+
approver: { role: 'support-lead' }, // org role, template-portable; `{users:[…]}` / `{group:'finance'}` are org data (§13 §13.3.3)
|
|
43
|
+
excludeInitiator: true, // the requester who started the run can never approve it (maker-checker)
|
|
44
|
+
fourEyes: { edit: { role: 'support-lead' }, approve: { role: 'finance-controller' } }, // whoever edits the amount cannot be the one who approves it
|
|
45
|
+
timeoutHours: 8,
|
|
46
|
+
businessHours: { tz: 'Europe/London', calendar: 'mon-fri' }, // 8 business hours, then…
|
|
47
|
+
onTimeout: [
|
|
48
|
+
{ escalateTo: { role: 'finance-controller' }, timeoutHours: 16 }, // …hop 1: finance, 16 business hours, then…
|
|
49
|
+
{ escalateTo: 'org-admins', timeoutHours: 24 }, // …hop 2: org admins, then…
|
|
50
|
+
'deny',
|
|
51
|
+
], // …the node completes {approved:false, timedOut:true, escalations:2} — branchable data (§06 §6.4.11)
|
|
52
|
+
editable: true,
|
|
53
|
+
editablePaths: ['amount'], // the approver may lower the amount; the approve call echoes the fingerprint of the revision it saw (§06 §6.4.9)
|
|
41
54
|
editedPayloadSchema: z.object({ ticketId: z.string(), amount: z.number().positive().max(500) }),
|
|
42
55
|
})
|
|
43
56
|
.then(postRefund)
|
|
@@ -1,44 +1,81 @@
|
|
|
1
1
|
// Support triage from a customer channel — knowledge grounding, toolScope, dataset ref, ctx.artefacts.
|
|
2
|
-
//
|
|
3
|
-
|
|
2
|
+
// From workflows-spec 03 §3.2 (g) (WF-215 / WF-223 — the spec is normative). LUA-635: the listing's `.conditional([{ when, branch }])`
|
|
3
|
+
// is not in the builder (03 §3.1 has `switch` / `branch` over `[predicate, StepRef]` arms) and it named an approval as an arm, which
|
|
4
|
+
// v1 forbids (approvals are top-level only) — the gate is a `switch` whose confident arm is (e)'s `refund-approval` as a child run.
|
|
5
|
+
// (The listing's `template(…)` approval title was the other drift: the grammar is `title: string`, bindings ride `details`.)
|
|
6
|
+
import {
|
|
7
|
+
createWorkflow,
|
|
8
|
+
createStep,
|
|
9
|
+
fromInit,
|
|
10
|
+
fromStep,
|
|
11
|
+
fromKnowledge,
|
|
12
|
+
rows,
|
|
13
|
+
template,
|
|
14
|
+
stepOf,
|
|
15
|
+
gt,
|
|
16
|
+
lit,
|
|
17
|
+
} from 'lua-cli';
|
|
4
18
|
import { z } from 'zod';
|
|
5
19
|
|
|
6
|
-
const pullHistory = createStep({
|
|
7
|
-
|
|
20
|
+
const pullHistory = createStep({
|
|
21
|
+
// array-typed output ⇒ oversize (> 8 MB) becomes a `{__datasetRef}` (NDJSON on CDN), never OUTPUT_TOO_LARGE
|
|
22
|
+
id: 'pullHistory',
|
|
23
|
+
inputSchema: z.object({ customerId: z.string() }),
|
|
8
24
|
outputSchema: z.object({ orders: z.array(z.object({ id: z.string(), total: z.number(), status: z.string() })) }),
|
|
9
25
|
execute: async (ctx) => {
|
|
10
26
|
const orders = await Orders.list({ customerId: ctx.inputData.customerId, limit: 50_000 });
|
|
11
27
|
const csv = ['id,total,status', ...orders.map((o) => `${o.id},${o.total},${o.status}`)].join('\n');
|
|
12
|
-
const { artefactId } = await ctx.artefacts.put('orders.csv', csv, {
|
|
13
|
-
|
|
28
|
+
const { artefactId } = await ctx.artefacts.put('orders.csv', csv, {
|
|
29
|
+
// P1-8: journaled in script form; ≤ 50 per step
|
|
30
|
+
contentType: 'text/csv',
|
|
31
|
+
kind: 'dataset',
|
|
32
|
+
datasetSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: { id: { type: 'string' }, total: { type: 'number' }, status: { type: 'string' } },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
14
37
|
ctx.log(`orders.csv → ${artefactId}`);
|
|
15
38
|
return { orders };
|
|
16
39
|
},
|
|
17
40
|
});
|
|
18
41
|
|
|
19
|
-
const classifyOut = z.object({
|
|
42
|
+
const classifyOut = z.object({
|
|
43
|
+
intent: z.enum(['refund', 'status', 'other']),
|
|
44
|
+
confidence: z.number(),
|
|
45
|
+
orderId: z.string().optional(),
|
|
46
|
+
amount: z.number().optional(),
|
|
47
|
+
}); // `amount`: the order total a confident refund carries into (e)
|
|
20
48
|
|
|
21
49
|
export default createWorkflow({
|
|
22
|
-
name: 'support-triage',
|
|
50
|
+
name: 'support-triage',
|
|
51
|
+
inputSchema: z.object({ ticketId: z.string(), customerId: z.string(), message: z.string() }),
|
|
23
52
|
})
|
|
24
53
|
.then(pullHistory)
|
|
25
|
-
.map(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
.map(
|
|
55
|
+
{
|
|
56
|
+
refund: fromKnowledge({ source: 'org-docs', query: 'refund policy', maxChars: 4000, topK: 3 }), // rendered at S2 with a provenance header per chunk
|
|
57
|
+
recent: rows('pullHistory', 'orders', { offset: 0, limit: 20 }), // paged rows; the bare ref would be the {__datasetRef} object
|
|
58
|
+
},
|
|
59
|
+
{ id: 'classifyInputs' }
|
|
60
|
+
)
|
|
29
61
|
.agentStep('classify', {
|
|
30
62
|
agentId: 'support-agent',
|
|
31
63
|
// `${initData.message}` is customer-channel content and `refund` is a {knowledge} binding: this step is EXTERNAL-CONTENT — `toolScope` is mandatory.
|
|
32
|
-
prompt: template(
|
|
33
|
-
|
|
64
|
+
prompt: template(
|
|
65
|
+
'Ticket ${initData.ticketId}: ${initData.message}\n\nRefund policy:\n${stepResults.classifyInputs.refund}\n\nRecent orders (first page):\n${stepResults.classifyInputs.recent}'
|
|
66
|
+
),
|
|
67
|
+
toolScope: {}, // `{}` = no tools; `defaultToolScopeMode:'deny'` would imply this when absent
|
|
34
68
|
outputSchema: classifyOut,
|
|
35
69
|
})
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
.switch([[gt(stepOf<typeof classifyOut>('classify').path('confidence'), lit(0.8)), 'refundGate']], 'handoff') // a confident refund goes to the gate, anything else to a human. An approval is TOP-LEVEL only (03 §3.1), so the gated one is a child run whose approval sits at its own top level — the same shape as a wait inside a loop (03 §3.2 (f))
|
|
71
|
+
.workflow('refundGate', 'refund-approval', {
|
|
72
|
+
ticketId: fromInit('ticketId'),
|
|
73
|
+
amount: fromStep('classify', 'amount'),
|
|
74
|
+
requesterId: fromInit('customerId'),
|
|
75
|
+
}) // (e)'s maker-checker run, nested by name on the same agent; declared here, placed inside the switch arm by the string ref above. Its approver is a role, NOT 'creator': `approver-is-run-principal` on a customer-channel workflow
|
|
76
|
+
.agentStep('handoff', {
|
|
77
|
+
agentId: 'support-agent',
|
|
78
|
+
prompt: template('Summarise ${initData.message} for a human agent.'),
|
|
79
|
+
toolScope: {},
|
|
80
|
+
}) // the switch's `otherwise`
|
|
44
81
|
.commit();
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR -> review loop -> approval -> merge.
|
|
2
|
-
//
|
|
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,10 +10,14 @@ import { prReviewRound } from './pr-review-round';
|
|
|
7
10
|
|
|
8
11
|
const runTests = createStep({
|
|
9
12
|
id: 'runTests',
|
|
10
|
-
inputSchema: z.any(),
|
|
11
|
-
|
|
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
|
|
12
19
|
async execute({ workspace, log }) {
|
|
13
|
-
const r = await $`cd ${workspace!.path} && npm ci && npm test -- --maxWorkers=2`.nothrow();
|
|
20
|
+
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)
|
|
14
21
|
log(r.stdout.slice(-4000));
|
|
15
22
|
return { passed: r.exitCode === 0, summary: r.stdout.slice(-2000) };
|
|
16
23
|
},
|
|
@@ -18,48 +25,113 @@ const runTests = createStep({
|
|
|
18
25
|
|
|
19
26
|
const openPr = createStep({
|
|
20
27
|
id: 'openPr',
|
|
21
|
-
inputSchema: z.object({ ticketId: z.string(), title: z.string() }),
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
inputSchema: z.object({ ticketId: z.string(), title: z.string() }),
|
|
29
|
+
outputSchema: z.object({ prNumber: z.number(), url: z.string() }),
|
|
30
|
+
tier: 'job',
|
|
31
|
+
workspace: { mount: 'ro' },
|
|
32
|
+
sideEffects: 'external',
|
|
33
|
+
onError: 'park', // a PR is an external effect: a platform fault PARKS (06 §6.3.5); `gh` is idempotent on the branch anyway
|
|
24
34
|
async execute({ workspace, inputData, runId }) {
|
|
25
|
-
const out =
|
|
26
|
-
|
|
35
|
+
const out =
|
|
36
|
+
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)
|
|
37
|
+
const url = out.stdout.trim();
|
|
38
|
+
return { prNumber: Number(url.split('/').pop()), url };
|
|
27
39
|
},
|
|
28
40
|
});
|
|
29
41
|
|
|
30
42
|
const mergePr = createStep({
|
|
31
|
-
id: 'mergePr',
|
|
32
|
-
|
|
43
|
+
id: 'mergePr',
|
|
44
|
+
inputSchema: z.object({ approved: z.boolean(), input: z.object({ prNumber: z.number() }) }),
|
|
45
|
+
outputSchema: z.object({ merged: z.boolean() }),
|
|
46
|
+
tier: 'job',
|
|
47
|
+
workspace: { mount: 'ro' },
|
|
48
|
+
jobTools: ['gh'],
|
|
49
|
+
sideEffects: 'external',
|
|
50
|
+
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
51
|
async execute({ workspace, inputData }) {
|
|
34
52
|
if (!inputData.approved) return { merged: false };
|
|
35
|
-
await $`cd ${workspace!.path} && gh pr merge ${inputData.input.prNumber} --squash --delete-branch`;
|
|
53
|
+
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
54
|
return { merged: true };
|
|
37
55
|
},
|
|
38
56
|
});
|
|
39
57
|
|
|
40
58
|
export const ticketToPr = createWorkflow({
|
|
41
59
|
name: 'ticket-to-pr',
|
|
42
|
-
description:
|
|
43
|
-
|
|
60
|
+
description:
|
|
61
|
+
'Linear label → implement/tests/docs in parallel worktrees → merge → test suite → PR → review rounds → approval → merge',
|
|
62
|
+
inputSchema: z.object({
|
|
63
|
+
ticketId: z.string(),
|
|
64
|
+
title: z.string(),
|
|
65
|
+
spec: z.string(),
|
|
66
|
+
repo: z.string(),
|
|
67
|
+
baseRef: z.string().default('main'),
|
|
68
|
+
}),
|
|
44
69
|
outputSchema: z.object({ merged: z.boolean(), prUrl: z.string().optional(), rounds: z.number() }),
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
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
|
|
71
|
+
workspace: {
|
|
72
|
+
kind: 'git',
|
|
73
|
+
repo: template('${initData.repo}'),
|
|
74
|
+
ref: template('${initData.baseRef}'),
|
|
75
|
+
credentialsRef: 'github',
|
|
76
|
+
sizeGb: 20,
|
|
77
|
+
verify: 'npm test',
|
|
78
|
+
}, // `credentialsRef` names the declared key above; an undeclared key fails `lua compile` / `lua push` with connection-key-undeclared
|
|
79
|
+
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
80
|
})
|
|
48
|
-
.parallel(['implement', 'writeTests', 'updateDocs'], { merge: { strategy: 'rebase', onConflict: 'agent' } })
|
|
49
|
-
.agentStep('implement', {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
.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)
|
|
82
|
+
.agentStep('implement', {
|
|
83
|
+
agentId: 'swe-implementer',
|
|
84
|
+
tier: 'job',
|
|
85
|
+
workspace: { mount: 'rw', isolation: 'worktree' },
|
|
86
|
+
timeoutSeconds: 7200,
|
|
87
|
+
prompt: template(
|
|
88
|
+
'Implement ${initData.spec} for ticket ${initData.ticketId}. Do not touch tests or docs; commit as you go.'
|
|
89
|
+
),
|
|
90
|
+
toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] },
|
|
91
|
+
})
|
|
92
|
+
.agentStep('writeTests', {
|
|
93
|
+
agentId: 'swe-tester',
|
|
94
|
+
tier: 'job',
|
|
95
|
+
workspace: { mount: 'rw', isolation: 'worktree' },
|
|
96
|
+
timeoutSeconds: 5400,
|
|
97
|
+
prompt: template(
|
|
98
|
+
'Write tests for the behaviour described in ${initData.spec}; they may fail until implementation lands. Only touch test files.'
|
|
99
|
+
),
|
|
100
|
+
})
|
|
101
|
+
.agentStep('updateDocs', {
|
|
102
|
+
agentId: 'swe-writer',
|
|
103
|
+
tier: 'job',
|
|
104
|
+
workspace: { mount: 'rw', isolation: 'worktree' },
|
|
105
|
+
timeoutSeconds: 1800,
|
|
106
|
+
jobResources: 'small',
|
|
107
|
+
prompt: template('Update docs/ for ${initData.spec}. Only touch markdown files.'),
|
|
108
|
+
})
|
|
109
|
+
.then(runTests) // shared mount, sequential — the merged run branch
|
|
110
|
+
.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
|
|
111
|
+
.agentStep('fixTests', {
|
|
112
|
+
agentId: 'swe-implementer',
|
|
113
|
+
tier: 'job',
|
|
114
|
+
workspace: { mount: 'rw' },
|
|
115
|
+
timeoutSeconds: 3600,
|
|
116
|
+
prompt: template(
|
|
117
|
+
'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.'
|
|
118
|
+
),
|
|
119
|
+
})
|
|
60
120
|
.then(openPr)
|
|
61
|
-
.dowhile('reviewRound', eq(step('reviewRound').path('state'), lit('changes_requested')), { maxIterations: 8 })
|
|
62
|
-
.workflow(
|
|
63
|
-
|
|
121
|
+
.dowhile('reviewRound', eq(step('reviewRound').path('state'), lit('changes_requested')), { maxIterations: 8 }) // ≤ 8 review rounds (the SWE_REVIEW_MAX_ROUNDS lesson)
|
|
122
|
+
.workflow(
|
|
123
|
+
'reviewRound',
|
|
124
|
+
prReviewRound,
|
|
125
|
+
{ prNumber: fromStep('openPr', 'prNumber'), repo: fromInit('repo') },
|
|
126
|
+
{ workspace: 'inherit' }
|
|
127
|
+
) // 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)
|
|
128
|
+
.approval('mergeGate', {
|
|
129
|
+
title: 'Merge the PR?',
|
|
130
|
+
details: template('Merge ${stepResults.openPr.url} into ${initData.baseRef}?'),
|
|
131
|
+
approver: { role: 'eng-lead' },
|
|
132
|
+
excludeInitiator: true,
|
|
133
|
+
timeoutHours: 72,
|
|
134
|
+
onTimeout: 'deny',
|
|
135
|
+
})
|
|
64
136
|
.then(mergePr)
|
|
65
137
|
.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
|
-
//
|
|
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({
|
|
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({
|
|
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);
|
|
11
|
-
const invoices = [];
|
|
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',
|
|
17
|
-
|
|
18
|
-
|
|
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', {
|
|
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: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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();
|