astrocode-workflow 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/state/db.ts CHANGED
@@ -48,7 +48,32 @@ export function configurePragmas(db: SqliteDb, pragmas: Record<string, any>) {
48
48
  if (pragmas.temp_store) db.pragma(`temp_store = ${pragmas.temp_store}`);
49
49
  }
50
50
 
51
- /** BEGIN IMMEDIATE transaction helper. */
51
+ /**
52
+ * Re-entrant transaction helper.
53
+ *
54
+ * SQLite rejects BEGIN inside BEGIN. We use:
55
+ * - depth=0: BEGIN IMMEDIATE ... COMMIT/ROLLBACK
56
+ * - depth>0: SAVEPOINT sp_n ... RELEASE / ROLLBACK TO + RELEASE
57
+ *
58
+ * This allows callers to safely nest withTx across layers (tools -> workflow -> state machine)
59
+ * without "cannot start a transaction within a transaction".
60
+ */
61
+ const TX_DEPTH = new WeakMap<object, number>();
62
+
63
+ function getDepth(db: SqliteDb): number {
64
+ return TX_DEPTH.get(db as any) ?? 0;
65
+ }
66
+
67
+ function setDepth(db: SqliteDb, depth: number) {
68
+ if (depth <= 0) TX_DEPTH.delete(db as any);
69
+ else TX_DEPTH.set(db as any, depth);
70
+ }
71
+
72
+ function savepointName(depth: number): string {
73
+ return `sp_${depth}`;
74
+ }
75
+
76
+ /** BEGIN IMMEDIATE transaction helper (re-entrant). */
52
77
  export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean }): T {
53
78
  const adapter = createDatabaseAdapter();
54
79
  const available = adapter.isAvailable();
@@ -58,18 +83,52 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
58
83
  return fn();
59
84
  }
60
85
 
61
- db.exec("BEGIN IMMEDIATE");
86
+ const depth = getDepth(db);
87
+
88
+ if (depth === 0) {
89
+ db.exec("BEGIN IMMEDIATE");
90
+ setDepth(db, 1);
91
+ try {
92
+ const out = fn();
93
+ db.exec("COMMIT");
94
+ return out;
95
+ } catch (e) {
96
+ try {
97
+ db.exec("ROLLBACK");
98
+ } catch {
99
+ // ignore
100
+ }
101
+ throw e;
102
+ } finally {
103
+ setDepth(db, 0);
104
+ }
105
+ }
106
+
107
+ // Nested: use SAVEPOINT
108
+ const nextDepth = depth + 1;
109
+ const sp = savepointName(nextDepth);
110
+
111
+ db.exec(`SAVEPOINT ${sp}`);
112
+ setDepth(db, nextDepth);
113
+
62
114
  try {
63
115
  const out = fn();
64
- db.exec("COMMIT");
116
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
65
117
  return out;
66
118
  } catch (e) {
67
119
  try {
68
- db.exec("ROLLBACK");
120
+ db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
121
+ } catch {
122
+ // ignore
123
+ }
124
+ try {
125
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
69
126
  } catch {
70
127
  // ignore
71
128
  }
72
129
  throw e;
130
+ } finally {
131
+ setDepth(db, depth);
73
132
  }
74
133
  }
75
134
 
@@ -1,7 +1,7 @@
1
+ // src/tools/workflow.ts
1
2
  import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
2
3
  import type { AstrocodeConfig } from "../config/schema";
3
4
  import type { SqliteDb } from "../state/db";
4
- import { withTx } from "../state/db";
5
5
  import type { StageKey } from "../state/types";
6
6
  import { buildContextSnapshot } from "../workflow/context";
7
7
  import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
@@ -11,6 +11,7 @@ import { nowISO } from "../shared/time";
11
11
  import { newEventId } from "../state/ids";
12
12
  import { debug } from "../shared/log";
13
13
  import { createToastManager } from "../ui/toasts";
14
+ import type { AgentConfig } from "@opencode-ai/sdk";
14
15
 
15
16
  // Agent name mapping for case-sensitive resolution
