lua-cli 3.32.1 → 3.32.3
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 +414 -103
- package/dist/api-exports.js +2216 -622
- package/dist/api-exports.js.map +1 -1
- package/dist/index.js +5374 -2188
- package/dist/index.js.map +1 -1
- package/dist/voice/test/index.d.ts +54 -54
- package/dist/workflow-builder.d.ts +228 -45
- package/dist/workflow-builder.js +1945 -1025
- package/dist/workflow-builder.js.map +1 -1
- package/docs/CLI_REFERENCE.md +126 -4
- package/docs/README.md +2 -2
- package/docs/api/LuaWorkflow.md +16 -16
- package/docs/api/Workflows.md +10 -0
- package/docs/workflows/approvals.md +24 -6
- package/docs/workflows/correlation-keys.md +1 -0
- package/docs/workflows/goals.md +2 -2
- package/docs/workflows/limits.md +6 -0
- package/docs/workflows/replay-local.md +9 -3
- package/docs/workflows/schedules.md +1 -1
- package/docs/workflows/script-form.md +22 -12
- package/docs/workflows/testing-offline.md +35 -21
- package/docs/workflows/workspaces-and-long-steps.md +36 -2
- package/package.json +5 -4
- package/template/examples/workflows/CLAUDE.md +17 -11
- package/template/examples/workflows/adversarial-verify.workflow.script.js +20 -16
- package/template/examples/workflows/outreach.ts +31 -15
- package/template/examples/workflows/pr-review-round.ts +7 -3
- package/template/examples/workflows/refund-approval.ts +26 -12
- package/template/examples/workflows/ticket-to-pr.ts +43 -36
- package/template/package.json +1 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Example (i) — adversarial verify: spawn finders until two consecutive rounds add nothing
|
|
2
2
|
// new, then have an independent verifier confirm each finding. Script form (04 §4.3);
|
|
3
3
|
// push with `lua push workflow`, run offline with `lua test workflow adversarial-verify`.
|
|
4
|
+
// LUA-751: `parallel()` takes THUNKS (04 §4.3.2 — `parallel(angles.map((angle) => () => agent(…)))`); an array of
|
|
5
|
+
// promises resolved to `null`s. And a `step()` closure is journaled once and never re-runs on replay, so it must not
|
|
6
|
+
// mutate module state: it RETURNS the fresh findings and the merge into `found` happens outside it — otherwise the
|
|
7
|
+
// replay tick saw an empty `found`, issued nothing to verify and diverged (`JOURNAL_DIVERGENCE(entry_before_issue)`).
|
|
4
8
|
export const meta = {
|
|
5
9
|
name: 'adversarial-verify',
|
|
6
10
|
description: 'Find candidate issues from several angles, verify each independently, report the confirmed set',
|
|
@@ -13,29 +17,29 @@ const found = new Map();
|
|
|
13
17
|
let quietRounds = 0;
|
|
14
18
|
for (let round = 1; round <= 5 && quietRounds < 2; round++) {
|
|
15
19
|
const batch = await parallel(
|
|
16
|
-
args.angles.map(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
args.angles.map(
|
|
21
|
+
(angle) => () =>
|
|
22
|
+
agent(`Round ${round}: list concrete issues in ${args.subject} from the ${angle} angle. One per line.`, {
|
|
23
|
+
phase: 'Find',
|
|
24
|
+
label: `find-${angle}-${round}`,
|
|
25
|
+
})
|
|
21
26
|
)
|
|
22
27
|
);
|
|
23
|
-
// step(): a pure reduction journaled once — its closure may use anything, it never re-runs on replay
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
// step(): a pure reduction journaled once — its closure may use anything, it never re-runs on replay, so it
|
|
29
|
+
// returns what it found and touches no state of its own; the merge below is plain script code that replays.
|
|
30
|
+
const fresh = await step(`dedupe-${round}`, () => {
|
|
31
|
+
const out = [];
|
|
26
32
|
for (const text of batch) {
|
|
27
|
-
for (const line of String(text).split('\n')) {
|
|
33
|
+
for (const line of String(text ?? '').split('\n')) {
|
|
28
34
|
const key = line.trim().toLowerCase();
|
|
29
|
-
if (key && !found.has(key)) {
|
|
30
|
-
found.set(key, line.trim());
|
|
31
|
-
fresh++;
|
|
32
|
-
}
|
|
35
|
+
if (key && !found.has(key) && !out.some((f) => f.key === key)) out.push({ key, line: line.trim() });
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
|
-
return
|
|
38
|
+
return out;
|
|
36
39
|
});
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
for (const f of fresh) found.set(f.key, f.line);
|
|
41
|
+
quietRounds = fresh.length === 0 ? quietRounds + 1 : 0;
|
|
42
|
+
log(`round ${round}: ${fresh.length} new finding(s)`);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
const verdicts = await foreach(
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// foreach + approval + an exactly-once send (`ctx.once`).
|
|
2
|
-
//
|
|
2
|
+
// From workflows-spec 03 §3.2 (b) (WF-215 / WF-223 — the spec is normative). LUA-751: `sendEmails` reads the approval node's
|
|
3
|
+
// REAL output — `WorkflowApprovalOutput` (@lua/shared-types), the ONE shape the engine emits and `lua workflows run --approve`
|
|
4
|
+
// emits offline: the approver's edited `{ drafts }` rides under `editedPayload`; otherwise the drafts are read back from the
|
|
5
|
+
// `drafts` map the approval was shown (`getStepResult`) — the output never echoes the original payload.
|
|
3
6
|
import { z } from 'zod';
|
|
4
7
|
import { createStep, createWorkflow, fromInit, fromStep, template, AI, Channels } from 'lua-cli';
|
|
5
8
|
|
|
@@ -8,7 +11,7 @@ const draft = z.object({ to: z.string(), body: z.string() });
|
|
|
8
11
|
|
|
9
12
|
const draftEmail = createStep({
|
|
10
13
|
id: 'draftEmail',
|
|
11
|
-
inputSchema: lead,
|
|
14
|
+
inputSchema: lead, // foreach passes the RAW item — `{ email, name }`, not `{ lead: {…} }` (§3.2.1 check 4)
|
|
12
15
|
outputSchema: draft,
|
|
13
16
|
async execute({ inputData }) {
|
|
14
17
|
// string overload → Promise<string> (`api-exports.ts:768`); the object overload returns `AiGenerateOutput{ text, … }` (`:786`, shared-types `ai-generate.types.ts:78-86`)
|
|
@@ -18,16 +21,22 @@ const draftEmail = createStep({
|
|
|
18
21
|
});
|
|
19
22
|
const sendEmails = createStep({
|
|
20
23
|
id: 'sendEmails',
|
|
21
|
-
|
|
24
|
+
// The approval node's output (`WorkflowApprovalOutput`): what this step reads, `.passthrough()` for the rest of the envelope
|
|
25
|
+
// (`decision`, `text`, `note`, `editRevision`, `decidedBy`, `timedOut`, `escalations`).
|
|
26
|
+
inputSchema: z
|
|
27
|
+
.object({ approved: z.boolean(), editedPayload: z.object({ drafts: z.array(draft) }).optional() })
|
|
28
|
+
.passthrough(),
|
|
22
29
|
outputSchema: z.object({ sent: z.number() }),
|
|
23
|
-
sideEffects: 'external',
|
|
24
|
-
onError: 'park',
|
|
25
|
-
async execute({ inputData, once }) {
|
|
30
|
+
sideEffects: 'external', // never auto-retried on platform-fault reclaim
|
|
31
|
+
onError: 'park', // an EFFECT_IN_DOUBT parks the step for R37 instead of failing the run
|
|
32
|
+
async execute({ inputData, getStepResult, once }) {
|
|
33
|
+
if (!inputData.approved) return { sent: 0 }; // denied — data, not an exception (§06 §6.4.11); a timeout cancels the run before this step (`onTimeout:'cancel-run'`)
|
|
34
|
+
const { drafts } = inputData.editedPayload ?? getStepResult<{ drafts: z.infer<typeof draft>[] }>('drafts'); // the approver's edits, else the batch the approval showed
|
|
26
35
|
let sent = 0;
|
|
27
|
-
for (const d of
|
|
36
|
+
for (const d of drafts) {
|
|
28
37
|
// exactly-once per {occurrenceId, key}: a retry, resume, repair run or migrated run that reaches this line again gets the stored
|
|
29
38
|
// 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 }));
|
|
39
|
+
const r = await once(d.to, () => Channels.email.send({ to: { email: d.to }, subject: 'Hello', body: d.body })); // `EmailSendInput.to` is `{ userId?, email? }`
|
|
31
40
|
// 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
41
|
// 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
42
|
// 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).
|
|
@@ -45,11 +54,18 @@ export const outreach = createWorkflow({
|
|
|
45
54
|
scheduleInput: { leads: [] },
|
|
46
55
|
concurrencyPolicy: 'forbid',
|
|
47
56
|
})
|
|
48
|
-
.map({ leads: fromInit('leads') }, { id: 'leads' })
|
|
49
|
-
.map({ '': fromStep('leads', 'leads') }, { id: 'items' })
|
|
50
|
-
.foreach(draftEmail, { concurrency: 8, maxItems: 500 })
|
|
51
|
-
.map({ drafts: fromStep('draftEmail') }, { id: 'drafts' })
|
|
52
|
-
.approval('reviewDrafts', {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
.map({ leads: fromInit('leads') }, { id: 'leads' }) // entry 1 — three maps in this workflow ⇒ every id explicit (`map-id-required`)
|
|
58
|
+
.map({ '': fromStep('leads', 'leads') }, { id: 'items' }) // entry 2 — '' key = "output IS this value" (Lua extension, §3.4): foreach needs a raw array upstream
|
|
59
|
+
.foreach(draftEmail, { concurrency: 8, maxItems: 500 }) // entry 3 — items are `lead`s, output is `draft[]`
|
|
60
|
+
.map({ drafts: fromStep('draftEmail') }, { id: 'drafts' }) // entry 4 — `{ drafts: draft[] }`, the approval's editable payload
|
|
61
|
+
.approval('reviewDrafts', {
|
|
62
|
+
title: 'Approve outreach batch',
|
|
63
|
+
details: template('${stepResults.drafts.drafts.length} drafts ready'),
|
|
64
|
+
approver: 'org-admins',
|
|
65
|
+
timeoutHours: 48,
|
|
66
|
+
onTimeout: 'cancel-run',
|
|
67
|
+
editable: true,
|
|
68
|
+
editablePaths: ['drafts', 'drafts[*].body'],
|
|
69
|
+
}) // entry 5
|
|
70
|
+
.then(sendEmails) // entry 6 — its input IS the approval's output; the drafts come from `editedPayload` or `getStepResult('drafts')`
|
|
55
71
|
.commit();
|
|
@@ -13,9 +13,13 @@ const pushFix = createStep({
|
|
|
13
13
|
tier: 'job',
|
|
14
14
|
workspace: { mount: 'rw' },
|
|
15
15
|
timeoutSeconds: 600,
|
|
16
|
-
async execute({ workspace }) {
|
|
17
|
-
//
|
|
18
|
-
|
|
16
|
+
async execute({ $, workspace, log }) {
|
|
17
|
+
// `ctx.$` (LUA-682) runs an allow-listed binary in the checkout — argv only, no shell — with git's remote on the
|
|
18
|
+
// credential proxy: no token in this container (05 §5.17.3). `child_process` is NOT available to a code step
|
|
19
|
+
// (`lua compile` refuses it: node-capability-unavailable); a coding turn's `shell` tool is the other way to run
|
|
20
|
+
// commands. The harness pushes the branch at the checkpoint / terminal; this step reports what it pushed.
|
|
21
|
+
const sha = (await $!.strict`git rev-parse HEAD`).stdout.trim();
|
|
22
|
+
log(`pushed ${sha} on ${workspace!.branch ?? 'the run branch'}`);
|
|
19
23
|
return { headSha: sha };
|
|
20
24
|
},
|
|
21
25
|
});
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
// Maker-checker refund with escalation, business-hours deadlines and a recoverable external step.
|
|
2
|
-
//
|
|
2
|
+
// From workflows-spec 03 §3.2 (e) (WF-215 / WF-223 — the spec is normative). LUA-751: the step after the approval reads the
|
|
3
|
+
// approval node's REAL output — `WorkflowApprovalOutput` (@lua/shared-types): `{ approved, decision, text, note?, editedPayload?,
|
|
4
|
+
// editRevision?, decidedBy?, timedOut?, escalations? }`, the ONE shape the engine emits and `lua workflows run --approve` emits
|
|
5
|
+
// offline. The listing's `input: <preceding output>` pass-through (04 §4.2.3) was never implemented — the original payload is not
|
|
6
|
+
// echoed back; the step reads the request back with `getStepResult('refundRequest')` (docs/workflows/approvals.md). The `refundRequest`
|
|
7
|
+
// map ahead of the approval makes the payload the approver sees and edits exactly what `editedPayloadSchema` describes
|
|
8
|
+
// (`{ ticketId, amount }`, not the whole run input): an edit is judged on the pointers that changed against that payload.
|
|
3
9
|
import { z } from 'zod';
|
|
4
|
-
import { createStep, createWorkflow, template, Integrations } from 'lua-cli';
|
|
10
|
+
import { createStep, createWorkflow, fromInit, template, Integrations } from 'lua-cli';
|
|
11
|
+
|
|
12
|
+
const refundRequest = z.object({ ticketId: z.string(), amount: z.number() });
|
|
5
13
|
|
|
6
14
|
const postRefund = createStep({
|
|
7
15
|
id: 'postRefund',
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
// The approval node's output (`WorkflowApprovalOutput`): the members this step reads; `.passthrough()` admits the rest of the
|
|
17
|
+
// envelope (`decision`, `text`, `note`, `editRevision`, `decidedBy`, `timedOut`, `escalations`) — the engine validates a step's
|
|
18
|
+
// input with `additionalProperties:false` otherwise.
|
|
19
|
+
inputSchema: z
|
|
20
|
+
.object({
|
|
21
|
+
approved: z.boolean(),
|
|
22
|
+
editedPayload: refundRequest.optional(), // only when the approver edited the amount (`editablePaths` below)
|
|
23
|
+
})
|
|
24
|
+
.passthrough(),
|
|
13
25
|
outputSchema: z.object({ refundId: z.string().nullable() }),
|
|
14
26
|
sideEffects: 'external', // a platform-fault reclaim PARKS this step (never auto-retried); the run gates `exception` (§06 §6.3.5)
|
|
15
27
|
onError: 'park', // a final customer-fault failure parks too — the operator retries / skips / supplies the refundId / fails it
|
|
16
28
|
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)
|
|
17
|
-
async execute({ inputData, occurrenceId }) {
|
|
18
|
-
if (!inputData.approved) return { refundId: null };
|
|
19
|
-
|
|
29
|
+
async execute({ inputData, getStepResult, occurrenceId }) {
|
|
30
|
+
if (!inputData.approved) return { refundId: null }; // denied, or timed out under the chain's terminal 'deny' — data, never an exception (§06 §6.4.11)
|
|
31
|
+
// The edited details when the approver lowered the amount, else the request the approver saw (the `refundRequest` map). The output never echoes it.
|
|
32
|
+
const r = inputData.editedPayload ?? getStepResult<z.infer<typeof refundRequest>>('refundRequest');
|
|
20
33
|
// `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)).
|
|
21
34
|
// 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)).
|
|
22
35
|
const res = await Integrations.passthrough('stripe', {
|
|
@@ -36,6 +49,7 @@ export const refund = createWorkflow({
|
|
|
36
49
|
budget: { maxDurationSeconds: 14 * 24 * 3600 }, // long enough for two business-hours escalation hops (the compiler warns `deadline-clamped` otherwise)
|
|
37
50
|
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)
|
|
38
51
|
})
|
|
52
|
+
.map({ ticketId: fromInit('ticketId'), amount: fromInit('amount') }, { id: 'refundRequest' }) // the payload the approver reviews — and may edit — is the refund request, not the whole run input
|
|
39
53
|
.approval('approveRefund', {
|
|
40
54
|
title: 'Refund request',
|
|
41
55
|
details: template('Refund ${initData.amount} for ticket ${initData.ticketId}'),
|
|
@@ -48,10 +62,10 @@ export const refund = createWorkflow({
|
|
|
48
62
|
{ escalateTo: { role: 'finance-controller' }, timeoutHours: 16 }, // …hop 1: finance, 16 business hours, then…
|
|
49
63
|
{ escalateTo: 'org-admins', timeoutHours: 24 }, // …hop 2: org admins, then…
|
|
50
64
|
'deny',
|
|
51
|
-
], // …the node completes {approved:false, timedOut:true, escalations:2} — branchable data (§06 §6.4.11)
|
|
65
|
+
], // …the node completes {approved:false, decision:'timed_out', text:'timed_out', timedOut:true, escalations:2} — branchable data (§06 §6.4.11)
|
|
52
66
|
editable: true,
|
|
53
67
|
editablePaths: ['amount'], // the approver may lower the amount; the approve call echoes the fingerprint of the revision it saw (§06 §6.4.9)
|
|
54
68
|
editedPayloadSchema: z.object({ ticketId: z.string(), amount: z.number().positive().max(500) }),
|
|
55
69
|
})
|
|
56
|
-
.then(postRefund)
|
|
70
|
+
.then(postRefund) // its input IS the approval's output (the default previous-output rule, §05 §5.5.1.1)
|
|
57
71
|
.commit();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR -> review loop -> approval -> merge.
|
|
1
|
+
// Linear label -> clone -> parallel worktree arms -> merge -> tests -> PR (coding turn) -> review loop -> approval -> the decision (the merge stays human).
|
|
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
3
|
// both test outcomes — the listing's `otherwise: 'openPr'` named a `.then(createStep)` placement, which never declares an id
|
|
4
4
|
// (03 §3.2.0); a string arm names an agentStep / specialistStep / toolStep / map / workflow declaration. `mergeGate.title` is a
|
|
@@ -16,49 +16,43 @@ const runTests = createStep({
|
|
|
16
16
|
workspace: { mount: 'rw' },
|
|
17
17
|
timeoutSeconds: 3600,
|
|
18
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({
|
|
20
|
-
|
|
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)
|
|
21
25
|
log(r.stdout.slice(-4000));
|
|
22
|
-
return { passed: r.
|
|
26
|
+
return { passed: r.code === 0, summary: r.stdout.slice(-2000) };
|
|
23
27
|
},
|
|
24
28
|
});
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
34
|
-
async execute({ workspace, inputData, runId }) {
|
|
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 };
|
|
39
|
-
},
|
|
40
|
-
});
|
|
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.
|
|
41
34
|
|
|
42
|
-
const
|
|
43
|
-
id: '
|
|
44
|
-
inputSchema: z.object({ approved: z.boolean(),
|
|
45
|
-
outputSchema: z.object({ merged: z.boolean() }),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 };
|
|
55
49
|
},
|
|
56
50
|
});
|
|
57
51
|
|
|
58
52
|
export const ticketToPr = createWorkflow({
|
|
59
53
|
name: 'ticket-to-pr',
|
|
60
54
|
description:
|
|
61
|
-
'Linear label → implement/tests/docs in parallel worktrees → merge → test suite → PR → review rounds → approval →
|
|
55
|
+
'Linear label → implement/tests/docs in parallel worktrees → merge → test suite → PR → review rounds → approval → decision (a human merges)',
|
|
62
56
|
inputSchema: z.object({
|
|
63
57
|
ticketId: z.string(),
|
|
64
58
|
title: z.string(),
|
|
@@ -66,7 +60,7 @@ export const ticketToPr = createWorkflow({
|
|
|
66
60
|
repo: z.string(),
|
|
67
61
|
baseRef: z.string().default('main'),
|
|
68
62
|
}),
|
|
69
|
-
outputSchema: z.object({
|
|
63
|
+
outputSchema: z.object({ approved: z.boolean(), prNumber: z.number(), merged: z.boolean() }), // the decision; the merge is a human act (see `recordDecision`)
|
|
70
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
|
|
71
65
|
workspace: {
|
|
72
66
|
kind: 'git',
|
|
@@ -117,7 +111,19 @@ export const ticketToPr = createWorkflow({
|
|
|
117
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.'
|
|
118
112
|
),
|
|
119
113
|
})
|
|
120
|
-
.
|
|
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
|
+
})
|
|
121
127
|
.dowhile('reviewRound', eq(step('reviewRound').path('state'), lit('changes_requested')), { maxIterations: 8 }) // ≤ 8 review rounds (the SWE_REVIEW_MAX_ROUNDS lesson)
|
|
122
128
|
.workflow(
|
|
123
129
|
'reviewRound',
|
|
@@ -133,5 +139,6 @@ export const ticketToPr = createWorkflow({
|
|
|
133
139
|
timeoutHours: 72,
|
|
134
140
|
onTimeout: 'deny',
|
|
135
141
|
})
|
|
136
|
-
.then(
|
|
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)
|
|
137
144
|
.commit();
|