astrocode-workflow 0.1.59 → 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.
@@ -1,5 +1,5 @@
1
+ // src/tools/workflow.ts
1
2
  import { tool } from "@opencode-ai/plugin/tool";
2
- import { withTx } from "../state/db";
3
3
  import { buildContextSnapshot } from "../workflow/context";
4
4
  import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
5
5
  import { buildStageDirective, directiveHash } from "../workflow/directives";
@@ -31,24 +31,20 @@ export function resolveAgentName(stageKey, config, agents, warnings) {
31
31
  // Validate that the agent actually exists in the registry
32
32
  if (agents && !agents[candidate]) {
33
33
  const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
34
- if (warnings) {
34
+ if (warnings)
35
35
  warnings.push(warning);
36
- }
37
- else {
36
+ else
38
37
  console.warn(`[Astrocode] ${warning}`);
39
- }
40
38
  candidate = "General";
41
39
  }
42
40
  // Final guard: ensure General exists, fallback to built-in "general" if not
43
41
  if (agents && !agents[candidate]) {
44
42
  const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
45
- if (warnings) {
43
+ if (warnings)
46
44
  warnings.push(finalWarning);
47
- }
48
- else {
45
+ else
49
46
  console.warn(`[Astrocode] ${finalWarning}`);
50
- }
51
- return "general"; // built-in, guaranteed by OpenCode
47
+ return "general";
52
48
  }
53
49
  return candidate;
54
50
  }
@@ -84,9 +80,6 @@ function stageConstraints(stage, cfg) {
84
80
  }
85
81
  return common;
86
82
  }
87
- function agentNameForStage(stage, cfg) {
88
- return cfg.agents.stage_agent_names[stage];
89
- }
90
83
  function buildDelegationPrompt(opts) {
91
84
  const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
92
85
  const stageUpper = stage_key.toUpperCase();
@@ -108,7 +101,6 @@ function buildDelegationPrompt(opts) {
108
101
  ``,
109
102
  `Important: do NOT do any stage work yourself in orchestrator mode.`,
110
103
  ].join("\n").trim();
111
- // Debug log the delegation prompt to troubleshoot agent output issues
112
104
  debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
113
105
  return prompt;
114
106
  }
@@ -134,59 +126,54 @@ export function createAstroWorkflowProceedTool(opts) {
134
126
  break;
135
127
  }
136
128
  if (next.kind === "start_run") {
137
- const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
129
+ // NOTE: createRunForStory owns its own tx (state-machine.ts).
130
+ const { run_id } = createRunForStory(db, config, next.story_key);
138
131
  actions.push(`started run ${run_id} for story ${next.story_key}`);
139
132
  if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
140
133
  await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
141
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
+ }
142
151
  if (mode === "step")
143
152
  break;
144
153
  continue;
145
154
  }
146
155
  if (next.kind === "complete_run") {
147
- withTx(db, () => completeRun(db, next.run_id));
156
+ // NOTE: completeRun owns its own tx (state-machine.ts).
157
+ completeRun(db, next.run_id);
148
158
  actions.push(`completed run ${next.run_id}`);
149
159
  if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
150
160
  await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
151
161
  }
152
- // Inject continuation directive for workflow resumption
162
+ // explicit injection on completeRun (requested)
153
163
  if (sessionId) {
154
- const continueDirective = [
155
- `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
156
- ``,
157
- `Run ${next.run_id} completed successfully.`,
158
- ``,
159
- `The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
160
- ``,
161
- `Next actions: Review the generated stories and approve the next one to continue development.`,
162
- ].join("\n");
163
- await injectChatPrompt({
164
- ctx,
165
- sessionId,
166
- text: continueDirective,
167
- agent: "Astro"
168
- });
169
- actions.push(`injected continuation directive for completed run ${next.run_id}`);
170
- }
171
- // Check for next approved story to start
172
- const nextStory = db.prepare("SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1").get();
173
- if (nextStory && sessionId) {
174
- const nextDirective = [
175
- `[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
176
- ``,
177
- `The previous run completed successfully. Start the next approved story.`,
178
- ``,
179
- `Next Story: ${nextStory.story_key} — ${nextStory.title}`,
180
- ``,
181
- `Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
182
- ].join("\n");
183
164
  await injectChatPrompt({
184
165
  ctx,
185
166
  sessionId,
186
- text: nextDirective,
187
- agent: "Astro"
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"),
188
175
  });
189
- actions.push(`injected directive to start next story ${nextStory.story_key}`);
176
+ actions.push(`injected run completed message for ${next.run_id}`);
190
177
  }
191
178
  if (mode === "step")
192
179
  break;