16
17
  export const STAGE_TO_AGENT_MAP: Record<string, string> = {
@@ -37,23 +38,17 @@ export function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig, ag
37
38
  // Validate that the agent actually exists in the registry
38
39
  if (agents && !agents[candidate]) {
39
40
  const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
40
- if (warnings) {
41
- warnings.push(warning);
42
- } else {
43
- console.warn(`[Astrocode] ${warning}`);
44
- }
41
+ if (warnings) warnings.push(warning);
42
+ else console.warn(`[Astrocode] ${warning}`);
45
43
  candidate = "General";
46
44
  }
47
45
 
48
46
  // Final guard: ensure General exists, fallback to built-in "general" if not
49
47
  if (agents && !agents[candidate]) {
50
48
  const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
51
- if (warnings) {
52
- warnings.push(finalWarning);
53
- } else {
54
- console.warn(`[Astrocode] ${finalWarning}`);
55
- }
56
- return "general"; // built-in, guaranteed by OpenCode
49
+ if (warnings) warnings.push(finalWarning);
50
+ else console.warn(`[Astrocode] ${finalWarning}`);
51
+ return "general";
57
52
  }
58
53
 
59
54
  return candidate;
@@ -94,10 +89,6 @@ function stageConstraints(stage: StageKey, cfg: AstrocodeConfig): string[] {
94
89
  return common;
95
90
  }
96
91
 
97
- function agentNameForStage(stage: StageKey, cfg: AstrocodeConfig): string {
98
- return cfg.agents.stage_agent_names[stage];
99
- }
100
-
101
92
  function buildDelegationPrompt(opts: {
102
93
  stageDirective: string;
103
94
  run_id: string;
@@ -126,14 +117,11 @@ function buildDelegationPrompt(opts: {
126
117
  `Important: do NOT do any stage work yourself in orchestrator mode.`,
127
118
  ].join("\n").trim();
128
119
 
129
- // Debug log the delegation prompt to troubleshoot agent output issues
130
120
  debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
131
121
 
132
122
  return prompt;
133
123
  }
134
124
 
135
- import { AgentConfig } from "@opencode-ai/sdk";
136
-
137
125
  export function createAstroWorkflowProceedTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb; agents?: Record<string, AgentConfig> }): ToolDefinition {
138
126
  const { ctx, config, db, agents } = opts;
139
127
  const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
@@ -162,71 +150,59 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
162
150
  }
163
151
 
164
152
  if (next.kind === "start_run") {
165
- const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
153
+ // NOTE: createRunForStory owns its own tx (state-machine.ts).
154
+ const { run_id } = createRunForStory(db, config, next.story_key);
166
155
  actions.push(`started run ${run_id} for story ${next.story_key}`);
167
156
 
168
157
  if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
169
158
  await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
170
159
  }
171
160
 
161
+ if (sessionId) {
162
+ await injectChatPrompt({
163
+ ctx,
164
+ sessionId,
165
+ agent: "Astro",
166
+ text: [
167
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_STARTED]`,
168
+ ``,
169
+ `Run started: \`${run_id}\``,
170
+ `Story: \`${next.story_key}\``,
171
+ ``,
172
+ `Next: call **astro_workflow_proceed** again to delegate the first stage.`,
173
+ ].join("\n"),
174
+ });
175
+ actions.push(`injected run started message for ${run_id}`);
176
+ }
177
+
172
178
  if (mode === "step") break;
173
179
  continue;
174
180
  }
175
181
 
176
182
  if (next.kind === "complete_run") {
177
- withTx(db, () => completeRun(db, next.run_id));
183
+ // NOTE: completeRun owns its own tx (state-machine.ts).
184
+ completeRun(db, next.run_id);
178
185
  actions.push(`completed run ${next.run_id}`);
179
186
 
180
187
  if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
181
188
  await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
182
189
  }
183
190
 
184
- // Inject continuation directive for workflow resumption
191
+ // explicit injection on completeRun (requested)
185
192
  if (sessionId) {
186
- const continueDirective = [
187
- `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
188
- ``,
189
- `Run ${next.run_id} completed successfully.`,
190
- ``,
191
- `The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
192
- ``,
193
- `Next actions: Review the generated stories and approve the next one to continue development.`,
194
- ].join("\n");
195
-
196
- await injectChatPrompt({
197
- ctx,
198
- sessionId,
199
- text: continueDirective,
200
- agent: "Astro"
201
- });
202
-
203
- actions.push(`injected continuation directive for completed run ${next.run_id}`);
204
- }
205
-
206
- // Check for next approved story to start
207
- const nextStory = db.prepare(
208
- "SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1"
209
- ).get() as { story_key: string; title: string } | undefined;
210
-
211
- if (nextStory && sessionId) {
212
- const nextDirective = [
213
- `[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
214
- ``,
215
- `The previous run completed successfully. Start the next approved story.`,
216
- ``,
217
- `Next Story: ${nextStory.story_key} — ${nextStory.title}`,
218
- ``,
219
- `Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
220
- ].join("\n");
221
-
222
193
  await injectChatPrompt({
223
194
  ctx,
224
195
  sessionId,
225
- text: nextDirective,
226
- agent: "Astro"
196
+ agent: "Astro",
197
+ text: [
198
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
199
+ ``,
200
+ `Run \`${next.run_id}\` completed.`,
201
+ ``,
202
+ `Next: call **astro_workflow_proceed** (mode=step) to start the next approved story (if any).`,
203
+ ].join("\n"),
227
204
  });
228
-
229
- actions.push(`injected directive to start next story ${nextStory.story_key}`);
205
+ actions.push(`injected run completed message for ${next.run_id}`);
230
206
  }
