astrocode-workflow 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +6 -0
- package/dist/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +224 -209
- package/dist/ui/inject.d.ts +9 -17
- package/dist/ui/inject.js +79 -102
- package/dist/workflow/state-machine.d.ts +32 -32
- package/dist/workflow/state-machine.js +85 -170
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/state/db.ts +63 -4
- package/src/state/repo-lock.ts +26 -0
- package/src/tools/workflow.ts +159 -142
- package/src/ui/inject.ts +98 -105
- package/src/workflow/state-machine.ts +123 -227
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// src/workflow/state-machine.ts
|
|
2
2
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
3
|
import type { SqliteDb } from "../state/db";
|
|
4
|
-
import { withTx } from "../state/db";
|
|
5
4
|
import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
|
|
6
5
|
import { nowISO } from "../shared/time";
|
|
7
6
|
import { newEventId, newRunId, newStageRunId } from "../state/ids";
|
|
8
7
|
import { warn } from "../shared/log";
|
|
9
8
|
import { sha256Hex } from "../shared/hash";
|
|
10
9
|
import { SCHEMA_VERSION } from "../state/schema";
|
|
11
|
-
import type { ToastOptions } from "../ui/toasts";
|
|
12
|
-
import { injectChatPrompt } from "../ui/inject";
|
|
13
10
|
|
|
14
11
|
export const EVENT_TYPES = {
|
|
15
12
|
RUN_STARTED: "run.started",
|
|
@@ -21,47 +18,6 @@ export const EVENT_TYPES = {
|
|
|
21
18
|
WORKFLOW_PROCEED: "workflow.proceed",
|
|
22
19
|
} as const;
|
|
23
20
|
|
|
24
|
-
/**
|
|
25
|
-
* UI HOOKS
|
|
26
|
-
* --------
|
|
27
|
-
* This workflow module is DB-first. UI emission is optional and happens AFTER the DB tx commits.
|
|
28
|
-
*
|
|
29
|
-
* Contract:
|
|
30
|
-
* - If you pass ui.ctx + ui.sessionId, we will inject a visible chat message deterministically.
|
|
31
|
-
* - If you pass ui.toast, we will also toast (throttling is handled by the toast manager).
|
|
32
|
-
*/
|
|
33
|
-
export type WorkflowUi = {
|
|
34
|
-
ctx: any;
|
|
35
|
-
sessionId: string;
|
|
36
|
-
agentName?: string; // label for injected chat messages
|
|
37
|
-
toast?: (t: ToastOptions) => Promise<void>;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
async function emitUi(ui: WorkflowUi | undefined, text: string, toast?: ToastOptions): Promise<void> {
|
|
41
|
-
if (!ui) return;
|
|
42
|
-
|
|
43
|
-
// Prefer toast (if provided) AND also inject chat (for audit trail / visibility).
|
|
44
|
-
// If you want toast-only, pass a toast function and omit ctx/sessionId.
|
|
45
|
-
if (toast && ui.toast) {
|
|
46
|
-
try {
|
|
47
|
-
await ui.toast(toast);
|
|
48
|
-
} catch {
|
|
49
|
-
// non-fatal
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
await injectChatPrompt({
|
|
55
|
-
ctx: ui.ctx,
|
|
56
|
-
sessionId: ui.sessionId,
|
|
57
|
-
text,
|
|
58
|
-
agent: ui.agentName ?? "Astro",
|
|
59
|
-
});
|
|
60
|
-
} catch {
|
|
61
|
-
// non-fatal (workflow correctness is DB-based)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
21
|
function tableExists(db: SqliteDb, tableName: string): boolean {
|
|
66
22
|
try {
|
|
67
23
|
const row = db
|
|
@@ -73,14 +29,31 @@ function tableExists(db: SqliteDb, tableName: string): boolean {
|
|
|
73
29
|
}
|
|
74
30
|
}
|
|
75
31
|
|
|
32
|
+
export type UiEmitEvent =
|
|
33
|
+
| { kind: "stage_started"; run_id: string; stage_key: StageKey; agent_name?: string }
|
|
34
|
+
| { kind: "run_completed"; run_id: string; story_key: string }
|
|
35
|
+
| { kind: "run_failed"; run_id: string; story_key: string; stage_key: StageKey; error_text: string };
|
|
36
|
+
|
|
37
|
+
export type UiEmit = (e: UiEmitEvent) => void;
|
|
38
|
+
|
|
76
39
|
/**
|
|
77
40
|
* PLANNING-FIRST REDESIGN
|
|
78
41
|
* ----------------------
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
42
|
+
* Never mutate story title/body.
|
|
43
|
+
*
|
|
44
|
+
* Deterministic trigger:
|
|
45
|
+
* - config.workflow.genesis_planning:
|
|
46
|
+
* - "off" => never attach directive
|
|
47
|
+
* - "first_story_only"=> only when story_key === "S-0001"
|
|
48
|
+
* - "always" => attach for every run
|
|
49
|
+
*
|
|
50
|
+
* Contract: DB is already initialized before workflow is used:
|
|
51
|
+
* - schema tables exist
|
|
52
|
+
* - repo_state singleton row (id=1) exists
|
|
53
|
+
*
|
|
54
|
+
* IMPORTANT: Do NOT call withTx() in here. The caller owns transaction boundaries.
|
|
83
55
|
*/
|
|
56
|
+
|
|
84
57
|
export type NextAction =
|
|
85
58
|
| { kind: "idle"; reason: "no_approved_stories" }
|
|
86
59
|
| { kind: "start_run"; story_key: string }
|
|
@@ -141,7 +114,7 @@ export function decideNextAction(db: SqliteDb, config: AstrocodeConfig): NextAct
|
|
|
141
114
|
return { kind: "failed", run_id: activeRun.run_id, stage_key: current.stage_key, error_text: current.error_text ?? "stage failed" };
|
|
142
115
|
}
|
|
143
116
|
|
|
144
|
-
warn("Unexpected stage status in decideNextAction", { status: current.status, stage_key: current.
|
|
117
|
+
warn("Unexpected stage status in decideNextAction", { status: current.status, stage_key: current.stage_key });
|
|
145
118
|
return { kind: "await_stage_completion", run_id: activeRun.run_id, stage_key: current.stage_key, stage_run_id: current.stage_run_id };
|
|
146
119
|
}
|
|
147
120
|
|
|
@@ -215,221 +188,144 @@ function attachRunPlanningDirective(db: SqliteDb, runId: string, story: StoryRow
|
|
|
215
188
|
}
|
|
216
189
|
|
|
217
190
|
export function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): { run_id: string } {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (story.state !== "approved") throw new Error(`Story must be approved to run: ${storyKey} (state=${story.state})`);
|
|
191
|
+
const story = getStory(db, storyKey);
|
|
192
|
+
if (!story) throw new Error(`Story not found: ${storyKey}`);
|
|
193
|
+
if (story.state !== "approved") throw new Error(`Story must be approved to run: ${storyKey} (state=${story.state})`);
|
|
222
194
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
db.prepare(
|
|
228
|
-
"UPDATE stories SET state='in_progress', in_progress=1, locked_by_run_id=?, locked_at=?, updated_at=? WHERE story_key=?"
|
|
229
|
-
).run(run_id, now, now, storyKey);
|
|
195
|
+
const run_id = newRunId();
|
|
196
|
+
const now = nowISO();
|
|
197
|
+
const pipeline = getPipelineFromConfig(config);
|
|
230
198
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
199
|
+
db.prepare(
|
|
200
|
+
"UPDATE stories SET state='in_progress', in_progress=1, locked_by_run_id=?, locked_at=?, updated_at=? WHERE story_key=?"
|
|
201
|
+
).run(run_id, now, now, storyKey);
|
|
234
202
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
pipeline.forEach((stageKey, idx) => {
|
|
239
|
-
insertStage.run(newStageRunId(), run_id, stageKey, idx, now, now);
|
|
240
|
-
});
|
|
203
|
+
db.prepare(
|
|
204
|
+
"INSERT INTO runs (run_id, story_key, status, pipeline_stages_json, current_stage_key, created_at, started_at, updated_at) VALUES (?, ?, 'running', ?, ?, ?, ?, ?)"
|
|
205
|
+
).run(run_id, storyKey, JSON.stringify(pipeline), pipeline[0] ?? null, now, now, now);
|
|
241
206
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
attachRunPlanningDirective(db, run_id, story, pipeline);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
db.prepare(`
|
|
251
|
-
INSERT INTO repo_state (id, schema_version, created_at, updated_at, last_run_id, last_story_key, last_event_at)
|
|
252
|
-
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
253
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
254
|
-
last_run_id=excluded.last_run_id,
|
|
255
|
-
last_story_key=excluded.last_story_key,
|
|
256
|
-
last_event_at=excluded.last_event_at,
|
|
257
|
-
updated_at=excluded.updated_at
|
|
258
|
-
`).run(SCHEMA_VERSION, now, now, run_id, storyKey, now);
|
|
259
|
-
|
|
260
|
-
return { run_id };
|
|
207
|
+
const insertStage = db.prepare(
|
|
208
|
+
"INSERT INTO stage_runs (stage_run_id, run_id, stage_key, stage_index, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'pending', ?, ?)"
|
|
209
|
+
);
|
|
210
|
+
pipeline.forEach((stageKey, idx) => {
|
|
211
|
+
insertStage.run(newStageRunId(), run_id, stageKey, idx, now, now);
|
|
261
212
|
});
|
|
213
|
+
|
|
214
|
+
db.prepare(
|
|
215
|
+
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)"
|
|
216
|
+
).run(newEventId(), run_id, EVENT_TYPES.RUN_STARTED, JSON.stringify({ story_key: storyKey, pipeline }), now);
|
|
217
|
+
|
|
218
|
+
if (shouldAttachPlanningDirective(config, story)) {
|
|
219
|
+
attachRunPlanningDirective(db, run_id, story, pipeline);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
db.prepare(`
|
|
223
|
+
INSERT INTO repo_state (id, schema_version, created_at, updated_at, last_run_id, last_story_key, last_event_at)
|
|
224
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
225
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
226
|
+
last_run_id=excluded.last_run_id,
|
|
227
|
+
last_story_key=excluded.last_story_key,
|
|
228
|
+
last_event_at=excluded.last_event_at,
|
|
229
|
+
updated_at=excluded.updated_at
|
|
230
|
+
`).run(SCHEMA_VERSION, now, now, now, run_id, storyKey, now);
|
|
231
|
+
|
|
232
|
+
return { run_id };
|
|
262
233
|
}
|
|
263
234
|
|
|
264
|
-
|
|
265
|
-
* STAGE MOVEMENT (START) — now async so UI injection is deterministic.
|
|
266
|
-
*/
|
|
267
|
-
export async function startStage(
|
|
235
|
+
export function startStage(
|
|
268
236
|
db: SqliteDb,
|
|
269
237
|
runId: string,
|
|
270
238
|
stageKey: StageKey,
|
|
271
|
-
meta?: { subagent_type?: string; subagent_session_id?: string
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
const now = nowISO();
|
|
276
|
-
|
|
277
|
-
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
278
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
279
|
-
if (run.status !== "running") throw new Error(`Run is not running: ${runId} (status=${run.status})`);
|
|
280
|
-
|
|
281
|
-
const stage = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(runId, stageKey) as StageRunRow | undefined;
|
|
282
|
-
if (!stage) throw new Error(`Stage run not found: ${runId}/${stageKey}`);
|
|
283
|
-
if (stage.status !== "pending") throw new Error(`Stage is not pending: ${stageKey} (status=${stage.status})`);
|
|
239
|
+
meta?: { subagent_type?: string; subagent_session_id?: string },
|
|
240
|
+
emit?: UiEmit
|
|
241
|
+
) {
|
|
242
|
+
const now = nowISO();
|
|
284
243
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
244
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
245
|
+
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
246
|
+
if (run.status !== "running") throw new Error(`Run is not running: ${runId} (status=${run.status})`);
|
|
288
247
|
|
|
289
|
-
|
|
248
|
+
const stage = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(runId, stageKey) as StageRunRow | undefined;
|
|
249
|
+
if (!stage) throw new Error(`Stage run not found: ${runId}/${stageKey}`);
|
|
250
|
+
if (stage.status !== "pending") throw new Error(`Stage is not pending: ${stageKey} (status=${stage.status})`);
|
|
290
251
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
252
|
+
db.prepare(
|
|
253
|
+
"UPDATE stage_runs SET status='running', started_at=?, updated_at=?, subagent_type=COALESCE(?, subagent_type), subagent_session_id=COALESCE(?, subagent_session_id) WHERE stage_run_id=?"
|
|
254
|
+
).run(now, now, meta?.subagent_type ?? null, meta?.subagent_session_id ?? null, stage.stage_run_id);
|
|
294
255
|
|
|
295
|
-
|
|
256
|
+
db.prepare("UPDATE runs SET current_stage_key=?, updated_at=? WHERE run_id=?").run(stageKey, now, runId);
|
|
296
257
|
|
|
297
|
-
|
|
258
|
+
db.prepare(
|
|
259
|
+
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
260
|
+
).run(newEventId(), runId, stageKey, EVENT_TYPES.STAGE_STARTED, JSON.stringify({ subagent_type: meta?.subagent_type ?? null }), now);
|
|
298
261
|
|
|
299
|
-
|
|
300
|
-
now,
|
|
301
|
-
story_key: run.story_key,
|
|
302
|
-
story_title: story?.title ?? "",
|
|
303
|
-
};
|
|
304
|
-
});
|
|
262
|
+
db.prepare("UPDATE repo_state SET last_event_at=?, updated_at=? WHERE id=1").run(now, now);
|
|
305
263
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
meta?.ui,
|
|
309
|
-
[
|
|
310
|
-
`🟦 Stage started`,
|
|
311
|
-
`- Run: \`${runId}\``,
|
|
312
|
-
`- Stage: \`${stageKey}\``,
|
|
313
|
-
`- Story: \`${payload.story_key}\` — ${payload.story_title || "(untitled)"}`,
|
|
314
|
-
].join("\n"),
|
|
315
|
-
{
|
|
316
|
-
title: "Stage started",
|
|
317
|
-
message: `${stageKey} (${payload.story_key})`,
|
|
318
|
-
variant: "info",
|
|
319
|
-
durationMs: 2500,
|
|
320
|
-
}
|
|
321
|
-
);
|
|
264
|
+
// ✅ Explicit wiring point (requested): stage movement
|
|
265
|
+
emit?.({ kind: "stage_started", run_id: runId, stage_key: stageKey, agent_name: meta?.subagent_type });
|
|
322
266
|
}
|
|
323
267
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const now = nowISO();
|
|
330
|
-
|
|
331
|
-
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
332
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
333
|
-
if (run.status !== "running") throw new Error(`Run not running: ${runId} (status=${run.status})`);
|
|
334
|
-
|
|
335
|
-
const stageRuns = getStageRuns(db, runId);
|
|
336
|
-
const incomplete = stageRuns.find((s) => s.status !== "completed" && s.status !== "skipped");
|
|
337
|
-
if (incomplete) throw new Error(`Cannot complete run: stage ${incomplete.stage_key} is ${incomplete.status}`);
|
|
338
|
-
|
|
339
|
-
db.prepare("UPDATE runs SET status='completed', completed_at=?, updated_at=?, current_stage_key=NULL WHERE run_id=?").run(now, now, runId);
|
|
268
|
+
export function completeRun(db: SqliteDb, runId: string, emit?: UiEmit) {
|
|
269
|
+
const now = nowISO();
|
|
270
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
271
|
+
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
272
|
+
if (run.status !== "running") throw new Error(`Run not running: ${runId} (status=${run.status})`);
|
|
340
273
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
274
|
+
const stageRuns = getStageRuns(db, runId);
|
|
275
|
+
const incomplete = stageRuns.find((s) => s.status !== "completed" && s.status !== "skipped");
|
|
276
|
+
if (incomplete) throw new Error(`Cannot complete run: stage ${incomplete.stage_key} is ${incomplete.status}`);
|
|
344
277
|
|
|
345
|
-
|
|
346
|
-
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)"
|
|
347
|
-
).run(newEventId(), runId, EVENT_TYPES.RUN_COMPLETED, JSON.stringify({ story_key: run.story_key }), now);
|
|
278
|
+
db.prepare("UPDATE runs SET status='completed', completed_at=?, updated_at=?, current_stage_key=NULL WHERE run_id=?").run(now, now, runId);
|
|
348
279
|
|
|
349
|
-
|
|
280
|
+
db.prepare(
|
|
281
|
+
"UPDATE stories SET state='done', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?"
|
|
282
|
+
).run(now, run.story_key);
|
|
350
283
|
|
|
351
|
-
|
|
284
|
+
db.prepare(
|
|
285
|
+
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)"
|
|
286
|
+
).run(newEventId(), runId, EVENT_TYPES.RUN_COMPLETED, JSON.stringify({ story_key: run.story_key }), now);
|
|
352
287
|
|
|
353
|
-
|
|
354
|
-
});
|
|
288
|
+
db.prepare("UPDATE repo_state SET last_event_at=?, updated_at=? WHERE id=1").run(now, now);
|
|
355
289
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
[
|
|
359
|
-
`✅ Run completed`,
|
|
360
|
-
`- Run: \`${runId}\``,
|
|
361
|
-
`- Story: \`${payload.story_key}\` — ${payload.story_title || "(untitled)"}`,
|
|
362
|
-
].join("\n"),
|
|
363
|
-
{
|
|
364
|
-
title: "Run completed",
|
|
365
|
-
message: `${payload.story_key} — done`,
|
|
366
|
-
variant: "success",
|
|
367
|
-
durationMs: 3000,
|
|
368
|
-
}
|
|
369
|
-
);
|
|
290
|
+
// ✅ Explicit wiring point (requested): run closed success
|
|
291
|
+
emit?.({ kind: "run_completed", run_id: runId, story_key: run.story_key });
|
|
370
292
|
}
|
|
371
293
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const payload = withTx(db, () => {
|
|
377
|
-
const now = nowISO();
|
|
378
|
-
|
|
379
|
-
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
380
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
381
|
-
|
|
382
|
-
db.prepare("UPDATE runs SET status='failed', error_text=?, updated_at=?, completed_at=? WHERE run_id=?").run(errorText, now, now, runId);
|
|
294
|
+
export function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, emit?: UiEmit) {
|
|
295
|
+
const now = nowISO();
|
|
296
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
297
|
+
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
383
298
|
|
|
384
|
-
|
|
385
|
-
"UPDATE stories SET state='blocked', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?"
|
|
386
|
-
).run(now, run.story_key);
|
|
299
|
+
db.prepare("UPDATE runs SET status='failed', error_text=?, updated_at=?, completed_at=? WHERE run_id=?").run(errorText, now, now, runId);
|
|
387
300
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
301
|
+
db.prepare(
|
|
302
|
+
"UPDATE stories SET state='blocked', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?"
|
|
303
|
+
).run(now, run.story_key);
|
|
391
304
|
|
|
392
|
-
|
|
305
|
+
db.prepare(
|
|
306
|
+
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
307
|
+
).run(newEventId(), runId, stageKey, EVENT_TYPES.RUN_FAILED, JSON.stringify({ error_text: errorText }), now);
|
|
393
308
|
|
|
394
|
-
|
|
309
|
+
db.prepare("UPDATE repo_state SET last_event_at=?, updated_at=? WHERE id=1").run(now, now);
|
|
395
310
|
|
|
396
|
-
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
await emitUi(
|
|
400
|
-
ui,
|
|
401
|
-
[
|
|
402
|
-
`⛔ Run failed`,
|
|
403
|
-
`- Run: \`${runId}\``,
|
|
404
|
-
`- Stage: \`${stageKey}\``,
|
|
405
|
-
`- Story: \`${payload.story_key}\` — ${payload.story_title || "(untitled)"}`,
|
|
406
|
-
`- Error: ${errorText}`,
|
|
407
|
-
].join("\n"),
|
|
408
|
-
{
|
|
409
|
-
title: "Run failed",
|
|
410
|
-
message: `${stageKey}: ${errorText}`,
|
|
411
|
-
variant: "error",
|
|
412
|
-
durationMs: 4500,
|
|
413
|
-
}
|
|
414
|
-
);
|
|
311
|
+
// ✅ Explicit wiring point (requested): run closed failure
|
|
312
|
+
emit?.({ kind: "run_failed", run_id: runId, story_key: run.story_key, stage_key: stageKey, error_text: errorText });
|
|
415
313
|
}
|
|
416
314
|
|
|
417
315
|
export function abortRun(db: SqliteDb, runId: string, reason: string) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
316
|
+
const now = nowISO();
|
|
317
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(runId) as RunRow | undefined;
|
|
318
|
+
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
422
319
|
|
|
423
|
-
|
|
320
|
+
db.prepare("UPDATE runs SET status='aborted', error_text=?, updated_at=?, completed_at=? WHERE run_id=?").run(reason, now, now, runId);
|
|
424
321
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
322
|
+
db.prepare(
|
|
323
|
+
"UPDATE stories SET state='approved', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?"
|
|
324
|
+
).run(now, run.story_key);
|
|
428
325
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
326
|
+
db.prepare(
|
|
327
|
+
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, ?, ?, ?)"
|
|
328
|
+
).run(newEventId(), runId, EVENT_TYPES.RUN_ABORTED, JSON.stringify({ reason }), now);
|
|
432
329
|
|
|
433
|
-
|
|
434
|
-
});
|
|
330
|
+
db.prepare("UPDATE repo_state SET last_event_at=?, updated_at=? WHERE id=1").run(now, now);
|
|
435
331
|
}
|