astrocode-workflow 0.3.0 → 0.3.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.
@@ -1,7 +1,8 @@
1
1
  // src/tools/workflow.ts
2
2
  import { tool } from "@opencode-ai/plugin/tool";
3
+ import { withTx } from "../state/db";
3
4
  import { buildContextSnapshot } from "../workflow/context";
4
- import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
5
+ import { decideNextAction, createRunForStory, startStage, completeRun, failRun, getActiveRun, EVENT_TYPES, } from "../workflow/state-machine";
5
6
  import { buildStageDirective, directiveHash } from "../workflow/directives";
6
7
  import { injectChatPrompt } from "../ui/inject";
7
8
  import { nowISO } from "../shared/time";
@@ -104,6 +105,46 @@ function buildDelegationPrompt(opts) {
104
105
  debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
105
106
  return prompt;
106
107
  }
108
+ function buildUiMessage(e) {
109
+ switch (e.kind) {
110
+ case "stage_started": {
111
+ const agent = e.agent_name ? ` (${e.agent_name})` : "";
112
+ const title = "Astrocode";
113
+ const message = `Stage started: ${e.stage_key}${agent}`;
114
+ const chatText = [
115
+ `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
116
+ ``,
117
+ `Run: ${e.run_id}`,
118
+ `Stage: ${e.stage_key}${agent}`,
119
+ ].join("\n");
120
+ return { title, message, variant: "info", chatText };
121
+ }
122
+ case "run_completed": {
123
+ const title = "Astrocode";
124
+ const message = `Run completed: ${e.run_id}`;
125
+ const chatText = [
126
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
127
+ ``,
128
+ `Run: ${e.run_id}`,
129
+ `Story: ${e.story_key}`,
130
+ ].join("\n");
131
+ return { title, message, variant: "success", chatText };
132
+ }
133
+ case "run_failed": {
134
+ const title = "Astrocode";
135
+ const message = `Run failed: ${e.run_id} (${e.stage_key})`;
136
+ const chatText = [
137
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
138
+ ``,
139
+ `Run: ${e.run_id}`,
140
+ `Story: ${e.story_key}`,
141
+ `Stage: ${e.stage_key}`,
142
+ `Error: ${e.error_text}`,
143
+ ].join("\n");
144
+ return { title, message, variant: "error", chatText };
145
+ }
146
+ }
147
+ }
107
148
  export function createAstroWorkflowProceedTool(opts) {
108
149
  const { ctx, config, db, agents } = opts;
109
150
  const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
@@ -119,6 +160,9 @@ export function createAstroWorkflowProceedTool(opts) {
119
160
  const actions = [];
120
161
  const warnings = [];
121
162
  const startedAt = nowISO();
163
+ // Collect UI events emitted inside state-machine functions, then flush AFTER tx.
164
+ const uiEvents = [];
165
+ const emit = (e) => uiEvents.push(e);
122
166
  for (let i = 0; i < steps; i++) {
123
167
  const next = decideNextAction(db, config);
124
168
  if (next.kind === "idle") {
@@ -126,55 +170,24 @@ export function createAstroWorkflowProceedTool(opts) {
126
170
  break;
127
171
  }
128
172
  if (next.kind === "start_run") {
129
- // NOTE: createRunForStory owns its own tx (state-machine.ts).
130
- const { run_id } = createRunForStory(db, config, next.story_key);
173
+ // SINGLE tx boundary: caller owns tx, state-machine is pure.
174
+ const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
131
175
  actions.push(`started run ${run_id} for story ${next.story_key}`);
132
- if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
133
- await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
134
- }
135
- if (sessionId) {
136
- await injectChatPrompt({
137
- ctx,
138
- sessionId,
139
- agent: "Astro",
140
- text: [
141
- `[SYSTEM DIRECTIVE: ASTROCODE — RUN_STARTED]`,
142
- ``,
143
- `Run started: \`${run_id}\``,
144
- `Story: \`${next.story_key}\``,
145
- ``,
146
- `Next: call **astro_workflow_proceed** again to delegate the first stage.`,
147
- ].join("\n"),
148
- });
149
- actions.push(`injected run started message for ${run_id}`);
150
- }
151
176
  if (mode === "step")
152
177
  break;
153
178
  continue;
154
179
  }