@@ -198,53 +185,55 @@ export function createAstroWorkflowProceedTool(opts) {
198
185
  throw new Error("Invariant: delegate_stage but no active run.");
199
186
  const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
200
187
  const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
201
- // Mark stage started + set subagent_type to the stage agent.
202
188
  let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
203
- // Validate agent availability with fallback chain
204
- const systemConfig = config;
205
- // Check both the system config agent map (if present) OR the local agents map passed to the tool
206
189
  const agentExists = (name) => {
207
- // Check local agents map first (populated from src/index.ts)
208
- if (agents && agents[name]) {
190
+ if (agents && agents[name])
209
191
  return true;
210
- }
211
- // Check system config agent map
212
- if (systemConfig.agent && systemConfig.agent[name]) {
213
- return true;
214
- }
215
- // For known stage agents, assume they exist (they are system-provided subagents)
216
- const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
217
- if (knownStageAgents.includes(name)) {
192
+ const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
193
+ if (knownStageAgents.includes(name))
218
194
  return true;
219
- }
220
195
  return false;
221
196
  };
222
197
  if (!agentExists(agentName)) {
223
198
  const originalAgent = agentName;
224
- console.warn(`[Astrocode] Agent ${agentName} not found in config. Falling back to orchestrator.`);
225
- // First fallback: orchestrator
199
+ console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
226
200
  agentName = config.agents?.orchestrator_name || "Astro";
227
201
  if (!agentExists(agentName)) {
228
202
  console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
229
- // Final fallback: General (guaranteed to exist)
230
203
  agentName = "General";
231
204
  if (!agentExists(agentName)) {
232
205
  throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
233
206
  }
234
207
  }
235
208
  }
236
- withTx(db, () => {
237
- startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
238
- // Log delegation observability
239
- if (config.debug?.telemetry?.enabled) {
240
- // eslint-disable-next-line no-console
241
- 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'}`);
242
- }
243
- });
209
+ // 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}`);
244
212
  if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
245
213
  await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
246
214
  }
247
- const context = buildContextSnapshot({ db, config, run_id: active.run_id, next_action: `delegate stage ${next.stage_key}` });
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
+ }
231
+ const context = buildContextSnapshot({
232
+ db,
233
+ config,
234
+ run_id: active.run_id,
235
+ next_action: `delegate stage ${next.stage_key}`,
236
+ });
248
237
  const stageDirective = buildStageDirective({
249
238
  config,
250
239
  stage_key: next.stage_key,
@@ -262,25 +251,29 @@ export function createAstroWorkflowProceedTool(opts) {
262
251
  stage_key: next.stage_key,
263
252
  stage_agent_name: agentName,
264
253
  });
265
- // Record in continuations as a stage directive (dedupe by hash)
266
- const h = directiveHash(delegatePrompt);
267
- const now = nowISO();
268
- if (sessionId) {
269
- 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);
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)}`);
270
264
  }
271
265
  // Visible injection so user can see state
272
266
  if (sessionId) {
273
267
  await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
274
- // Inject continuation guidance
275
268
  const continueMessage = [
276
269
  `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
277
270
  ``,
278
- `Stage ${next.stage_key} delegated to ${agentName}.`,
271
+ `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
279
272
  ``,
280
- `When ${agentName} completes, call:`,
273
+ `When \`${agentName}\` completes, call:`,
281
274
  `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
282
275
  ``,
283
- `This advances the workflow.`,
276
+ `Then run **astro_workflow_proceed** again.`,
284
277
  ].join("\n");
285
278
  await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
286
279
  }
@@ -290,35 +283,62 @@ export function createAstroWorkflowProceedTool(opts) {
290
283
  }
291
284
  if (next.kind === "await_stage_completion") {
292
285
  actions.push(`await stage completion: ${next.stage_key}`);
293
- // Optionally nudge with a short directive
294
286
  if (sessionId) {
295
- const context = buildContextSnapshot({ db, config, run_id: next.run_id, next_action: `complete stage ${next.stage_key}` });
287
+ const context = buildContextSnapshot({
288
+ db,
289
+ config,
290
+ run_id: next.run_id,
291
+ next_action: `complete stage ${next.stage_key}`,
292
+ });
296
293
  const prompt = [
297
294
  `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
298
295
  ``,
299
- `Run ${next.run_id} is waiting for stage ${next.stage_key} output.`,
296
+ `Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
300
297
  `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
301
298
  ``,
302
299
  `Context snapshot:`,
303
300
  context,
304
301
  ].join("\n").trim();
305
- const h = directiveHash(prompt);
306
- const now = nowISO();
307
- 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);
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
+ }
308
310
  await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
309
311
  }
310
312
  break;
311
313
  }
312
314
  if (next.kind === "failed") {
313
315
  actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
316
+ if (sessionId) {
317
+ await injectChatPrompt({
318
+ ctx,
319
+ sessionId,
320
+ 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
+ });
328
+ actions.push(`injected run failed message for ${next.run_id}`);
329
+ }
314
330
  break;
315
331
  }
316
- // safety
317
332
  actions.push(`unhandled next action: ${next.kind}`);
318
333
  break;
319
334
  }
320
- // Housekeeping event
321
- 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());
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)}`);
341
+ }
322
342
  const active = getActiveRun(db);
323
343
  const lines = [];
324
344
  lines.push(`# astro_workflow_proceed`);
