lastlight-workflow-engine 0.1.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.
@@ -0,0 +1,460 @@
1
+ import { z } from "zod";
2
+ // ── Output rules ──────────────────────────────────────────────────────
3
+ const OutputRuleSchema = z.object({
4
+ action: z.enum(["fail", "continue", "pause"]),
5
+ message: z.string().optional(),
6
+ /** Skip the action if the request has this label */
7
+ unless_label: z.string().optional(),
8
+ /** Skip the action if ctx.issueTitle matches this case-insensitive regex */
9
+ unless_title_matches: z.string().optional(),
10
+ /** Template rendered when an `unless_*` clause fires and the rule is bypassed */
11
+ bypass_message: z.string().optional(),
12
+ });
13
+ const PhaseOnOutputSchema = z.object({
14
+ contains_BLOCKED: OutputRuleSchema.optional(),
15
+ contains_READY: OutputRuleSchema.optional(),
16
+ });
17
+ // ── Loop configuration ────────────────────────────────────────────────
18
+ const PhaseLoopMessagesSchema = z.object({
19
+ on_cycle_start: z.string().optional(),
20
+ on_approved: z.string().optional(),
21
+ on_request_changes: z.string().optional(),
22
+ on_max_cycles: z.string().optional(),
23
+ on_fix_start: z.string().optional(),
24
+ on_fix_failed: z.string().optional(),
25
+ on_pause_for_approval: z.string().optional(),
26
+ });
27
+ const PhaseLoopSchema = z.object({
28
+ max_cycles: z.number().int().positive(),
29
+ on_request_changes: z.object({
30
+ fix_prompt: z.string(),
31
+ fix_model: z.string().optional(),
32
+ /** Reasoning-effort override for the fix cycle (e.g. {{variants.fix}}). */
33
+ fix_variant: z.string().optional(),
34
+ re_review_prompt: z.string(),
35
+ }),
36
+ /** Gate to pause at before running the fix (optional) */
37
+ approval_gate: z.string().optional(),
38
+ /**
39
+ * Artifact (handoff doc) filename this gate is asking the reviewer to
40
+ * approve, e.g. `reviewer-verdict.md`. Surfaced by the focused approval
41
+ * view so the reviewer reads/edits the right doc before approving.
42
+ */
43
+ approval_artifact: z.string().optional(),
44
+ /** Optional per-event notification templates rendered through the template engine. */
45
+ messages: PhaseLoopMessagesSchema.optional(),
46
+ });
47
+ const GenericLoopSchema = z
48
+ .object({
49
+ max_iterations: z.number().int().positive(),
50
+ /** Expression to evaluate for completion: "output.contains('PASS')" or "verdict == 'APPROVED'" */
51
+ until: z.string().optional(),
52
+ /** Shell command: exit 0 = loop complete, non-zero = continue */
53
+ until_bash: z.string().optional(),
54
+ /** Pause for human approval between iterations */
55
+ interactive: z.boolean().default(false),
56
+ /** Message shown at the interactive gate */
57
+ gate_message: z.string().optional(),
58
+ /**
59
+ * Gate flavor for interactive loops. `approve` (default when absent)
60
+ * pauses until the user sends an explicit approve/reject command;
61
+ * `reply` pauses until the user sends *any* free-form message in the
62
+ * same thread — the reply body is merged into `scratch` and passed
63
+ * into the next iteration. Used by the socratic explore loop.
64
+ */
65
+ gate_kind: z.enum(["approve", "reply"]).optional(),
66
+ /**
67
+ * Dotted key into `scratch` that the loop writes the current
68
+ * iteration state under (e.g. `socratic`). Only meaningful when
69
+ * `gate_kind: reply` — the runner reads it back on resume to continue
70
+ * from the right iteration instead of restarting from 1.
71
+ */
72
+ scratch_key: z.string().optional(),
73
+ /** Reset agent context each iteration (don't pass previousOutput) */
74
+ fresh_context: z.boolean().default(false),
75
+ /**
76
+ * Policy for a *soft* iteration outcome — the agent exited cleanly but
77
+ * produced no usable output (stop reason `unknown` / `error_truncated`),
78
+ * as opposed to a real crash (fatal / tool error / non-zero exit /
79
+ * terminated), which always fails the run. Absent ⇒ `{ retries: 0, then:
80
+ * "fail" }`, i.e. any non-success iteration hard-fails the workflow (the
81
+ * historical behavior). Opt in to make a loop resilient to a degenerate
82
+ * turn — e.g. the socratic explore loop retries once, then advances to
83
+ * synthesis with the Q&A gathered so far instead of discarding the run.
84
+ */
85
+ on_soft_failure: z
86
+ .object({
87
+ /** Re-run the same iteration up to N times on a soft outcome. */
88
+ retries: z.number().int().min(0).default(0),
89
+ /**
90
+ * What to do when an iteration is still soft after `retries`:
91
+ * `fail` (default) hard-fails the workflow; `complete` treats the
92
+ * loop as finished (as if the `until` condition matched) and lets
93
+ * downstream phases run.
94
+ */
95
+ then: z.enum(["fail", "complete"]).default("fail"),
96
+ })
97
+ .optional(),
98
+ })
99
+ .refine((v) => v.until !== undefined || v.until_bash !== undefined, {
100
+ message: "generic_loop requires at least one of: until, until_bash",
101
+ });
102
+ // ── Phase definition ──────────────────────────────────────────────────
103
+ const PhaseDefinitionSchema = z
104
+ .object({
105
+ name: z.string(),
106
+ /**
107
+ * Optional human-readable label for dashboards and notifications.
108
+ * Defaults to the phase `name` if not set. Source of truth for any
109
+ * UI element that wants to display this phase.
110
+ */
111
+ label: z.string().optional(),
112
+ /**
113
+ * Phase kind:
114
+ * - `context`: no execution (just a dashboard checkpoint marker).
115
+ * - `agent` (default): run an LLM agent session.
116
+ * - `bash`: run a deterministic shell command inside the sandbox
117
+ * container (no LLM). Requires `command:`.
118
+ * - `script`: run an inline JS/TS (via `node`) or Python (via `uv run`)
119
+ * program inside the sandbox container. Requires `script:`.
120
+ * - `post-review`: first-class, in-process PR-review submission. Reads the
121
+ * reviewer agent's `.lastlight/pr-review/findings.json` (content only:
122
+ * `{ skip?, summary, event, findings[] }`), supplies the PR number / base
123
+ * ref / head SHA / diff from the harness's own run context + checkout,
124
+ * anchors each finding to a changed line, and posts ONE formal review via
125
+ * `GitHubClient`. Runs on the harness (no sandbox); a genuine failure
126
+ * fails the phase, a legitimate `skip` succeeds without posting. See
127
+ * `PhaseExecutor.runPostReview`.
128
+ *
129
+ * `bash`/`script` phases run in the SAME sandbox/workspace as agent
130
+ * phases (the host workDir persists across phases keyed by taskId), honour
131
+ * `unrestricted_egress`/`sandbox_image`/`timeout_seconds`, and expose
132
+ * their stdout downstream exactly like an agent phase (`output_var` →
133
+ * `{{phaseOutputs.<name>.output}}`). A non-zero exit fails the phase.
134
+ */
135
+ type: z.enum(["context", "agent", "bash", "script", "post-review"]).default("agent"),
136
+ /**
137
+ * Shell command for `type: bash`. Rendered through the template engine
138
+ * first (so it may reference `{{phaseOutputs.*}}`, `{{branch}}`, etc.),
139
+ * then executed via `sh -c` inside the sandbox container. Exit 0 = success.
140
+ */
141
+ command: z.string().optional(),
142
+ /**
143
+ * Inline program source for `type: script`. Rendered through the template
144
+ * engine, written to a temp file in the workspace, and executed with the
145
+ * runtime selected by `runtime`. Python sources may carry a PEP 723
146
+ * `# /// script` inline-dependency block (resolved by `uv run`).
147
+ */
148
+ script: z.string().optional(),
149
+ /**
150
+ * Runtime for `type: script` (default `js`):
151
+ * - `js` → `node <file>`
152
+ * - `ts` → `node --experimental-strip-types <file>` (sandbox Node 22)
153
+ * - `python` → `uv run <file>`
154
+ */
155
+ runtime: z.enum(["js", "ts", "python"]).optional(),
156
+ /**
157
+ * Per-step timeout in seconds for `bash`/`script` phases. Defaults to the
158
+ * sandbox's configured timeout (fallback 300s for deterministic steps).
159
+ */
160
+ timeout_seconds: z.number().int().positive().optional(),
161
+ /**
162
+ * Path to a prompt template file (relative to workflowDir).
163
+ * Mutually exclusive with `skill`.
164
+ */
165
+ prompt: z.string().optional(),
166
+ /**
167
+ * Single skill to make available to the phase. Sugar for
168
+ * `skills: [<name>]`. Mutually exclusive with `skills` (use one or
169
+ * the other), but may coexist with `prompt`.
170
+ */
171
+ skill: z.string().optional(),
172
+ /**
173
+ * Skills to make available to the phase. Each entry names a directory
174
+ * under `skills/<name>/` (containing SKILL.md plus optional scripts/,
175
+ * references/, assets/). The first entry is the "primary" skill the
176
+ * runner directs the agent to use; the rest are available for the
177
+ * agent to read on demand via pi's progressive-disclosure model.
178
+ *
179
+ * Each named skill is staged into a per-phase bundle at
180
+ * `.lastlight-skills/<phaseName>/<name>/` before the agent runs (symlink
181
+ * in none, copy in docker/gondolin — a symlink's target would be outside
182
+ * the guest's mounted cwd and dangle) and mapped to the agent explicitly via
183
+ * pi's `--skill`/`skillPaths` (absolute paths). cwd stays the repo; the
184
+ * bundle is staged at the workspace root — a sibling of the repo, outside
185
+ * its git tree — for docker/none (gondolin, which mounts only cwd, stages
186
+ * it under the repo + local `.git/info/exclude` so it's never committed).
187
+ * Keyed per phase so concurrent phases can't clobber each other. Phases
188
+ * with no `skills:`/`skill:` field get no bundle — `prompt:`-only phases
189
+ * are unaffected.
190
+ *
191
+ * May coexist with `prompt`: when both are set, the prompt template
192
+ * is the user prompt and `skills:` just stages the catalogue. The
193
+ * prompt can then reference skills by name ("see the `pr-review`
194
+ * skill for the structured-feedback format") and the agent can
195
+ * read them via its built-in `read` tool.
196
+ *
197
+ * Mutually exclusive with the singular `skill:` field.
198
+ */
199
+ skills: z.array(z.string()).min(1).optional(),
200
+ /** Model override — can reference template vars like {{models.architect}} */
201
+ model: z.string().optional(),
202
+ /**
203
+ * Reasoning-effort override — can reference template vars like
204
+ * `{{variants.architect}}`. Maps to agentic-pi's thinking level
205
+ * (`off | minimal | low | medium | high | xhigh`). pi-ai translates
206
+ * this into each provider's reasoning-effort API.
207
+ */
208
+ variant: z.string().optional(),
209
+ /**
210
+ * Bypass the sandbox HTTP egress allowlist for this phase.
211
+ *
212
+ * Default: the phase runs with the standard allowlist (GitHub, LLM
213
+ * providers, public package registries — see
214
+ * `src/sandbox/egress-allowlist.ts`). Set `unrestricted_egress: true`
215
+ * for phases that need broad web access, e.g. an `explore` phase that
216
+ * searches third-party documentation. Use sparingly — this is the
217
+ * exfil control the allowlist exists to enforce.
218
+ */
219
+ unrestricted_egress: z.boolean().optional(),
220
+ /**
221
+ * Enable agentic-pi's web-search extension (`web_search` /
222
+ * `web_fetch` tools) for this phase. Default: undefined → false.
223
+ *
224
+ * Set `true` on phases that should be able to search the web — e.g.
225
+ * the research phases of the `explore` workflow. The opt-out
226
+ * default is load-bearing: agentic-pi auto-enables web search
227
+ * whenever a provider env var (`TAVILY_API_KEY`,
228
+ * `BRAVE_SEARCH_API_KEY`, `EXA_API_KEY`) is present in
229
+ * process.env, so we need an explicit `false` for every phase
230
+ * that should not see the tools.
231
+ *
232
+ * Phases that opt in usually also want `unrestricted_egress: true`
233
+ * so `web_fetch` calls can reach arbitrary docs sites through the
234
+ * open-mode firewall — `web_search` itself only needs the provider
235
+ * host (api.tavily.com / api.search.brave.com / api.exa.ai) which
236
+ * is also covered by the open-mode tunnel.
237
+ */
238
+ web_search: z.boolean().optional(),
239
+ /**
240
+ * Capability gate: the sandbox backend this phase needs to run on.
241
+ *
242
+ * Default (absent): the phase runs on whatever backend the harness is
243
+ * configured for. When set, the phase runs only if the **active** backend
244
+ * matches; otherwise it is **silently skipped** — recorded as a
245
+ * non-failing skip (like a trigger-rule skip), never a failure. This is
246
+ * safe-by-default graceful degradation: a phase that depends on tooling
247
+ * baked only into a specific sandbox image (a `/demo` video render, a
248
+ * headless-browser QA step) just no-ops on a host that can't provide it
249
+ * instead of breaking the workflow. E.g. the demo step in `build` declares
250
+ * `requires_sandbox: docker` so it skips (rather than fails the build) on a
251
+ * gondolin-only host.
252
+ *
253
+ * A skipped node is not `succeeded`, so a downstream phase that depends on
254
+ * a gated phase via the default `all_success` rule would itself skip. Keep
255
+ * gated phases **terminal** (nothing depends on them) or give their
256
+ * dependants `trigger_rule: all_done`. Pair with `messages.on_skipped_done`
257
+ * to surface why the phase was skipped.
258
+ */
259
+ requires_sandbox: z.enum(["docker", "gondolin", "none"]).optional(),
260
+ /**
261
+ * Which docker sandbox image this phase runs in. `default` (or omitted) uses
262
+ * the lean `lastlight-sandbox:latest`; `qa` uses the heavier
263
+ * `lastlight-sandbox-qa:latest` (Playwright + Chromium baked in) for the
264
+ * browser-QA path. Only meaningful on the docker backend — pair it with
265
+ * `requires_sandbox: docker` so the phase skips on gondolin. When `qa` is
266
+ * requested but that image isn't built on the host, the scheduler skips the
267
+ * phase too (graceful degradation), so keep such phases terminal.
268
+ */
269
+ sandbox_image: z.enum(["default", "qa"]).optional(),
270
+ /** Named approval gate to pause at after this phase */
271
+ approval_gate: z.string().optional(),
272
+ /**
273
+ * Artifact (handoff doc) filename this gate is asking the reviewer to
274
+ * approve, e.g. `architect-plan.md`. Surfaced by the focused approval
275
+ * view so the reviewer reads/edits the right doc before approving.
276
+ */
277
+ approval_artifact: z.string().optional(),
278
+ /** Message template rendered when pausing at this phase's approval gate. */
279
+ approval_gate_message: z.string().optional(),
280
+ /** Optional per-event notification templates rendered through the template engine. */
281
+ messages: z
282
+ .object({
283
+ on_start: z.string().optional(),
284
+ on_success: z.string().optional(),
285
+ on_failure: z.string().optional(),
286
+ on_skipped_done: z.string().optional(),
287
+ on_blocked: z.string().optional(),
288
+ on_blocked_bypassed: z.string().optional(),
289
+ })
290
+ .optional(),
291
+ /** Loop configuration for reviewer-style looping phases */
292
+ loop: PhaseLoopSchema.optional(),
293
+ /** Generic loop configuration — expression/bash-based completion conditions */
294
+ generic_loop: GenericLoopSchema.optional(),
295
+ /** Rules applied to agent output */
296
+ on_output: PhaseOnOutputSchema.optional(),
297
+ /** Actions taken on successful completion */
298
+ on_success: z
299
+ .object({
300
+ set_phase: z.string().optional(),
301
+ })
302
+ .optional(),
303
+ /** DAG: list of phase names this phase depends on */
304
+ depends_on: z.array(z.string()).optional(),
305
+ /** DAG: trigger rule for this phase — when to run based on dependency outcomes */
306
+ trigger_rule: z.enum(["all_success", "one_success", "none_failed_min_one_success", "all_done"]).optional(),
307
+ /** DAG: variable name to store the output of this phase for use in downstream phases */
308
+ output_var: z.string().optional(),
309
+ })
310
+ .refine((p) => !(p.skill && p.skills), {
311
+ message: "phase cannot specify both `skill` and `skills` — use `skills` for multiple",
312
+ })
313
+ .superRefine((p, ctx) => {
314
+ const type = p.type ?? "agent";
315
+ if (type === "bash") {
316
+ if (!p.command) {
317
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "phase type `bash` requires `command:`" });
318
+ }
319
+ if (p.script !== undefined) {
320
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`script:` is only valid on type `script`" });
321
+ }
322
+ }
323
+ else if (type === "script") {
324
+ if (!p.script) {
325
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "phase type `script` requires `script:`" });
326
+ }
327
+ if (p.command !== undefined) {
328
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`command:` is only valid on type `bash`" });
329
+ }
330
+ }
331
+ else {
332
+ // context / agent
333
+ if (p.command !== undefined) {
334
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`command:` is only valid on type `bash`" });
335
+ }
336
+ if (p.script !== undefined) {
337
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`script:` is only valid on type `script`" });
338
+ }
339
+ if (p.runtime !== undefined) {
340
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`runtime:` is only valid on type `script`" });
341
+ }
342
+ }
343
+ });
344
+ /**
345
+ * Normalize a phase's skill declaration to a single list of names.
346
+ * Returns `[]` for phases that have neither `skill` nor `skills`
347
+ * (e.g. `prompt:`-only or `context:` phases).
348
+ */
349
+ export function phaseSkillNames(phase) {
350
+ if (phase.skills?.length)
351
+ return phase.skills;
352
+ if (phase.skill)
353
+ return [phase.skill];
354
+ return [];
355
+ }
356
+ // ── Agent workflow ────────────────────────────────────────────────────
357
+ //
358
+ // "Agent workflows" are the unified definition for everything Last Light does
359
+ // that involves running an agent — whether it's a multi-phase build cycle, a
360
+ // single-shot triage, a PR review, a health report, or a custom user-defined
361
+ // workflow. Each is a list of phases that the runner executes; simple
362
+ // workflows have a single phase, complex ones have many with optional loops,
363
+ // approval gates, DAG dependencies, etc.
364
+ //
365
+ // The `kind` field is purely a categorization label used by the dashboard
366
+ // (e.g. to group runs by purpose). The runner ignores it.
367
+ /**
368
+ * Intents owned by the harness itself (workflow gates / session control), not by
369
+ * any workflow. A workflow's `classification.intent` may not claim one of these.
370
+ * The classifier keeps their category text + examples in its base prompt template.
371
+ */
372
+ export const RESERVED_CONTROL_INTENTS = [
373
+ "approve",
374
+ "reject",
375
+ "status",
376
+ "reset",
377
+ "chat",
378
+ ];
379
+ /**
380
+ * Uppercase prompt token for an intent — the string the classifier emits on its
381
+ * `INTENT:` line and parses back. `qa-test` → `QATEST`, `incident` → `INCIDENT`.
382
+ * Kept here (workflows layer) so the loader's validation and the classifier's
383
+ * prompt/parser derive tokens the same way.
384
+ */
385
+ export function intentToken(intent) {
386
+ return intent.toUpperCase().replace(/-/g, "");
387
+ }
388
+ export const AgentWorkflowSchema = z.object({
389
+ /** Categorization label — e.g. "build", "triage", "review", "health". Free string. */
390
+ kind: z.string().default("agent"),
391
+ name: z.string(),
392
+ description: z.string().optional(),
393
+ /** What can trigger this workflow (informational; routing is in code). */
394
+ trigger: z.string().optional(),
395
+ /**
396
+ * Render progress as a single in-place "task list" comment/message that is
397
+ * edited as phases run, instead of posting a new comment per phase. When
398
+ * true, the runner drives `callbacks.reporter` (see `src/notify/`) and the
399
+ * per-phase `messages.on_*` strings become the one-line detail on each
400
+ * checklist step. Default (absent/false) keeps the legacy one-comment-per-
401
+ * phase behavior. Opt in on chatty multi-phase workflows (build, explore,
402
+ * pr-fix); leave off for single-deliverable workflows (triage, pr-review,
403
+ * health) that post their own result.
404
+ */
405
+ status_checklist: z.boolean().optional(),
406
+ /**
407
+ * Template rendered once at workflow wrap-up (against the accumulated
408
+ * `output_var`s) and delivered as the single final update: set as the
409
+ * footer of the in-place checklist comment when `status_checklist` is on, or
410
+ * posted as one standalone comment otherwise. Renders empty ⇒ no-op. Lets a
411
+ * workflow end with one synthesized result (e.g. verify/qa-test fold their
412
+ * text + browser passes into a single verdict via a final `synthesize` phase
413
+ * whose `output_var` this references) instead of a comment per phase.
414
+ */
415
+ final_message: z.string().optional(),
416
+ variables: z.record(z.string(), z.string()).optional(),
417
+ /**
418
+ * How the free-text intent classifier should route to this workflow. When
419
+ * present, this workflow contributes one category to the composed classifier
420
+ * prompt (`src/engine/screen/classifier.ts`) AND claims the `intent` token:
421
+ * a comment/message the classifier tags with this intent routes here (via the
422
+ * router's `getWorkflowByIntent` fallback) unless a bespoke router branch
423
+ * already handles that intent. The prompt token is derived from `intent`
424
+ * (`intent.toUpperCase().replace(/-/g, "")` — e.g. `qa-test` → `QATEST`).
425
+ * Validated in the loader: `intent` must be unique across workflows and must
426
+ * not collide with a reserved control intent (approve/reject/status/reset/chat).
427
+ */
428
+ classification: z
429
+ .object({
430
+ /** Intent token this workflow owns (e.g. "build", "qa-test", "incident"). */
431
+ intent: z.string(),
432
+ /** The category paragraph, conventionally prefixed with its UPPER token. */
433
+ description: z.string(),
434
+ /** Optional one-line classifier examples (verbatim lines for the prompt). */
435
+ examples: z.array(z.string()).optional(),
436
+ })
437
+ .optional(),
438
+ phases: z.array(PhaseDefinitionSchema),
439
+ });
440
+ // ── Cron workflow ─────────────────────────────────────────────────────
441
+ //
442
+ // Cron workflows are NOT runnable themselves — they describe a schedule that
443
+ // triggers another workflow. The `workflow` field is the name of an
444
+ // AgentWorkflow to invoke on each tick. (Previously this carried a `skill`
445
+ // field referencing the legacy executeSkill path; that path no longer exists.)
446
+ export const CronWorkflowSchema = z.object({
447
+ kind: z.literal("cron"),
448
+ name: z.string(),
449
+ schedule: z.string(),
450
+ /** Name of the AgentWorkflow to run on each tick */
451
+ workflow: z.string(),
452
+ /** Static context to merge into the workflow's input on each tick */
453
+ context: z.record(z.string(), z.unknown()).default({}),
454
+ condition: z
455
+ .object({
456
+ unless: z.string().optional(),
457
+ })
458
+ .optional(),
459
+ });
460
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/core/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Minimal template engine for workflow prompt files.
3
+ *
4
+ * Supports:
5
+ * {{varName}} — simple variable substitution
6
+ * {{slugify varName}} — slugify helper applied to a variable
7
+ * {{branchUrl file}} — generate a GitHub branch URL for a file in issueDir
8
+ * {{#if varName}}...{{/if}} — conditional blocks (no nesting, truthy check)
9
+ */
10
+ export interface TemplateContext {
11
+ owner: string;
12
+ repo: string;
13
+ issueNumber: number;
14
+ issueTitle: string;
15
+ issueBody: string;
16
+ issueLabels: string[];
17
+ commentBody: string;
18
+ sender: string;
19
+ branch: string;
20
+ taskId: string;
21
+ issueDir: string;
22
+ bootstrapLabel: string;
23
+ /**
24
+ * True only in server mode (handoff docs externalized to the Last Light
25
+ * store instead of committed into the target repo). Prompts gate their
26
+ * `git add .lastlight/ && commit` behind `{{#if !externalizeArtifacts}}`, so
27
+ * an absent flag defaults to repo behaviour (commit the docs) — fail-safe for
28
+ * any render path that doesn't set it.
29
+ */
30
+ externalizeArtifacts?: boolean;
31
+ /**
32
+ * Dashboard base URL (no trailing slash). Used by the `{{artifactUrl file}}`
33
+ * helper to build a server-mode dashboard link to a handoff doc. Absent when
34
+ * no public URL is configured.
35
+ */
36
+ publicUrl?: string;
37
+ /**
38
+ * Public, unauthenticated, image-only base URL for inline screenshot embeds
39
+ * in the browser-QA comment (`<publicUrl>/admin/api/public/artifacts/<owner>/
40
+ * <repo>/<issueKey>`). The agent appends `/<name>.png` per screenshot. Empty
41
+ * string when no public URL is configured — browser-QA prompts then fall back
42
+ * to filename-only references via `{{#if !artifactBaseUrl}}`.
43
+ */
44
+ artifactBaseUrl?: string;
45
+ /**
46
+ * Id of the approval being created — injected into an approval-gate message's
47
+ * render context by `PhaseExecutor.pauseForApproval` so the `{{approvalUrl}}`
48
+ * helper can deep-link to the focused approval view.
49
+ */
50
+ approvalId?: string;
51
+ approved?: boolean;
52
+ fixCycles?: number;
53
+ reviewerNote?: string;
54
+ docLinks?: string;
55
+ fixCycle?: number;
56
+ prNumber?: number;
57
+ prTitle?: string;
58
+ prBody?: string;
59
+ failedChecks?: string;
60
+ ciSection?: string;
61
+ prePopulateBranch?: string;
62
+ contextSnapshot?: string;
63
+ iteration?: number;
64
+ maxIterations?: number;
65
+ previousOutput?: string;
66
+ phaseOutputs?: Record<string, unknown>;
67
+ /**
68
+ * Mutable phase-to-phase state surfaced from `workflow_runs.scratch`.
69
+ * Used by the socratic explore loop to accumulate Q&A across reply-gate
70
+ * pauses. Templates can read {{scratch.socratic.qa}} etc. via the
71
+ * existing two-level dot notation in renderTemplate.
72
+ */
73
+ scratch?: Record<string, unknown>;
74
+ /**
75
+ * Non-GitHub trigger id (currently only `slack:{teamId}:{channel}:{thread}`).
76
+ * When set, the runner uses this instead of deriving one from
77
+ * owner/repo/issueNumber — that gives Slack-initiated workflows a stable
78
+ * key to pause/resume on.
79
+ */
80
+ triggerIdOverride?: string;
81
+ [key: string]: unknown;
82
+ }
83
+ export declare function slugify(text: string): string;
84
+ /**
85
+ * Render a template string with the given context.
86
+ * Processes: {{#if}}, {{slugify}}, {{branchUrl}}, {{varName}}.
87
+ */
88
+ export declare function renderTemplate(template: string, ctx: TemplateContext): string;