231
207
 
232
208
  if (mode === "step") break;
@@ -236,62 +212,66 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
236
212
  if (next.kind === "delegate_stage") {
237
213
  const active = getActiveRun(db);
238
214
  if (!active) throw new Error("Invariant: delegate_stage but no active run.");
215
+
239
216
  const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id) as any;
240
217
  const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key) as any;
241
218
 
242
- // Mark stage started + set subagent_type to the stage agent.
243
219
  let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
244
220
 
245
- // Validate agent availability with fallback chain
246
- const systemConfig = config as any;
247
- // Check both the system config agent map (if present) OR the local agents map passed to the tool
248
221
  const agentExists = (name: string) => {
249
- // Check local agents map first (populated from src/index.ts)
250
- if (agents && agents[name]) {
251
- return true;
252
- }
253
- // Check system config agent map
254
- if (systemConfig.agent && systemConfig.agent[name]) {
255
- return true;
256
- }
257
- // For known stage agents, assume they exist (they are system-provided subagents)
258
- const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
259
- if (knownStageAgents.includes(name)) {
260
- return true;
261
- }
262
- return false;
222
+ if (agents && agents[name]) return true;
223
+ const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
224
+ if (knownStageAgents.includes(name)) return true;
225
+ return false;
263
226
  };
264
227
 
228
+ if (!agentExists(agentName)) {
229
+ const originalAgent = agentName;
230
+ console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
231
+ agentName = config.agents?.orchestrator_name || "Astro";
265
232
  if (!agentExists(agentName)) {
266
- const originalAgent = agentName;
267
- console.warn(`[Astrocode] Agent ${agentName} not found in config. Falling back to orchestrator.`);
268
- // First fallback: orchestrator
269
- agentName = config.agents?.orchestrator_name || "Astro";
233
+ console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
234
+ agentName = "General";
270
235
  if (!agentExists(agentName)) {
271
- console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
272
- // Final fallback: General (guaranteed to exist)
273
- agentName = "General";
274
- if (!agentExists(agentName)) {
275
- throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
276
- }
236
+ throw new Error(
237
+ `Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`
238
+ );
277
239
  }
278
240
  }
241
+ }
279
242
 
280
- withTx(db, () => {
281
- startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
243
+ // NOTE: startStage owns its own tx (state-machine.ts).
244
+ startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
282
245
 
283
- // Log delegation observability
284
- if (config.debug?.telemetry?.enabled) {
285
- // eslint-disable-next-line no-console
286
- console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
287
- }
288
- });
246
+ actions.push(`stage started: ${next.stage_key}`);
289
247
 
290
248
  if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
291
249
  await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
292
250
  }
293
251
 
294
- const context = buildContextSnapshot({ db, config, run_id: active.run_id, next_action: `delegate stage ${next.stage_key}` });
252
+ // explicit injection on startStage (requested)
253
+ if (sessionId) {
254
+ await injectChatPrompt({
255
+ ctx,
256
+ sessionId,
257
+ agent: "Astro",
258
+ text: [
259
+ `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
260
+ ``,
261
+ `Run: \`${active.run_id}\``,
262
+ `Stage: \`${next.stage_key}\``,
263
+ `Delegated to: \`${agentName}\``,
264
+ ].join("\n"),
265
+ });
266
+ actions.push(`injected stage started message for ${next.stage_key}`);
267
+ }
268
+
269
+ const context = buildContextSnapshot({
270
+ db,
271
+ config,
272
+ run_id: active.run_id,
273
+ next_action: `delegate stage ${next.stage_key}`,
274
+ });
295
275
 
296
276
  const stageDirective = buildStageDirective({
297
277
  config,
@@ -312,35 +292,38 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
312
292
  stage_agent_name: agentName,
313
293
  });
314
294
 