@@ -1,6 +1,6 @@
1
1
  export declare function injectChatPrompt(opts: {
2
2
  ctx: any;
3
- sessionId: string;
3
+ sessionId?: string;
4
4
  text: string;
5
5
  agent?: string;
6
6
  }): Promise<void>;
package/dist/ui/inject.js CHANGED
@@ -1,47 +1,88 @@
1
+ // src/ui/inject.ts
1
2
  let isInjecting = false;
2
3
  const injectionQueue = [];
4
+ function sleep(ms) {
5
+ return new Promise((r) => setTimeout(r, ms));
6
+ }
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) {
20
+ throw new Error("API not available (ctx.client.session.prompt)");
21
+ }
22
+ const prefixedText = `[${agent}]\n\n${text}`;
23
+ // Some hosts reject unknown fields; keep body minimal and stable.
24
+ await promptApi({
25
+ path: { id: sessionId },
26
+ body: {
27
+ parts: [{ type: "text", text: prefixedText }],
28
+ },
29
+ });
30
+ }
3
31
  async function processQueue() {
4
- if (isInjecting || injectionQueue.length === 0)
32
+ if (isInjecting)
5
33
  return;
6
- isInjecting = true;
7
- const opts = injectionQueue.shift();
8
- if (!opts) {
9
- isInjecting = false;
34
+ if (injectionQueue.length === 0)
10
35
  return;
11
- }
36
+ isInjecting = true;
12
37
  try {
13
- const { ctx, sessionId, text, agent = "Astro" } = opts;
14
- const prefixedText = `[${agent}]\n\n${text}`;
15
- // Basic validation
16
- if (!sessionId) {
17
- console.warn("[Astrocode] Skipping injection: No sessionId provided");
18
- return;
19
- }
20
- if (!ctx?.client?.session?.prompt) {
21
- console.warn("[Astrocode] Skipping injection: API not available (ctx.client.session.prompt)");
22
- return;
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;
50
+ }
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);
66
+ }
67
+ }
68
+ if (lastErr) {
69
+ // eslint-disable-next-line no-console
70
+ console.warn(`[Astrocode] Injection failed permanently after ${maxAttempts} attempts: ${String(lastErr)}`);
71
+ }
23
72
  }
24
- await ctx.client.session.prompt({
25
- path: { id: sessionId },
26
- body: {
27
- parts: [{ type: "text", text: prefixedText }],
28
- // Pass agent context for systems that support it
29
- agent: agent,
30
- },
31
- });
32
- }
33
- catch (error) {
34
- console.warn(`[Astrocode] Injection failed: ${error}`);
35
73
  }
36
74
  finally {
37
75
  isInjecting = false;
38
- // Process next item immediately
39
- if (injectionQueue.length > 0) {
40
- setImmediate(processQueue);
41
- }
42
76
  }
43
77
  }
44
78
  export async function injectChatPrompt(opts) {
45
- injectionQueue.push(opts);
46
- processQueue();
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 },
85
+ });
86
+ // Fire-and-forget; queue drain is serialized by isInjecting.
87
+ void processQueue();
47
88
  }
@@ -1,6 +1,7 @@
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";
4
5
  export declare const EVENT_TYPES: {
5
6
  readonly RUN_STARTED: "run.started";
6
7
  readonly RUN_COMPLETED: "run.completed";
@@ -10,23 +11,28 @@ export declare const EVENT_TYPES: {
10
11
  readonly STAGE_STARTED: "stage.started";
11
12
  readonly WORKFLOW_PROCEED: "workflow.proceed";
12
13
  };
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>;
28
+ };
13
29
  /**
14
30
  * PLANNING-FIRST REDESIGN
15
31
  * ----------------------
16
- * Old behavior: mutate an approved story into a planning/decomposition instruction.
17
- * New behavior: NEVER mutate story title/body.
18
- *
19
- * Planning-first is a run-scoped directive stored in `injects` (scope = run:<run_id>).
20
- *
21
- * Deterministic trigger (config-driven):
22
- * - config.workflow.genesis_planning:
23
- * - "off" => never attach directive
24
- * - "first_story_only"=> attach only when story_key === "S-0001"
25
- * - "always" => attach for every run
26
- *
27
- * Contract: DB is already initialized before workflow is used:
28
- * - schema tables exist
29
- * - repo_state singleton row (id=1) exists
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"
30
36
  */
31
37
  export type NextAction = {
32
38
  kind: "idle";
@@ -61,10 +67,20 @@ export declare function decideNextAction(db: SqliteDb, config: AstrocodeConfig):
61
67
  export declare function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): {
62
68
  run_id: string;
63
69
  };
70
+ /**
71
+ * STAGE MOVEMENT (START) — now async so UI injection is deterministic.
72
+ */
64
73
  export declare function startStage(db: SqliteDb, runId: string, stageKey: StageKey, meta?: {
65
74
  subagent_type?: string;
66
75
  subagent_session_id?: string;
67
- }): void;
68
- export declare function completeRun(db: SqliteDb, runId: string): void;
69
- export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string): void;
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>;
70
86
  export declare function abortRun(db: SqliteDb, runId: string, reason: string): void;