astrocode-workflow 0.1.59 → 0.2.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/dist/tools/injects.js +90 -26
- package/dist/ui/inject.d.ts +16 -2
- package/dist/ui/inject.js +104 -33
- package/dist/workflow/state-machine.d.ts +33 -17
- package/dist/workflow/state-machine.js +116 -40
- package/package.json +1 -1
- package/src/tools/injects.ts +147 -53
- package/src/ui/inject.ts +115 -41
- package/src/workflow/state-machine.ts +161 -67
package/dist/tools/injects.js
CHANGED
|
@@ -2,28 +2,41 @@ import { tool } from "@opencode-ai/plugin/tool";
|
|
|
2
2
|
import { withTx } from "../state/db";
|
|
3
3
|
import { nowISO } from "../shared/time";
|
|
4
4
|
import { sha256Hex } from "../shared/hash";
|
|
5
|
-
const VALID_INJECT_TYPES = [
|
|
5
|
+
const VALID_INJECT_TYPES = ["note", "policy", "reminder", "debug"];
|
|
6
6
|
function validateInjectType(type) {
|
|
7
7
|
if (!VALID_INJECT_TYPES.includes(type)) {
|
|
8
|
-
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(
|
|
8
|
+
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(", ")}`);
|
|
9
9
|
}
|
|
10
10
|
return type;
|
|
11
11
|
}
|
|
12
12
|
function validateTimestamp(timestamp) {
|
|
13
13
|
if (!timestamp)
|
|
14
14
|
return null;
|
|
15
|
-
//
|
|
15
|
+
// Strict ISO 8601 UTC with Z suffix, sortable as string.
|
|
16
16
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
17
17
|
if (!isoRegex.test(timestamp)) {
|
|
18
18
|
throw new Error(`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (isNaN(parsed.getTime())) {
|
|
20
|
+
const parsed = Date.parse(timestamp);
|
|
21
|
+
if (!Number.isFinite(parsed)) {
|
|
23
22
|
throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
|
|
24
23
|
}
|
|
25
24
|
return timestamp;
|
|
26
25
|
}
|
|
26
|
+
function parseJsonStringArray(name, raw) {
|
|
27
|
+
let v;
|
|
28
|
+
try {
|
|
29
|
+
v = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
33
|
+
throw new Error(`${name} must be valid JSON. Parse error: ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
36
|
+
throw new Error(`${name} must be a JSON array of strings`);
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
27
40
|
function newInjectId() {
|
|
28
41
|
return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
29
42
|
}
|
|
@@ -46,15 +59,34 @@ export function createAstroInjectPutTool(opts) {
|
|
|
46
59
|
const id = inject_id ?? newInjectId();
|
|
47
60
|
const now = nowISO();
|
|
48
61
|
const sha = sha256Hex(body_md);
|
|
49
|
-
// Validate inputs
|
|
50
62
|
const validatedType = validateInjectType(type);
|
|
51
63
|
const validatedExpiresAt = validateTimestamp(expires_at);
|
|
64
|
+
// Ensure tags_json is at least valid JSON (we do not enforce schema here beyond validity).
|
|
65
|
+
try {
|
|
66
|
+
JSON.parse(tags_json);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
70
|
+
throw new Error(`tags_json must be valid JSON. Parse error: ${msg}`);
|
|
71
|
+
}
|
|
52
72
|
return withTx(db, () => {
|
|
53
73
|
const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id);
|
|
54
74
|
if (existing) {
|
|
55
|
-
// Use INSERT ... ON CONFLICT for atomic updates
|
|
56
75
|
db.prepare(`
|
|
57
|
-
INSERT INTO injects (
|
|
76
|
+
INSERT INTO injects (
|
|
77
|
+
inject_id,
|
|
78
|
+
type,
|
|
79
|
+
title,
|
|
80
|
+
body_md,
|
|
81
|
+
tags_json,
|
|
82
|
+
scope,
|
|
83
|
+
source,
|
|
84
|
+
priority,
|
|
85
|
+
expires_at,
|
|
86
|
+
sha256,
|
|
87
|
+
created_at,
|
|
88
|
+
updated_at
|
|
89
|
+
)
|
|
58
90
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
91
|
ON CONFLICT(inject_id) DO UPDATE SET
|
|
60
92
|
type=excluded.type,
|
|
@@ -67,7 +99,7 @@ export function createAstroInjectPutTool(opts) {
|
|
|
67
99
|
expires_at=excluded.expires_at,
|
|
68
100
|
sha256=excluded.sha256,
|
|
69
101
|
updated_at=excluded.updated_at
|
|
70
|
-
`).run(id,
|
|
102
|
+
`).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
|
|
71
103
|
return `✅ Updated inject ${id}: ${title}`;
|
|
72
104
|
}
|
|
73
105
|
db.prepare("INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
|
|
@@ -93,10 +125,18 @@ export function createAstroInjectListTool(opts) {
|
|
|
93
125
|
params.push(scope);
|
|
94
126
|
}
|
|
95
127
|
if (type) {
|
|
128
|
+
// Keep list tool permissive (debugging), but still prevents obvious garbage if used.
|
|
129
|
+
validateInjectType(type);
|
|
96
130
|
where.push("type = ?");
|
|
97
131
|
params.push(type);
|
|
98
132
|
}
|
|
99
|
-
const sql = `
|
|
133
|
+
const sql = `
|
|
134
|
+
SELECT inject_id, type, title, scope, priority, created_at, updated_at
|
|
135
|
+
FROM injects
|
|
136
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
137
|
+
ORDER BY priority DESC, updated_at DESC
|
|
138
|
+
LIMIT ?
|
|
139
|
+
`;
|
|
100
140
|
const rows = db.prepare(sql).all(...params, limit);
|
|
101
141
|
return JSON.stringify(rows, null, 2);
|
|
102
142
|
},
|
|
@@ -134,7 +174,13 @@ export function createAstroInjectSearchTool(opts) {
|
|
|
134
174
|
where.push("scope = ?");
|
|
135
175
|
params.push(scope);
|
|
136
176
|
}
|
|
137
|
-
const sql = `
|
|
177
|
+
const sql = `
|
|
178
|
+
SELECT inject_id, type, title, scope, priority, updated_at
|
|
179
|
+
FROM injects
|
|
180
|
+
WHERE ${where.join(" AND ")}
|
|
181
|
+
ORDER BY priority DESC, updated_at DESC
|
|
182
|
+
LIMIT ?
|
|
183
|
+
`;
|
|
138
184
|
const rows = db.prepare(sql).all(...params, limit);
|
|
139
185
|
return JSON.stringify(rows, null, 2);
|
|
140
186
|
},
|
|
@@ -142,7 +188,13 @@ export function createAstroInjectSearchTool(opts) {
|
|
|
142
188
|
}
|
|
143
189
|
export function selectEligibleInjects(db, opts) {
|
|
144
190
|
const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
|
|
145
|
-
|
|
191
|
+
if (!Array.isArray(scopeAllowlist) || scopeAllowlist.length === 0) {
|
|
192
|
+
throw new Error("selectEligibleInjects: scopeAllowlist must be a non-empty array");
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(typeAllowlist) || typeAllowlist.length === 0) {
|
|
195
|
+
throw new Error("selectEligibleInjects: typeAllowlist must be a non-empty array");
|
|
196
|
+
}
|
|
197
|
+
// Build placeholders safely (guaranteed non-empty).
|
|
146
198
|
const scopeQs = scopeAllowlist.map(() => "?").join(", ");
|
|
147
199
|
const typeQs = typeAllowlist.map(() => "?").join(", ");
|
|
148
200
|
const sql = `
|
|
@@ -168,8 +220,11 @@ export function createAstroInjectEligibleTool(opts) {
|
|
|
168
220
|
},
|
|
169
221
|
execute: async ({ scopes_json, types_json, limit }) => {
|
|
170
222
|
const now = nowISO();
|
|
171
|
-
const scopes =
|
|
172
|
-
const types =
|
|
223
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
224
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
225
|
+
// Validate types against the known set to keep selection sane.
|
|
226
|
+
for (const t of types)
|
|
227
|
+
validateInjectType(t);
|
|
173
228
|
const rows = selectEligibleInjects(db, {
|
|
174
229
|
nowIso: now,
|
|
175
230
|
scopeAllowlist: scopes,
|
|
@@ -190,30 +245,38 @@ export function createAstroInjectDebugDueTool(opts) {
|
|
|
190
245
|
},
|
|
191
246
|
execute: async ({ scopes_json, types_json }) => {
|
|
192
247
|
const now = nowISO();
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
248
|
+
const nowMs = Date.parse(now);
|
|
249
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
250
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
251
|
+
for (const t of types)
|
|
252
|
+
validateInjectType(t);
|
|
196
253
|
const allInjects = db.prepare("SELECT * FROM injects").all();
|
|
197
254
|
let total = allInjects.length;
|
|
198
255
|
let selected = 0;
|
|
199
256
|
let skippedExpired = 0;
|
|
200
257
|
let skippedScope = 0;
|
|
201
258
|
let skippedType = 0;
|
|
259
|
+
let skippedUnparseableExpiresAt = 0;
|
|
202
260
|
const excludedReasons = [];
|
|
203
261
|
const selectedInjects = [];
|
|
204
262
|
for (const inject of allInjects) {
|
|
205
263
|
const reasons = [];
|
|
206
|
-
//
|
|
207
|
-
if (inject.expires_at
|
|
208
|
-
|
|
209
|
-
|
|
264
|
+
// Expiration: parse to ms for correctness across legacy rows.
|
|
265
|
+
if (inject.expires_at) {
|
|
266
|
+
const expMs = Date.parse(String(inject.expires_at));
|
|
267
|
+
if (!Number.isFinite(expMs)) {
|
|
268
|
+
reasons.push("expires_at_unparseable");
|
|
269
|
+
skippedUnparseableExpiresAt++;
|
|
270
|
+
}
|
|
271
|
+
else if (expMs <= nowMs) {
|
|
272
|
+
reasons.push("expired");
|
|
273
|
+
skippedExpired++;
|
|
274
|
+
}
|
|
210
275
|
}
|
|
211
|
-
// Check scope
|
|
212
276
|
if (!scopes.includes(inject.scope)) {
|
|
213
277
|
reasons.push("scope");
|
|
214
278
|
skippedScope++;
|
|
215
279
|
}
|
|
216
|
-
// Check type
|
|
217
280
|
if (!types.includes(inject.type)) {
|
|
218
281
|
reasons.push("type");
|
|
219
282
|
skippedType++;
|
|
@@ -222,7 +285,7 @@ export function createAstroInjectDebugDueTool(opts) {
|
|
|
222
285
|
excludedReasons.push({
|
|
223
286
|
inject_id: inject.inject_id,
|
|
224
287
|
title: inject.title,
|
|
225
|
-
reasons
|
|
288
|
+
reasons,
|
|
226
289
|
scope: inject.scope,
|
|
227
290
|
type: inject.type,
|
|
228
291
|
expires_at: inject.expires_at,
|
|
@@ -249,9 +312,10 @@ export function createAstroInjectDebugDueTool(opts) {
|
|
|
249
312
|
excluded_total: total - selected,
|
|
250
313
|
skipped_breakdown: {
|
|
251
314
|
expired: skippedExpired,
|
|
315
|
+
expires_at_unparseable: skippedUnparseableExpiresAt,
|
|
252
316
|
scope: skippedScope,
|
|
253
317
|
type: skippedType,
|
|
254
|
-
}
|
|
318
|
+
},
|
|
255
319
|
},
|
|
256
320
|
selected_injects: selectedInjects,
|
|
257
321
|
excluded_injects: excludedReasons,
|
package/dist/ui/inject.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
type QueueItem = {
|
|
2
2
|
ctx: any;
|
|
3
3
|
sessionId: string;
|
|
4
4
|
text: string;
|
|
5
5
|
agent?: string;
|
|
6
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Enqueue an injection and ensure the worker is running.
|
|
9
|
+
* Does NOT wait for delivery — use `flushChatPrompts()` to wait.
|
|
10
|
+
*/
|
|
11
|
+
export declare function enqueueChatPrompt(opts: QueueItem): void;
|
|
12
|
+
/**
|
|
13
|
+
* Wait until all queued injections have been processed (sent or exhausted retries).
|
|
14
|
+
*/
|
|
15
|
+
export declare function flushChatPrompts(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Deterministic helper: enqueue + flush (recommended for stage boundaries).
|
|
18
|
+
*/
|
|
19
|
+
export declare function injectChatPrompt(opts: QueueItem): Promise<void>;
|
|
20
|
+
export {};
|
package/dist/ui/inject.js
CHANGED
|
@@ -1,47 +1,118 @@
|
|
|
1
|
-
|
|
1
|
+
// src/ui/inject.ts
|
|
2
|
+
//
|
|
3
|
+
// Deterministic chat injection:
|
|
4
|
+
// - Always enqueue
|
|
5
|
+
// - Process sequentially (per-process single worker)
|
|
6
|
+
// - Retries with backoff
|
|
7
|
+
// - flush() lets callers wait until injections are actually sent
|
|
8
|
+
//
|
|
9
|
+
// IMPORTANT: Callers who need reliability must `await injectChatPrompt(...)`
|
|
10
|
+
// or `await flushChatPrompts()` after enqueueing.
|
|
2
11
|
const injectionQueue = [];
|
|
3
|
-
|
|
4
|
-
|
|
12
|
+
let workerRunning = false;
|
|
13
|
+
// Used to let callers await "queue drained"
|
|
14
|
+
let drainWaiters = [];
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
function resolveDrainWaitersIfIdle() {
|
|
19
|
+
if (workerRunning)
|
|
5
20
|
return;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
21
|
+
if (injectionQueue.length !== 0)
|
|
22
|
+
return;
|
|
23
|
+
const waiters = drainWaiters;
|
|
24
|
+
drainWaiters = [];
|
|
25
|
+
for (const w of waiters)
|
|
26
|
+
w();
|
|
27
|
+
}
|
|
28
|
+
function getPromptApi(ctx) {
|
|
29
|
+
const fn = ctx?.client?.session?.prompt;
|
|
30
|
+
return typeof fn === "function" ? fn.bind(ctx.client.session) : null;
|
|
31
|
+
}
|
|
32
|
+
async function sendWithRetries(opts) {
|
|
33
|
+
const { ctx, sessionId, text } = opts;
|
|
34
|
+
const agent = opts.agent ?? "Astro";
|
|
35
|
+
const prefixedText = `[${agent}]\n\n${text}`;
|
|
36
|
+
if (!sessionId) {
|
|
37
|
+
console.warn("[Astrocode] Injection skipped: missing sessionId");
|
|
10
38
|
return;
|
|
11
39
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
40
|
+
const prompt = getPromptApi(ctx);
|
|
41
|
+
if (!prompt) {
|
|
42
|
+
console.warn("[Astrocode] Injection skipped: ctx.client.session.prompt unavailable");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const maxAttempts = 3;
|
|
46
|
+
let attempt = 0;
|
|
47
|
+
while (attempt < maxAttempts) {
|
|
48
|
+
attempt += 1;
|
|
49
|
+
try {
|
|
50
|
+
await prompt({
|
|
51
|
+
path: { id: sessionId },
|
|
52
|
+
body: {
|
|
53
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
54
|
+
agent,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
18
57
|
return;
|
|
19
58
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
const isLast = attempt >= maxAttempts;
|
|
62
|
+
if (isLast) {
|
|
63
|
+
console.warn(`[Astrocode] Injection failed (final): ${msg}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Exponential backoff + jitter
|
|
67
|
+
const base = 150 * Math.pow(2, attempt - 1); // 150, 300, 600
|
|
68
|
+
const jitter = Math.floor(Math.random() * 120);
|
|
69
|
+
await sleep(base + jitter);
|
|
23
70
|
}
|
|
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
71
|
}
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
}
|
|
73
|
+
async function runWorkerLoop() {
|
|
74
|
+
if (workerRunning)
|
|
75
|
+
return;
|
|
76
|
+
workerRunning = true;
|
|
77
|
+
try {
|
|
78
|
+
// Drain sequentially to preserve ordering
|
|
79
|
+
while (injectionQueue.length > 0) {
|
|
80
|
+
const item = injectionQueue.shift();
|
|
81
|
+
if (!item)
|
|
82
|
+
continue;
|
|
83
|
+
await sendWithRetries(item);
|
|
84
|
+
}
|
|
35
85
|
}
|
|
36
86
|
finally {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (injectionQueue.length > 0) {
|
|
40
|
-
setImmediate(processQueue);
|
|
41
|
-
}
|
|
87
|
+
workerRunning = false;
|
|
88
|
+
resolveDrainWaitersIfIdle();
|
|
42
89
|
}
|
|
43
90
|
}
|
|
44
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Enqueue an injection and ensure the worker is running.
|
|
93
|
+
* Does NOT wait for delivery — use `flushChatPrompts()` to wait.
|
|
94
|
+
*/
|
|
95
|
+
export function enqueueChatPrompt(opts) {
|
|
45
96
|
injectionQueue.push(opts);
|
|
46
|
-
|
|
97
|
+
// Kick worker
|
|
98
|
+
void runWorkerLoop();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Wait until all queued injections have been processed (sent or exhausted retries).
|
|
102
|
+
*/
|
|
103
|
+
export function flushChatPrompts() {
|
|
104
|
+
if (!workerRunning && injectionQueue.length === 0)
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
drainWaiters.push(resolve);
|
|
108
|
+
// Ensure worker is running (in case someone enqueued without kick)
|
|
109
|
+
void runWorkerLoop();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Deterministic helper: enqueue + flush (recommended for stage boundaries).
|
|
114
|
+
*/
|
|
115
|
+
export async function injectChatPrompt(opts) {
|
|
116
|
+
enqueueChatPrompt(opts);
|
|
117
|
+
await flushChatPrompts();
|
|
47
118
|
}
|
|
@@ -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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|