155
180
  if (next.kind === "complete_run") {
156
- // NOTE: completeRun owns its own tx (state-machine.ts).
157
- completeRun(db, next.run_id);
181
+ withTx(db, () => completeRun(db, next.run_id, emit));
158
182
  actions.push(`completed run ${next.run_id}`);
159
- if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
160
- await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
161
- }
162
- // ✅ explicit injection on completeRun (requested)
163
- if (sessionId) {
164
- await injectChatPrompt({
165
- ctx,
166
- sessionId,
167
- agent: "Astro",
168
- text: [
169
- `[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
170
- ``,
171
- `Run \`${next.run_id}\` completed.`,
172
- ``,
173
- `Next: call **astro_workflow_proceed** (mode=step) to start the next approved story (if any).`,
174
- ].join("\n"),
175
- });
176
- actions.push(`injected run completed message for ${next.run_id}`);
177
- }
183
+ if (mode === "step")
184
+ break;
185
+ continue;
186
+ }
187
+ if (next.kind === "failed") {
188
+ // Ensure DB state reflects failure in one tx; emit UI event.
189
+ withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
190
+ actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
178
191
  if (mode === "step")
179
192
  break;
180
193
  continue;
@@ -207,27 +220,9 @@ export function createAstroWorkflowProceedTool(opts) {
207
220
  }
208
221
  }
209
222
  // NOTE: startStage owns its own tx (state-machine.ts).
210
- startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
211
- actions.push(`stage started: ${next.stage_key}`);
212
- if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
213
- await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
214
- }
215
- // ✅ explicit injection on startStage (requested)
216
- if (sessionId) {
217
- await injectChatPrompt({
218
- ctx,
219
- sessionId,
220
- agent: "Astro",
221
- text: [
222
- `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
223
- ``,
224
- `Run: \`${active.run_id}\``,
225
- `Stage: \`${next.stage_key}\``,
226
- `Delegated to: \`${agentName}\``,
227
- ].join("\n"),
228
- });
229
- actions.push(`injected stage started message for ${next.stage_key}`);
230
- }
223
+ withTx(db, () => {
224
+ startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
225
+ });
231
226
  const context = buildContextSnapshot({
232
227
  db,
233
228
  config,
@@ -251,18 +246,14 @@ export function createAstroWorkflowProceedTool(opts) {
251
246
  stage_key: next.stage_key,
252
247
  stage_agent_name: agentName,
253
248
  });
254
- // Best-effort: continuations table may not exist on older DBs.
255
- try {
256
- const h = directiveHash(delegatePrompt);
257
- const now = nowISO();
258
- if (sessionId) {
259
- db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
260
- }
261
- }
262
- catch (e) {
263
- warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
249
+ // Record continuation (best-effort; no tx wrapper needed but safe either way)
250
+ const h = directiveHash(delegatePrompt);
251
+ const now = nowISO();
252
+ if (sessionId) {
253
+ // This assumes continuations table exists in vNext schema.
254
+ db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
264
255
  }
265
- // Visible injection so user can see state
256
+ // Visible injection so user can see state (awaited)
266
257
  if (sessionId) {
267
258
  await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
268
259
  const continueMessage = [
@@ -273,7 +264,7 @@ export function createAstroWorkflowProceedTool(opts) {
273
264
  `When \`${agentName}\` completes, call:`,
274
265
  `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
275
266
  ``,
276
- `Then run **astro_workflow_proceed** again.`,
267
+ `This advances the workflow.`,
277
268
  ].join("\n");
278
269
  await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
279
270
  }
@@ -299,46 +290,40 @@ export function createAstroWorkflowProceedTool(opts) {
299
290
  `Context snapshot:`,
300
291
  context,
301
292
  ].join("\n").trim();
302
- try {
303
- const h = directiveHash(prompt);
304
- const now = nowISO();
305
- db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
306
- }
307
- catch (e) {
308
- warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
309
- }
293
+ const h = directiveHash(prompt);
294
+ const now = nowISO();
295
+ db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
310
296
  await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
311
297
  }
312
298
  break;
313
299
  }
314
- if (next.kind === "failed") {
315
- actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
316
- if (sessionId) {
300
+ actions.push(`unhandled next action: ${next.kind}`);
301
+ break;
302
+ }
303
+ // Flush UI events (toast + prompt) AFTER state transitions
304
+ if (uiEvents.length > 0) {
305
+ for (const e of uiEvents) {
306
+ const msg = buildUiMessage(e);
307
+ if (config.ui.toasts.enabled) {
308
+ await toasts.show({
309
+ title: msg.title,
310
+ message: msg.message,
311
+ variant: msg.variant,
312
+ });
313
+ }
314
+ if (ctx?.sessionID) {
317
315
  await injectChatPrompt({
318
316
  ctx,
319
- sessionId,
317
+ sessionId: ctx.sessionID,
318
+ text: msg.chatText,
320
319
  agent: "Astro",
321
- text: [
322
- `[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
323
- ``,
324
- `Run \`${next.run_id}\` failed at stage \`${next.stage_key}\`.`,
325
- `Error: ${next.error_text}`,
326
- ].join("\n"),
327
320
  });
328
- actions.push(`injected run failed message for ${next.run_id}`);
329
321
  }
330
- break;
331
322
  }
332
- actions.push(`unhandled next action: ${next.kind}`);
333
- break;
334
- }
335
- // Housekeeping event (best-effort)
336
- try {
337
- db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
338
- }
339
- catch (e) {
340
- warnings.push(`workflow.proceed event insert failed (non-fatal): ${String(e)}`);
323
+ actions.push(`ui: flushed ${uiEvents.length} event(s)`);
341
324
  }
