lua-cli 3.30.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-exports.d.ts +1020 -41
- package/dist/api-exports.js +4984 -137
- package/dist/api-exports.js.map +1 -1
- package/dist/index.js +25539 -14391
- package/dist/index.js.map +1 -1
- package/dist/voice/test/index.d.ts +4 -4
- package/dist/workflow-builder.d.ts +766 -0
- package/dist/workflow-builder.js +5732 -0
- package/dist/workflow-builder.js.map +1 -0
- package/docs/API_INDEX.md +2 -0
- package/docs/README.md +27 -9
- package/docs/api/Jobs.md +10 -10
- package/docs/api/LuaWorkflow.md +73 -0
- package/docs/api/Workflows.md +110 -0
- package/docs/workflows/approvals.md +28 -0
- package/docs/workflows/artefacts-and-datasets.md +19 -0
- package/docs/workflows/coding-harness.md +12 -0
- package/docs/workflows/compliance-gates.md +16 -0
- package/docs/workflows/connections-in-coding-turns.md +9 -0
- package/docs/workflows/correlation-keys.md +11 -0
- package/docs/workflows/env-overlays.md +12 -0
- package/docs/workflows/evidence-bundles.md +11 -0
- package/docs/workflows/exports.md +5 -0
- package/docs/workflows/external-content-and-toolscope.md +11 -0
- package/docs/workflows/git-credentials.md +11 -0
- package/docs/workflows/knowledge-bindings.md +13 -0
- package/docs/workflows/limits.md +11 -0
- package/docs/workflows/long-steps-and-checkpoints.md +13 -0
- package/docs/workflows/migrating-cloud-tasks.md +9 -0
- package/docs/workflows/migrating-runs.md +13 -0
- package/docs/workflows/output-visibility.md +9 -0
- package/docs/workflows/per-item-approvals.md +9 -0
- package/docs/workflows/private-network-sources.md +12 -0
- package/docs/workflows/recovery.md +28 -0
- package/docs/workflows/replay-local.md +35 -0
- package/docs/workflows/reply-channels.md +11 -0
- package/docs/workflows/retention-and-archival.md +82 -0
- package/docs/workflows/roles.md +12 -0
- package/docs/workflows/schedules.md +11 -0
- package/docs/workflows/script-form.md +50 -0
- package/docs/workflows/testing-offline.md +46 -0
- package/docs/workflows/workspace-backends.md +11 -0
- package/docs/workflows/workspaces-and-long-steps.md +27 -0
- package/package.json +8 -3
- package/scripts/run-api-extractor.mjs +1 -1
- package/template/.gitignore +2 -0
- package/template/examples/workflows/CLAUDE.md +24 -0
- package/template/examples/workflows/adversarial-verify.workflow.script.js +48 -0
- package/template/examples/workflows/github-review.webhook.ts +19 -0
- package/template/examples/workflows/linear-ready.trigger.ts +21 -0
- package/template/examples/workflows/outreach.ts +55 -0
- package/template/examples/workflows/pr-review-round.ts +38 -0
- package/template/examples/workflows/provision-tenant.ts +18 -0
- package/template/examples/workflows/refund-approval.ts +44 -0
- package/template/examples/workflows/research-brief.ts +42 -0
- package/template/examples/workflows/reviewed-brief.ts +19 -0
- package/template/examples/workflows/support-triage.ts +44 -0
- package/template/examples/workflows/ticket-to-pr.ts +65 -0
- package/template/examples/workflows/vendor-invoices.ts +30 -0
- package/template/lua.skill.yaml +1 -0
- package/template/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// foreach + approval + an exactly-once send (`ctx.once`).
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (b) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createStep, createWorkflow, fromInit, fromStep, template, AI, Channels } from 'lua-cli';
|
|
5
|
+
|
|
6
|
+
const lead = z.object({ email: z.string().email(), name: z.string() });
|
|
7
|
+
const draft = z.object({ to: z.string(), body: z.string() });
|
|
8
|
+
|
|
9
|
+
const draftEmail = createStep({
|
|
10
|
+
id: 'draftEmail',
|
|
11
|
+
inputSchema: lead, // foreach passes the RAW item — `{ email, name }`, not `{ lead: {…} }` (§3.2.1 check 4)
|
|
12
|
+
outputSchema: draft,
|
|
13
|
+
async execute({ inputData }) {
|
|
14
|
+
// string overload → Promise<string> (`api-exports.ts:768`); the object overload returns `AiGenerateOutput{ text, … }` (`:786`, shared-types `ai-generate.types.ts:78-86`)
|
|
15
|
+
const text = await AI.generate(`Write a 3-line intro email to ${inputData.name}`);
|
|
16
|
+
return { to: inputData.email, body: text };
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const sendEmails = createStep({
|
|
20
|
+
id: 'sendEmails',
|
|
21
|
+
inputSchema: z.object({ drafts: z.array(draft) }),
|
|
22
|
+
outputSchema: z.object({ sent: z.number() }),
|
|
23
|
+
sideEffects: 'external', // never auto-retried on platform-fault reclaim
|
|
24
|
+
onError: 'park', // an EFFECT_IN_DOUBT parks the step for R37 instead of failing the run
|
|
25
|
+
async execute({ inputData, once }) {
|
|
26
|
+
let sent = 0;
|
|
27
|
+
for (const d of inputData.drafts) {
|
|
28
|
+
// exactly-once per {occurrenceId, key}: a retry, resume, repair run or migrated run that reaches this line again gets the stored
|
|
29
|
+
// result back and never re-sends (I27 claim/settle). No hand-rolled Data check-then-act — that pattern was non-atomic.
|
|
30
|
+
const r = await once(d.to, () => Channels.email.send({ to: { email: d.to }, subject: 'Hello', body: d.body })); // `EmailSendInput.to` is `{ userId?, email? }`
|
|
31
|
+
// The whole `Channels` facade is callable in-step — incl. `Channels.whatsapp.send({ threadId: ctx.runtime.replyTo!.threadId, text })` on a customer-channel run (07 §7.0-L / §7.4.5): a step MAY answer the customer
|
|
32
|
+
// itself (e.g. the refund outcome right after the Stripe step, before a QA step that must not delay it); `once()` covers it exactly like email, and the platform's terminal reply then lands as a
|
|
33
|
+
// duplicate-safe fallback (same threadId; the customer sees two lines only if the step's text differs). Outside WhatsApp's session the in-step send needs a template too — `Channels.whatsapp.sendTemplate` (closing pass 2026-08-27).
|
|
34
|
+
if (r) sent++;
|
|
35
|
+
}
|
|
36
|
+
return { sent };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const outreach = createWorkflow({
|
|
41
|
+
name: 'outreach',
|
|
42
|
+
inputSchema: z.object({ leads: z.array(lead) }),
|
|
43
|
+
outputSchema: z.object({ sent: z.number() }),
|
|
44
|
+
schedule: { type: 'cron', expression: '0 9 * * 1', timezone: 'Europe/London' },
|
|
45
|
+
scheduleInput: { leads: [] },
|
|
46
|
+
concurrencyPolicy: 'forbid',
|
|
47
|
+
})
|
|
48
|
+
.map({ leads: fromInit('leads') }, { id: 'leads' }) // entry 1 — three maps in this workflow ⇒ every id explicit (`map-id-required`)
|
|
49
|
+
.map({ '': fromStep('leads', 'leads') }, { id: 'items' }) // entry 2 — '' key = "output IS this value" (Lua extension, §3.4): foreach needs a raw array upstream
|
|
50
|
+
.foreach(draftEmail, { concurrency: 8, maxItems: 500 }) // entry 3 — items are `lead`s, output is `draft[]`
|
|
51
|
+
.map({ drafts: fromStep('draftEmail') }, { id: 'drafts' }) // entry 4 — `{ drafts: draft[] }`, the approval's editable payload
|
|
52
|
+
.approval('reviewDrafts', { title: 'Approve outreach batch', details: template('${stepResults.drafts.drafts.length} drafts ready'),
|
|
53
|
+
approver: 'org-admins', timeoutHours: 48, onTimeout: 'cancel-run', editable: true, editablePaths: ['drafts', 'drafts[*].body'] }) // entry 5
|
|
54
|
+
.then(sendEmails) // entry 6 — reads `stepResults.drafts` (the approval passes its edited payload through)
|
|
55
|
+
.commit();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// One review iteration; runs as a child of ticket-to-pr with the SAME workspace.
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (f) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
// src/workflows/pr-review-round.ts — one review iteration; runs as a child of ticket-to-pr with the SAME workspace
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { createStep, createWorkflow, step, eq, lit, template } from 'lua-cli';
|
|
6
|
+
|
|
7
|
+
const pushFix = createStep({
|
|
8
|
+
id: 'pushFix',
|
|
9
|
+
inputSchema: z.any(), outputSchema: z.object({ headSha: z.string() }),
|
|
10
|
+
tier: 'job', workspace: { mount: 'rw' }, timeoutSeconds: 600,
|
|
11
|
+
async execute({ workspace }) { // git runs through the credential proxy — no token in this container (05 §5.17.3)
|
|
12
|
+
const sha = (await $`git -C ${workspace!.path} rev-parse HEAD`).stdout.trim(); // `$` = the Job image's shell helper (`@lua/coding-harness/shell`); a plain child_process spawn works too — the Job site has no REQUIRE_BLOCKLIST for `child_process`
|
|
13
|
+
return { headSha: sha };
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const prReviewRound = createWorkflow({
|
|
18
|
+
name: 'pr-review-round',
|
|
19
|
+
inputSchema: z.object({ prNumber: z.number(), repo: z.string() }),
|
|
20
|
+
outputSchema: z.object({ state: z.enum(['approved', 'changes_requested', 'timed_out']), round: z.number().optional() }),
|
|
21
|
+
})
|
|
22
|
+
.waitForSignal('review', { signal: 'github.review', timeoutHours: 168, // delivered by the GitHub webhook (§3.7) — to the PARENT run; the engine descends it into this child (06 §6.5.3 step 2b)
|
|
23
|
+
schema: z.object({ state: z.enum(['approved', 'changes_requested', 'commented']), comments: z.array(z.object({ path: z.string().optional(), body: z.string() })) }),
|
|
24
|
+
acceptedSources: ['webhook'], onTimeout: 'continue' })
|
|
25
|
+
.switch([[eq(step('review').path('payload.state'), lit('changes_requested')), 'addressReview']], 'done')
|
|
26
|
+
.agentStep('addressReview', { // a CODING TURN: Claude Code headless with git/gh/shell/edit on the mounted checkout (05 §5.17.6)
|
|
27
|
+
agentId: 'swe-implementer', tier: 'job', workspace: { mount: 'rw' }, timeoutSeconds: 7200, jobResources: 'medium',
|
|
28
|
+
prompt: template('Address every review comment in ${stepResults.review.payload.comments} on the current branch. Run the relevant tests. Commit with a message that references the comment you addressed. Do not force-push.'),
|
|
29
|
+
toolScope: { jobTools: ['shell', 'read', 'write', 'edit', 'glob', 'grep', 'git'] }, // no `gh`: the push + PR update is the next code step
|
|
30
|
+
outputSchema: z.object({ summary: z.string() }),
|
|
31
|
+
})
|
|
32
|
+
.then(pushFix)
|
|
33
|
+
.then(createStep({ id: 'done', inputSchema: z.any(), outputSchema: z.object({ state: z.enum(['approved', 'changes_requested', 'timed_out']) }),
|
|
34
|
+
async execute({ getStepResult }) {
|
|
35
|
+
const r = getStepResult<{ received: boolean; timedOut?: boolean; payload?: { state: string } }>('review');
|
|
36
|
+
return { state: r?.received ? (r.payload!.state === 'changes_requested' ? 'changes_requested' : 'approved') : 'timed_out' };
|
|
37
|
+
} }))
|
|
38
|
+
.commit();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Nested workflow + sleep + signal.
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (c) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createStep, createWorkflow, step, eq, lit, fromInit } from 'lua-cli';
|
|
5
|
+
|
|
6
|
+
export const provision = createWorkflow({
|
|
7
|
+
name: 'provision-tenant',
|
|
8
|
+
inputSchema: z.object({ tenantId: z.string() }),
|
|
9
|
+
outputSchema: z.object({ ready: z.boolean() }),
|
|
10
|
+
})
|
|
11
|
+
.workflow('createResources', 'research-brief', { topic: fromInit('tenantId') }) // nested by name (same agent) or LuaWorkflow ref
|
|
12
|
+
.sleep(5 * 60 * 1000) // engine-side; holds no compute
|
|
13
|
+
.waitForSignal('vendorReady', { signal: 'vendor.ready', schema: z.object({ ok: z.boolean() }),
|
|
14
|
+
timeoutHours: 72, acceptedSources: ['webhook', 'api'] })
|
|
15
|
+
.switch([[eq(step('vendorReady').path('ok'), lit(true)), 'finalize']], 'rollback')
|
|
16
|
+
.then(createStep({ id: 'finalize', inputSchema: z.any(), outputSchema: z.object({ ready: z.boolean() }), execute: async () => ({ ready: true }) }))
|
|
17
|
+
.then(createStep({ id: 'rollback', inputSchema: z.any(), outputSchema: z.object({ ready: z.boolean() }), execute: async () => ({ ready: false }) }))
|
|
18
|
+
.commit();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Maker-checker refund with escalation, business-hours deadlines and a recoverable external step.
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (e) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createStep, createWorkflow, template, Integrations } from 'lua-cli';
|
|
5
|
+
|
|
6
|
+
const postRefund = createStep({
|
|
7
|
+
id: 'postRefund',
|
|
8
|
+
inputSchema: z.object({ approved: z.boolean(), editedPayload: z.object({ ticketId: z.string(), amount: z.number() }).optional(),
|
|
9
|
+
input: z.object({ ticketId: z.string(), amount: z.number() }) }), // the approval node's output shape (04 §4.2.3)
|
|
10
|
+
outputSchema: z.object({ refundId: z.string().nullable() }),
|
|
11
|
+
sideEffects: 'external', // a platform-fault reclaim PARKS this step (never auto-retried); the run gates `exception` (§06 §6.3.5)
|
|
12
|
+
onError: 'park', // a final customer-fault failure parks too — the operator retries / skips / supplies the refundId / fails it
|
|
13
|
+
requiredConnections: ['stripe'], // unmountable ⇒ failed{credentials_revoked, reason:'required_connection_unmountable'} → parked via onError:'park' (§11 §11.5.5)
|
|
14
|
+
async execute({ inputData, occurrenceId }) {
|
|
15
|
+
if (!inputData.approved) return { refundId: null };
|
|
16
|
+
const r = inputData.editedPayload ?? inputData.input;
|
|
17
|
+
// `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
|
+
// 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', { method: 'POST', path: '/v1/refunds',
|
|
20
|
+
headers: { 'Idempotency-Key': `refund:${r.ticketId}` }, body: { charge: r.ticketId, amount: r.amount } });
|
|
21
|
+
return { refundId: res.body.id };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const refund = createWorkflow({
|
|
26
|
+
name: 'refund-approval',
|
|
27
|
+
inputSchema: z.object({ ticketId: z.string(), amount: z.number(), requesterId: z.string() }),
|
|
28
|
+
outputSchema: z.object({ refundId: z.string().nullable() }),
|
|
29
|
+
budget: { maxDurationSeconds: 14 * 24 * 3600 }, // long enough for two business-hours escalation hops (the compiler warns `deadline-clamped` otherwise)
|
|
30
|
+
})
|
|
31
|
+
.approval('approveRefund', {
|
|
32
|
+
title: 'Refund request', details: template('Refund ${initData.amount} for ticket ${initData.ticketId}'),
|
|
33
|
+
approver: { role: 'support-lead' }, // org role, template-portable; `{users:[…]}` / `{group:'finance'}` are org data (§13 §13.3.3)
|
|
34
|
+
excludeInitiator: true, // the requester who started the run can never approve it (maker-checker)
|
|
35
|
+
fourEyes: { edit: { role: 'support-lead' }, approve: { role: 'finance-controller' } }, // whoever edits the amount cannot be the one who approves it
|
|
36
|
+
timeoutHours: 8, businessHours: { tz: 'Europe/London', calendar: 'mon-fri' }, // 8 business hours, then…
|
|
37
|
+
onTimeout: [{ escalateTo: { role: 'finance-controller' }, timeoutHours: 16 }, // …hop 1: finance, 16 business hours, then…
|
|
38
|
+
{ escalateTo: 'org-admins', timeoutHours: 24 }, // …hop 2: org admins, then…
|
|
39
|
+
'deny'], // …the node completes {approved:false, timedOut:true, escalations:2} — branchable data (§06 §6.4.11)
|
|
40
|
+
editable: true, editablePaths: ['amount'], // the approver may lower the amount; the approve call echoes the fingerprint of the revision it saw (§06 §6.4.9)
|
|
41
|
+
editedPayloadSchema: z.object({ ticketId: z.string(), amount: z.number().positive().max(500) }),
|
|
42
|
+
})
|
|
43
|
+
.then(postRefund)
|
|
44
|
+
.commit();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Sequential + parallel research, typed end to end.
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (a) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createStep, createWorkflow, stepOf, fromStep, template, gt, lit } from 'lua-cli';
|
|
5
|
+
|
|
6
|
+
const fetchSources = createStep({
|
|
7
|
+
id: 'fetchSources',
|
|
8
|
+
inputSchema: z.object({ topic: z.string() }),
|
|
9
|
+
outputSchema: z.object({ urls: z.array(z.string().url()) }),
|
|
10
|
+
timeoutSeconds: 60,
|
|
11
|
+
async execute({ inputData, log }) {
|
|
12
|
+
const res = await fetch(`https://api.example.com/search?q=${encodeURIComponent(inputData.topic)}`);
|
|
13
|
+
if (!res.ok) throw new Error(`search failed: ${res.status}`);
|
|
14
|
+
const json = (await res.json()) as { url: string }[];
|
|
15
|
+
log(`found ${json.length} sources`);
|
|
16
|
+
return { urls: json.slice(0, 10).map((r) => r.url) };
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const angle = z.object({ summary: z.string(), confidence: z.number() });
|
|
21
|
+
|
|
22
|
+
const wf = createWorkflow({
|
|
23
|
+
name: 'research-brief',
|
|
24
|
+
description: 'Fetch sources, summarise from two angles in parallel, merge — or fall back when confidence is low.',
|
|
25
|
+
inputSchema: z.object({ topic: z.string() }),
|
|
26
|
+
outputSchema: z.object({ brief: z.string() }),
|
|
27
|
+
budget: { maxCredits: 40 },
|
|
28
|
+
})
|
|
29
|
+
.then(fetchSources) // entry 1: step
|
|
30
|
+
.parallel(['techAngle', 'marketAngle']) // entry 2: parallel — 2 arms, both declared BELOW (§3.2.0: "declare here, inside me")
|
|
31
|
+
.agentStep('techAngle', { agentId: 'analyst', prompt: template('Summarise the technical angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
|
|
32
|
+
outputSchema: angle }) // no top-level entry — placed by the parallel above
|
|
33
|
+
.agentStep('marketAngle', { agentId: 'analyst', prompt: template('Summarise the market angle of ${initData.topic} using ${stepResults.fetchSources.urls}'),
|
|
34
|
+
outputSchema: angle }) // same
|
|
35
|
+
.switch( // entry 3: conditional{exclusive}
|
|
36
|
+
[[gt(stepOf<typeof angle>('techAngle').path('confidence'), lit(0.6)), 'merge']], // TypedRef<number> vs Literal<number> — `lit('a')` would not compile
|
|
37
|
+
'lowConfidence')
|
|
38
|
+
.map({ brief: fromStep('techAngle', 'summary'), market: fromStep('marketAngle', 'summary') }, { id: 'merge' }) // placed INSIDE the switch arm (string ref above) — one entry, not two
|
|
39
|
+
.agentStep('lowConfidence', { agentId: 'analyst', // placed as the switch's `otherwise`
|
|
40
|
+
prompt: template('Confidence was low. Write a cautious brief on ${initData.topic} from ${stepResults.techAngle.summary} and ${stepResults.marketAngle.summary}'),
|
|
41
|
+
outputSchema: z.object({ brief: z.string() }) })
|
|
42
|
+
.commit(); // the conditional is the last entry: the taken arm's output IS the run output
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ephemeral reviewer on the owning agent (D25).
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (d) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createWorkflow, template } from 'lua-cli';
|
|
5
|
+
|
|
6
|
+
export const reviewedBrief = createWorkflow({
|
|
7
|
+
name: 'reviewed-brief',
|
|
8
|
+
inputSchema: z.object({ company: z.string() }),
|
|
9
|
+
outputSchema: z.object({ findings: z.array(z.string()), verdict: z.enum(['pass', 'revise']) }),
|
|
10
|
+
})
|
|
11
|
+
.agentStep('draft', { agentId: 'web-researcher', prompt: template('Draft a one-page brief on ${initData.company}.') })
|
|
12
|
+
.specialistStep('review', {
|
|
13
|
+
role: { name: 'Reviewer',
|
|
14
|
+
instructions: 'You are a sceptical reviewer. Check every claim in the draft against a source you can cite; flag anything unsupported. Never rewrite the draft — return findings only.',
|
|
15
|
+
tools: ['searchWeb', 'fetchUrl'] }, // ⊆ the owning agent's toolset; delegation tools (`agent-*`) are never allowlistable (04 §4.2.7)
|
|
16
|
+
prompt: template('Review the draft: ${stepResults.draft.text}'),
|
|
17
|
+
outputSchema: z.object({ findings: z.array(z.string()), verdict: z.enum(['pass', 'revise']) }),
|
|
18
|
+
})
|
|
19
|
+
.commit();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Support triage from a customer channel — knowledge grounding, toolScope, dataset ref, ctx.artefacts.
|
|
2
|
+
// Verbatim from workflows-spec 03 §3.2 (g) (WF-215 / WF-223 — the spec is normative).
|
|
3
|
+
import { createWorkflow, createStep, fromInit, fromKnowledge, rows, template, stepOf, gt, lit } from 'lua-cli';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const pullHistory = createStep({ // array-typed output ⇒ oversize (> 8 MB) becomes a `{__datasetRef}` (NDJSON on CDN), never OUTPUT_TOO_LARGE
|
|
7
|
+
id: 'pullHistory', inputSchema: z.object({ customerId: z.string() }),
|
|
8
|
+
outputSchema: z.object({ orders: z.array(z.object({ id: z.string(), total: z.number(), status: z.string() })) }),
|
|
9
|
+
execute: async (ctx) => {
|
|
10
|
+
const orders = await Orders.list({ customerId: ctx.inputData.customerId, limit: 50_000 });
|
|
11
|
+
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, { // P1-8: journaled in script form; ≤ 50 per step
|
|
13
|
+
contentType: 'text/csv', kind: 'dataset', datasetSchema: { type: 'object', properties: { id: { type: 'string' }, total: { type: 'number' }, status: { type: 'string' } } } });
|
|
14
|
+
ctx.log(`orders.csv → ${artefactId}`);
|
|
15
|
+
return { orders };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const classifyOut = z.object({ intent: z.enum(['refund', 'status', 'other']), confidence: z.number(), orderId: z.string().optional() });
|
|
20
|
+
|
|
21
|
+
export default createWorkflow({
|
|
22
|
+
name: 'support-triage', inputSchema: z.object({ ticketId: z.string(), customerId: z.string(), message: z.string() }),
|
|
23
|
+
})
|
|
24
|
+
.then(pullHistory)
|
|
25
|
+
.map({
|
|
26
|
+
refund: fromKnowledge({ source: 'org-docs', query: 'refund policy', maxChars: 4000, topK: 3 }), // rendered at S2 with a provenance header per chunk
|
|
27
|
+
recent: rows('pullHistory', 'orders', { offset: 0, limit: 20 }), // paged rows; the bare ref would be the {__datasetRef} object
|
|
28
|
+
}, { id: 'classifyInputs' })
|
|
29
|
+
.agentStep('classify', {
|
|
30
|
+
agentId: 'support-agent',
|
|
31
|
+
// `${initData.message}` is customer-channel content and `refund` is a {knowledge} binding: this step is EXTERNAL-CONTENT — `toolScope` is mandatory.
|
|
32
|
+
prompt: template('Ticket ${initData.ticketId}: ${initData.message}\n\nRefund policy:\n${stepResults.classifyInputs.refund}\n\nRecent orders (first page):\n${stepResults.classifyInputs.recent}'),
|
|
33
|
+
toolScope: {}, // `{}` = no tools; `defaultToolScopeMode:'deny'` would imply this when absent
|
|
34
|
+
outputSchema: classifyOut,
|
|
35
|
+
})
|
|
36
|
+
.approval('refundGate', { title: template('Refund ${stepResults.classify.orderId}?'), approver: 'org-admins', // NOT 'creator': `approver-is-run-principal` on a customer-channel workflow
|
|
37
|
+
details: template('${stepResults.classify.intent} @ ${stepResults.classify.confidence}'), timeoutHours: 24,
|
|
38
|
+
onTimeout: [{ escalateTo: 'org-admins', timeoutHours: 24 }, 'deny'] }) // approvers re-resolved at suspend AND decision (§06 §6.11); the card previews `orders.csv` (name/size/rowCount + an R32 link)
|
|
39
|
+
.conditional([
|
|
40
|
+
{ when: gt(stepOf<typeof classifyOut>('classify').path('confidence'), lit(0.8)), branch: 'refundGate' },
|
|
41
|
+
{ when: lit(true), branch: 'handoff' },
|
|
42
|
+
])
|
|
43
|
+
.agentStep('handoff', { agentId: 'support-agent', prompt: template('Summarise ${initData.message} for a human agent.'), toolScope: {} })
|
|
44
|
+
.commit();
|
|
@@ -0,0 +1,65 @@
|
|
|
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).
|
|
3
|
+
// src/workflows/ticket-to-pr.ts — Linear label → clone → parallel worktree arms → merge → test suite → PR → review loop → approval → merge
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { createStep, createWorkflow, step, fromInit, fromStep, template, eq, lit } from 'lua-cli';
|
|
6
|
+
import { prReviewRound } from './pr-review-round';
|
|
7
|
+
|
|
8
|
+
const runTests = createStep({
|
|
9
|
+
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)
|
|
14
|
+
log(r.stdout.slice(-4000));
|
|
15
|
+
return { passed: r.exitCode === 0, summary: r.stdout.slice(-2000) };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
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
|
+
});
|
|
29
|
+
|
|
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 };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const ticketToPr = createWorkflow({
|
|
41
|
+
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.
|
|
47
|
+
})
|
|
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)
|
|
65
|
+
.commit();
|
|
@@ -0,0 +1,30 @@
|
|
|
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).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createStep, createWorkflow, env, fromInit, fromKnowledge, fromStep, template } from 'lua-cli';
|
|
5
|
+
|
|
6
|
+
const invoiceSchema = z.object({ vendorId: z.string(), approverEmail: z.string().email(), amount: z.number(), pdf: z.object({ __artefactRef: z.string() }) });
|
|
7
|
+
|
|
8
|
+
const loadInvoices = createStep({ id: 'loadInvoices', inputSchema: z.object({ batchId: z.string() }), outputSchema: z.object({ invoices: z.array(invoiceSchema) }),
|
|
9
|
+
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; }
|
|
12
|
+
return { invoices };
|
|
13
|
+
} });
|
|
14
|
+
|
|
15
|
+
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
|
|
19
|
+
})
|
|
20
|
+
.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
|
|
22
|
+
prompt: template('Flag any invoice that breaks policy.\n${knowledge.policy}\n${stepResults.loadInvoices.invoices}'),
|
|
23
|
+
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
|
|
30
|
+
.commit();
|
package/template/lua.skill.yaml
CHANGED