315
- // Record in continuations as a stage directive (dedupe by hash)
316
- const h = directiveHash(delegatePrompt);
317
- const now = nowISO();
318
- if (sessionId) {
319
- db.prepare(
320
- "INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)"
321
- ).run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
295
+ // Best-effort: continuations table may not exist on older DBs.
296
+ try {
297
+ const h = directiveHash(delegatePrompt);
298
+ const now = nowISO();
299
+ if (sessionId) {
300
+ db.prepare(
301
+ "INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)"
302
+ ).run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
303
+ }
304
+ } catch (e) {
305
+ warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
322
306
  }
323
307
 
324
- // Visible injection so user can see state
325
- if (sessionId) {
326
- await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
308
+ // Visible injection so user can see state
309
+ if (sessionId) {
310
+ await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
327
311
 
328
- // Inject continuation guidance
329
- const continueMessage = [
330
- `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
331
- ``,
332
- `Stage ${next.stage_key} delegated to ${agentName}.`,
333
- ``,
334
- `When ${agentName} completes, call:`,
335
- `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
336
- ``,
337
- `This advances the workflow.`,
338
- ].join("\n");
312
+ const continueMessage = [
313
+ `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
314
+ ``,
315
+ `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
316
+ ``,
317
+ `When \`${agentName}\` completes, call:`,
318
+ `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
319
+ ``,
320
+ `Then run **astro_workflow_proceed** again.`,
321
+ ].join("\n");
339
322
 
340
- await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
341
- }
323
+ await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
324
+ }
342
325
 
343
- actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
326
+ actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
344
327
 
345
328
  // Stop here; subagent needs to run.
346
329
  break;
@@ -348,43 +331,79 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
348
331
 
349
332
  if (next.kind === "await_stage_completion") {
350
333
  actions.push(`await stage completion: ${next.stage_key}`);
351
- // Optionally nudge with a short directive
334
+
352
335
  if (sessionId) {
353
- const context = buildContextSnapshot({ db, config, run_id: next.run_id, next_action: `complete stage ${next.stage_key}` });
336
+ const context = buildContextSnapshot({
337
+ db,
338
+ config,
339
+ run_id: next.run_id,
340
+ next_action: `complete stage ${next.stage_key}`,
341
+ });
342
+
354
343
  const prompt = [
355
344
  `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
356
345
  ``,
357
- `Run ${next.run_id} is waiting for stage ${next.stage_key} output.`,
346
+ `Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
358
347
  `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
359
348
  ``,
360
349
  `Context snapshot:`,
361
350
  context,
362
351
  ].join("\n").trim();
363
- const h = directiveHash(prompt);
364
- const now = nowISO();
365
- db.prepare(
366
- "INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)"
367
- ).run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
352
+
353
+ try {
354
+ const h = directiveHash(prompt);
355
+ const now = nowISO();
356
+ db.prepare(
357
+ "INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)"
358
+ ).run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
359
+ } catch (e) {
360
+ warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
361
+ }
368
362
 
369
363
  await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
370
364
  }
365
+
371
366
  break;
372
367
  }
373
368
 
374
369
  if (next.kind === "failed") {
375
370
  actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
371
+
372
+ if (sessionId) {
373
+ await injectChatPrompt({
374
+ ctx,
375
+ sessionId,
376
+ agent: "Astro",
377
+ text: [
378
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
379
+ ``,
380
+ `Run \`${next.run_id}\` failed at stage \`${next.stage_key}\`.`,
381
+ `Error: ${next.error_text}`,
382
+ ].join("\n"),
383
+ });
384
+ actions.push(`injected run failed message for ${next.run_id}`);
385
+ }
386
+
376
387
  break;
377
388
  }
378
389
 
379
- // safety
380
390
  actions.push(`unhandled next action: ${(next as any).kind}`);
381
391
  break;
382
392
  }
383
393
 
384
- // Housekeeping event
385
- db.prepare(
386
- "INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)"
387
- ).run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
394
+ // Housekeeping event (best-effort)
395
+ try {
396
+ db.prepare(
397
+ "INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)"
398
+ ).run(
399
+ newEventId(),
400
+ EVENT_TYPES.WORKFLOW_PROCEED,
401
+ JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }),
402
+ nowISO()
403
+ );
404
+ } catch (e) {
405
+ warnings.push(`workflow.proceed event insert failed (non-fatal): ${String(e)}`);
406
+ }
388
407
 
389
408
  const active = getActiveRun(db);
390
409
  const lines: string[] = [];