325
+ // Housekeeping event
326
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
342
327
  const active = getActiveRun(db);
343
328
  const lines = [];
344
329
  lines.push(`# astro_workflow_proceed`);
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Inject a visible prompt into the conversation.
3
+ * - Deterministic ordering per session
4
+ * - Correct SDK binding (prevents `this._client` undefined)
5
+ * - Awaitable: resolves when delivered, rejects after max retries
6
+ */
1
7
  export declare function injectChatPrompt(opts: {
2
8
  ctx: any;
3
9
  sessionId?: string;
package/dist/ui/inject.js CHANGED
@@ -1,88 +1,95 @@
1
- // src/ui/inject.ts
2
- let isInjecting = false;
3
- const injectionQueue = [];
1
+ const MAX_ATTEMPTS = 4;
2
+ const RETRY_DELAYS_MS = [250, 500, 1000, 2000]; // attempt 1..4
3
+ // Per-session queues so one stuck session doesn't block others
4
+ const queues = new Map();
5
+ const running = new Set();
4
6
  function sleep(ms) {
5
7
  return new Promise((r) => setTimeout(r, ms));
6
8
  }
7
- function resolveSessionId(ctx, sessionId) {
8
- if (sessionId)
9
- return sessionId;
10
- const direct = ctx?.sessionID ?? ctx?.sessionId ?? ctx?.session?.id;
11
- if (typeof direct === "string" && direct.length > 0)
12
- return direct;
13
- return null;
14
- }
15
- async function tryInjectOnce(opts) {
16
- const { ctx, sessionId, text, agent } = opts;
17
- // Prefer explicit chat prompt API
18
- const promptApi = ctx?.client?.session?.prompt;
19
- if (!promptApi) {
9
+ function getPromptInvoker(ctx) {
10
+ const session = ctx?.client?.session;
11
+ const prompt = session?.prompt;
12
+ if (!session || typeof prompt !== "function") {
20
13
  throw new Error("API not available (ctx.client.session.prompt)");
21
14
  }
15
+ return { session, prompt };
16
+ }
17
+ async function tryInjectOnce(item) {
18
+ const { ctx, sessionId, text, agent = "Astro" } = item;
22
19
  const prefixedText = `[${agent}]\n\n${text}`;
23
- // Some hosts reject unknown fields; keep body minimal and stable.
24
- await promptApi({
20
+ const { session, prompt } = getPromptInvoker(ctx);
21
+ // IMPORTANT: force correct `this` binding
22
+ await prompt.call(session, {
25
23
  path: { id: sessionId },
26
24
  body: {
27
25
  parts: [{ type: "text", text: prefixedText }],
26
+ agent,
28
27
  },
29
28
  });
30
29
  }
31
- async function processQueue() {
32
- if (isInjecting)
30
+ async function runSessionQueue(sessionId) {
31
+ if (running.has(sessionId))
33
32
  return;
34
- if (injectionQueue.length === 0)
35
- return;
36
- isInjecting = true;
33
+ running.add(sessionId);
37
34
  try {
38
- while (injectionQueue.length > 0) {
39
- const item = injectionQueue.shift();
40
- if (!item)
41
- continue;
42
- const { ctx, text, agent = "Astro" } = item;
43
- const sessionId = resolveSessionId(ctx, item.sessionId);
44
- if (!sessionId) {
45
- // Drop on floor: we cannot recover without a session id.
46
- // Keep draining the queue so we don't stall.
47
- // eslint-disable-next-line no-console
48
- console.warn("[Astrocode] Injection skipped: no sessionId");
49
- continue;
35
+ // eslint-disable-next-line no-constant-condition
36
+ while (true) {
37
+ const q = queues.get(sessionId);
38
+ if (!q || q.length === 0)
39
+ break;
40
+ const item = q.shift();
41
+ try {
42
+ await tryInjectOnce(item);
43
+ item.resolve();
50
44
  }
51
- const maxAttempts = item.retry?.maxAttempts ?? 4;
52
- const baseDelayMs = item.retry?.baseDelayMs ?? 250;
53
- let lastErr = null;
54
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
55
- try {
56
- await tryInjectOnce({ ctx, sessionId, text, agent });
57
- lastErr = null;
58
- break;
59
- }
60
- catch (e) {
61
- lastErr = e;
62
- const delay = baseDelayMs * Math.pow(2, attempt - 1); // 250, 500, 1000, 2000
63
- // eslint-disable-next-line no-console
64
- console.warn(`[Astrocode] Injection attempt ${attempt}/${maxAttempts} failed: ${String(e)}; retrying in ${delay}ms`);
65
- await sleep(delay);
45
+ catch (err) {
46
+ item.attempts += 1;
47
+ const msg = err instanceof Error ? err.message : String(err);
48
+ const delay = RETRY_DELAYS_MS[Math.min(item.attempts - 1, RETRY_DELAYS_MS.length - 1)] ?? 2000;
49
+ if (item.attempts >= MAX_ATTEMPTS) {
50
+ console.warn(`[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`);
51
+ item.reject(err);
52
+ continue;
66
53
  }
67
- }
68
- if (lastErr) {
69
- // eslint-disable-next-line no-console
70
- console.warn(`[Astrocode] Injection failed permanently after ${maxAttempts} attempts: ${String(lastErr)}`);
54
+ console.warn(`[Astrocode] Injection attempt ${item.attempts}/${MAX_ATTEMPTS} failed: ${msg}; retrying in ${delay}ms`);
55
+ await sleep(delay);
56
+ // Requeue at front to preserve order (and avoid starving later messages)
57
+ const q2 = queues.get(sessionId) ?? [];
58
+ q2.unshift(item);
59
+ queues.set(sessionId, q2);
71
60
  }
72
61
  }
73
62
  }
74
63
  finally {
75
- isInjecting = false;
64
+ running.delete(sessionId);
76
65
  }
77
66
  }
67
+ /**
68
+ * Inject a visible prompt into the conversation.
69
+ * - Deterministic ordering per session
70
+ * - Correct SDK binding (prevents `this._client` undefined)
71
+ * - Awaitable: resolves when delivered, rejects after max retries
72
+ */
78
73
  export async function injectChatPrompt(opts) {
79
- injectionQueue.push({
80
- ctx: opts.ctx,
81
- sessionId: opts.sessionId,
82
- text: opts.text,
83
- agent: opts.agent ?? "Astro",
84
- retry: { maxAttempts: 4, baseDelayMs: 250 },
74
+ const sessionId = opts.sessionId ?? opts.ctx?.sessionID;
75
+ if (!sessionId) {
76
+ console.warn("[Astrocode] Skipping injection: No sessionId provided");
77
+ return;
78
+ }
79
+ return new Promise((resolve, reject) => {
80
+ const item = {
81
+ ctx: opts.ctx,
82
+ sessionId,
83
+ text: opts.text,
84
+ agent: opts.agent,
85
+ attempts: 0,
86
+ resolve,
87
+ reject,
88
+ };
89
+ const q = queues.get(sessionId) ?? [];
90
+ q.push(item);
91
+ queues.set(sessionId, q);
92
+ // Fire worker (don't await here; caller awaits the returned Promise)
93
+ void runSessionQueue(sessionId);
85
94
  });
86
- // Fire-and-forget; queue drain is serialized by isInjecting.
87
- void processQueue();
88
95
  }
@@ -1,7 +1,6 @@
1
1
  import type { AstrocodeConfig } from "../config/schema";
2
2
  import type { SqliteDb } from "../state/db";
3
3
  import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
4
- import type { ToastOptions } from "../ui/toasts";
5
4
  export declare const EVENT_TYPES: {
6
5
  readonly RUN_STARTED: "run.started";
7
6
  readonly RUN_COMPLETED: "run.completed";
@@ -11,28 +10,39 @@ export declare const EVENT_TYPES: {
11
10
  readonly STAGE_STARTED: "stage.started";
12
11
  readonly WORKFLOW_PROCEED: "workflow.proceed";
13
12
  };
14
- /**
15
- * UI HOOKS
16
- * --------
17
- * This workflow module is DB-first. UI emission is optional and happens AFTER the DB tx commits.
18
- *
19
- * Contract:
20
- * - If you pass ui.ctx + ui.sessionId, we will inject a visible chat message deterministically.
21
- * - If you pass ui.toast, we will also toast (throttling is handled by the toast manager).
22
- */
23
- export type WorkflowUi = {
24
- ctx: any;
25
- sessionId: string;
26
- agentName?: string;
27
- toast?: (t: ToastOptions) => Promise<void>;
13
+ export type UiEmitEvent = {
14
+ kind: "stage_started";
15
+ run_id: string;
16
+ stage_key: StageKey;
17
+ agent_name?: string;
18
+ } | {
19
+ kind: "run_completed";
20
+ run_id: string;
21
+ story_key: string;
22
+ } | {
23
+ kind: "run_failed";
24
+ run_id: string;
25
+ story_key: string;
26
+ stage_key: StageKey;
27
+ error_text: string;
28
28
  };
29
+ export type UiEmit = (e: UiEmitEvent) => void;
29
30
  /**
30
31
  * PLANNING-FIRST REDESIGN
31
32
  * ----------------------
32
- * - Never mutate story title/body.
33
- * - Planning-first becomes a run-scoped inject stored in `injects` (scope = run:<run_id>).
34
- * - Trigger is deterministic via config.workflow.genesis_planning:
35
- * - "off" | "first_story_only" | "always"
33
+ * Never mutate story title/body.
34
+ *
35
+ * Deterministic trigger:
36
+ * - config.workflow.genesis_planning:
37
+ * - "off" => never attach directive
38
+ * - "first_story_only"=> only when story_key === "S-0001"
39
+ * - "always" => attach for every run
40
+ *
41
+ * Contract: DB is already initialized before workflow is used:
42
+ * - schema tables exist
43
+ * - repo_state singleton row (id=1) exists
44
+ *
45
+ * IMPORTANT: Do NOT call withTx() in here. The caller owns transaction boundaries.
36
46
  */
37
47
  export type NextAction = {
38
48
  kind: "idle";
@@ -67,20 +77,10 @@ export declare function decideNextAction(db: SqliteDb, config: AstrocodeConfig):
67
77
  export declare function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): {
68
78
  run_id: string;
69
79
  };
70
- /**
71
- * STAGE MOVEMENT (START) — now async so UI injection is deterministic.
72
- */
73
80
  export declare function startStage(db: SqliteDb, runId: string, stageKey: StageKey, meta?: {
74
81
  subagent_type?: string;
75
82
  subagent_session_id?: string;
76
- ui?: WorkflowUi;
77
- }): Promise<void>;
78
- /**
79
- * STAGE CLOSED (RUN COMPLETED) — now async so UI injection is deterministic.
80
- */
81
- export declare function completeRun(db: SqliteDb, runId: string, ui?: WorkflowUi): Promise<void>;
82
- /**
83
- * STAGE CLOSED (RUN FAILED) — now async so UI injection is deterministic.
84
- */
85
- export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, ui?: WorkflowUi): Promise<void>;
83
+ }, emit?: UiEmit): void;
84
+ export declare function completeRun(db: SqliteDb, runId: string, emit?: UiEmit): void;
85
+ export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, emit?: UiEmit): void;
86
86
  export declare function abortRun(db: SqliteDb, runId: string, reason: string): void;