astrocode-workflow 0.1.58 → 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/README.md +243 -11
- package/dist/agents/prompts.d.ts +1 -0
- package/dist/agents/prompts.js +159 -0
- package/dist/agents/registry.js +11 -1
- package/dist/config/loader.js +34 -0
- package/dist/config/schema.d.ts +7 -1
- package/dist/config/schema.js +2 -0
- package/dist/hooks/continuation-enforcer.d.ts +9 -1
- package/dist/hooks/continuation-enforcer.js +2 -1
- package/dist/hooks/inject-provider.d.ts +9 -1
- package/dist/hooks/inject-provider.js +2 -1
- package/dist/hooks/tool-output-truncator.d.ts +9 -1
- package/dist/hooks/tool-output-truncator.js +2 -1
- package/dist/index.js +228 -45
- package/dist/state/adapters/index.d.ts +4 -2
- package/dist/state/adapters/index.js +23 -27
- package/dist/state/db.d.ts +6 -8
- package/dist/state/db.js +106 -45
- package/dist/tools/index.d.ts +13 -3
- package/dist/tools/index.js +14 -31
- package/dist/tools/init.d.ts +10 -1
- package/dist/tools/init.js +73 -18
- package/dist/tools/injects.js +90 -26
- package/dist/tools/spec.d.ts +0 -1
- package/dist/tools/spec.js +4 -1
- package/dist/tools/status.d.ts +1 -1
- package/dist/tools/status.js +70 -52
- package/dist/tools/workflow.js +2 -2
- package/dist/ui/inject.d.ts +16 -2
- package/dist/ui/inject.js +104 -33
- package/dist/workflow/directives.d.ts +2 -0
- package/dist/workflow/directives.js +34 -19
- package/dist/workflow/state-machine.d.ts +46 -3
- package/dist/workflow/state-machine.js +249 -92
- package/package.json +1 -1
- package/src/agents/prompts.ts +160 -0
- package/src/agents/registry.ts +16 -1
- package/src/config/loader.ts +39 -4
- package/src/config/schema.ts +3 -0
- package/src/hooks/continuation-enforcer.ts +9 -2
- package/src/hooks/inject-provider.ts +9 -2
- package/src/hooks/tool-output-truncator.ts +9 -2
- package/src/index.ts +260 -56
- package/src/state/adapters/index.ts +21 -26
- package/src/state/db.ts +114 -58
- package/src/tools/index.ts +29 -31
- package/src/tools/init.ts +91 -22
- package/src/tools/injects.ts +147 -53
- package/src/tools/spec.ts +6 -2
- package/src/tools/status.ts +71 -55
- package/src/tools/workflow.ts +3 -3
- package/src/ui/inject.ts +115 -41
- package/src/workflow/directives.ts +103 -75
- package/src/workflow/state-machine.ts +327 -109
package/src/tools/injects.ts
CHANGED
|
@@ -5,34 +5,51 @@ import { withTx } from "../state/db";
|
|
|
5
5
|
import { nowISO } from "../shared/time";
|
|
6
6
|
import { sha256Hex } from "../shared/hash";
|
|
7
7
|
|
|
8
|
-
const VALID_INJECT_TYPES = [
|
|
9
|
-
type InjectType = typeof VALID_INJECT_TYPES[number];
|
|
8
|
+
const VALID_INJECT_TYPES = ["note", "policy", "reminder", "debug"] as const;
|
|
9
|
+
type InjectType = (typeof VALID_INJECT_TYPES)[number];
|
|
10
10
|
|
|
11
11
|
function validateInjectType(type: string): InjectType {
|
|
12
12
|
if (!VALID_INJECT_TYPES.includes(type as InjectType)) {
|
|
13
|
-
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(
|
|
13
|
+
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(", ")}`);
|
|
14
14
|
}
|
|
15
15
|
return type as InjectType;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function validateTimestamp(timestamp: string | null): string | null {
|
|
18
|
+
function validateTimestamp(timestamp: string | null | undefined): string | null {
|
|
19
19
|
if (!timestamp) return null;
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// Strict ISO 8601 UTC with Z suffix, sortable as string.
|
|
22
22
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
23
23
|
if (!isoRegex.test(timestamp)) {
|
|
24
|
-
throw new Error(
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`
|
|
26
|
+
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (isNaN(parsed.getTime())) {
|
|
29
|
+
const parsed = Date.parse(timestamp);
|
|
30
|
+
if (!Number.isFinite(parsed)) {
|
|
30
31
|
throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
return timestamp;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function parseJsonStringArray(name: string, raw: string): string[] {
|
|
38
|
+
let v: unknown;
|
|
39
|
+
try {
|
|
40
|
+
v = JSON.parse(raw);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
43
|
+
throw new Error(`${name} must be valid JSON. Parse error: ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
47
|
+
throw new Error(`${name} must be a JSON array of strings`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return v as string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
function newInjectId(): string {
|
|
37
54
|
return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
38
55
|
}
|
|
@@ -58,17 +75,36 @@ export function createAstroInjectPutTool(opts: { ctx: any; config: AstrocodeConf
|
|
|
58
75
|
const now = nowISO();
|
|
59
76
|
const sha = sha256Hex(body_md);
|
|
60
77
|
|
|
61
|
-
// Validate inputs
|
|
62
78
|
const validatedType = validateInjectType(type);
|
|
63
79
|
const validatedExpiresAt = validateTimestamp(expires_at);
|
|
64
80
|
|
|
81
|
+
// Ensure tags_json is at least valid JSON (we do not enforce schema here beyond validity).
|
|
82
|
+
try {
|
|
83
|
+
JSON.parse(tags_json);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
86
|
+
throw new Error(`tags_json must be valid JSON. Parse error: ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
return withTx(db, () => {
|
|
66
90
|
const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id) as any;
|
|
67
91
|
|
|
68
92
|
if (existing) {
|
|
69
|
-
// Use INSERT ... ON CONFLICT for atomic updates
|
|
70
93
|
db.prepare(`
|
|
71
|
-
INSERT INTO injects (
|
|
94
|
+
INSERT INTO injects (
|
|
95
|
+
inject_id,
|
|
96
|
+
type,
|
|
97
|
+
title,
|
|
98
|
+
body_md,
|
|
99
|
+
tags_json,
|
|
100
|
+
scope,
|
|
101
|
+
source,
|
|
102
|
+
priority,
|
|
103
|
+
expires_at,
|
|
104
|
+
sha256,
|
|
105
|
+
created_at,
|
|
106
|
+
updated_at
|
|
107
|
+
)
|
|
72
108
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
109
|
ON CONFLICT(inject_id) DO UPDATE SET
|
|
74
110
|
type=excluded.type,
|
|
@@ -81,7 +117,8 @@ export function createAstroInjectPutTool(opts: { ctx: any; config: AstrocodeConf
|
|
|
81
117
|
expires_at=excluded.expires_at,
|
|
82
118
|
sha256=excluded.sha256,
|
|
83
119
|
updated_at=excluded.updated_at
|
|
84
|
-
`).run(id,
|
|
120
|
+
`).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
|
|
121
|
+
|
|
85
122
|
return `✅ Updated inject ${id}: ${title}`;
|
|
86
123
|
}
|
|
87
124
|
|
|
@@ -108,9 +145,27 @@ export function createAstroInjectListTool(opts: { ctx: any; config: AstrocodeCon
|
|
|
108
145
|
execute: async ({ scope, type, limit }) => {
|
|
109
146
|
const where: string[] = [];
|
|
110
147
|
const params: any[] = [];
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
148
|
+
|
|
149
|
+
if (scope) {
|
|
150
|
+
where.push("scope = ?");
|
|
151
|
+
params.push(scope);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (type) {
|
|
155
|
+
// Keep list tool permissive (debugging), but still prevents obvious garbage if used.
|
|
156
|
+
validateInjectType(type);
|
|
157
|
+
where.push("type = ?");
|
|
158
|
+
params.push(type);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sql = `
|
|
162
|
+
SELECT inject_id, type, title, scope, priority, created_at, updated_at
|
|
163
|
+
FROM injects
|
|
164
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
165
|
+
ORDER BY priority DESC, updated_at DESC
|
|
166
|
+
LIMIT ?
|
|
167
|
+
`;
|
|
168
|
+
|
|
114
169
|
const rows = db.prepare(sql).all(...params, limit) as any[];
|
|
115
170
|
return JSON.stringify(rows, null, 2);
|
|
116
171
|
},
|
|
@@ -147,8 +202,20 @@ export function createAstroInjectSearchTool(opts: { ctx: any; config: AstrocodeC
|
|
|
147
202
|
const like = `%${q}%`;
|
|
148
203
|
const where: string[] = ["(title LIKE ? OR body_md LIKE ? OR tags_json LIKE ?)"];
|
|
149
204
|
const params: any[] = [like, like, like];
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
|
|
206
|
+
if (scope) {
|
|
207
|
+
where.push("scope = ?");
|
|
208
|
+
params.push(scope);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sql = `
|
|
212
|
+
SELECT inject_id, type, title, scope, priority, updated_at
|
|
213
|
+
FROM injects
|
|
214
|
+
WHERE ${where.join(" AND ")}
|
|
215
|
+
ORDER BY priority DESC, updated_at DESC
|
|
216
|
+
LIMIT ?
|
|
217
|
+
`;
|
|
218
|
+
|
|
152
219
|
const rows = db.prepare(sql).all(...params, limit) as any[];
|
|
153
220
|
return JSON.stringify(rows, null, 2);
|
|
154
221
|
},
|
|
@@ -170,15 +237,25 @@ export type InjectRow = {
|
|
|
170
237
|
updated_at: string;
|
|
171
238
|
};
|
|
172
239
|
|
|
173
|
-
export function selectEligibleInjects(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
240
|
+
export function selectEligibleInjects(
|
|
241
|
+
db: SqliteDb,
|
|
242
|
+
opts: {
|
|
243
|
+
nowIso: string;
|
|
244
|
+
scopeAllowlist: string[];
|
|
245
|
+
typeAllowlist: string[];
|
|
246
|
+
limit?: number;
|
|
247
|
+
}
|
|
248
|
+
): InjectRow[] {
|
|
179
249
|
const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
|
|
180
250
|
|
|
181
|
-
|
|
251
|
+
if (!Array.isArray(scopeAllowlist) || scopeAllowlist.length === 0) {
|
|
252
|
+
throw new Error("selectEligibleInjects: scopeAllowlist must be a non-empty array");
|
|
253
|
+
}
|
|
254
|
+
if (!Array.isArray(typeAllowlist) || typeAllowlist.length === 0) {
|
|
255
|
+
throw new Error("selectEligibleInjects: typeAllowlist must be a non-empty array");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build placeholders safely (guaranteed non-empty).
|
|
182
259
|
const scopeQs = scopeAllowlist.map(() => "?").join(", ");
|
|
183
260
|
const typeQs = typeAllowlist.map(() => "?").join(", ");
|
|
184
261
|
|
|
@@ -208,8 +285,11 @@ export function createAstroInjectEligibleTool(opts: { ctx: any; config: Astrocod
|
|
|
208
285
|
},
|
|
209
286
|
execute: async ({ scopes_json, types_json, limit }) => {
|
|
210
287
|
const now = nowISO();
|
|
211
|
-
const scopes =
|
|
212
|
-
const types =
|
|
288
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
289
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
290
|
+
|
|
291
|
+
// Validate types against the known set to keep selection sane.
|
|
292
|
+
for (const t of types) validateInjectType(t);
|
|
213
293
|
|
|
214
294
|
const rows = selectEligibleInjects(db, {
|
|
215
295
|
nowIso: now,
|
|
@@ -234,10 +314,13 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
234
314
|
},
|
|
235
315
|
execute: async ({ scopes_json, types_json }) => {
|
|
236
316
|
const now = nowISO();
|
|
237
|
-
const
|
|
238
|
-
|
|
317
|
+
const nowMs = Date.parse(now);
|
|
318
|
+
|
|
319
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
320
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
321
|
+
|
|
322
|
+
for (const t of types) validateInjectType(t);
|
|
239
323
|
|
|
240
|
-
// Get ALL injects to analyze filtering
|
|
241
324
|
const allInjects = db.prepare("SELECT * FROM injects").all() as any[];
|
|
242
325
|
|
|
243
326
|
let total = allInjects.length;
|
|
@@ -245,25 +328,31 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
245
328
|
let skippedExpired = 0;
|
|
246
329
|
let skippedScope = 0;
|
|
247
330
|
let skippedType = 0;
|
|
331
|
+
let skippedUnparseableExpiresAt = 0;
|
|
332
|
+
|
|
248
333
|
const excludedReasons: any[] = [];
|
|
249
334
|
const selectedInjects: any[] = [];
|
|
250
335
|
|
|
251
336
|
for (const inject of allInjects) {
|
|
252
337
|
const reasons: string[] = [];
|
|
253
338
|
|
|
254
|
-
//
|
|
255
|
-
if (inject.expires_at
|
|
256
|
-
|
|
257
|
-
|
|
339
|
+
// Expiration: parse to ms for correctness across legacy rows.
|
|
340
|
+
if (inject.expires_at) {
|
|
341
|
+
const expMs = Date.parse(String(inject.expires_at));
|
|
342
|
+
if (!Number.isFinite(expMs)) {
|
|
343
|
+
reasons.push("expires_at_unparseable");
|
|
344
|
+
skippedUnparseableExpiresAt++;
|
|
345
|
+
} else if (expMs <= nowMs) {
|
|
346
|
+
reasons.push("expired");
|
|
347
|
+
skippedExpired++;
|
|
348
|
+
}
|
|
258
349
|
}
|
|
259
350
|
|
|
260
|
-
// Check scope
|
|
261
351
|
if (!scopes.includes(inject.scope)) {
|
|
262
352
|
reasons.push("scope");
|
|
263
353
|
skippedScope++;
|
|
264
354
|
}
|
|
265
355
|
|
|
266
|
-
// Check type
|
|
267
356
|
if (!types.includes(inject.type)) {
|
|
268
357
|
reasons.push("type");
|
|
269
358
|
skippedType++;
|
|
@@ -273,7 +362,7 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
273
362
|
excludedReasons.push({
|
|
274
363
|
inject_id: inject.inject_id,
|
|
275
364
|
title: inject.title,
|
|
276
|
-
reasons
|
|
365
|
+
reasons,
|
|
277
366
|
scope: inject.scope,
|
|
278
367
|
type: inject.type,
|
|
279
368
|
expires_at: inject.expires_at,
|
|
@@ -290,23 +379,28 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
290
379
|
}
|
|
291
380
|
}
|
|
292
381
|
|
|
293
|
-
return JSON.stringify(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
382
|
+
return JSON.stringify(
|
|
383
|
+
{
|
|
384
|
+
now,
|
|
385
|
+
scopes_considered: scopes,
|
|
386
|
+
types_considered: types,
|
|
387
|
+
summary: {
|
|
388
|
+
total_injects: total,
|
|
389
|
+
selected_eligible: selected,
|
|
390
|
+
excluded_total: total - selected,
|
|
391
|
+
skipped_breakdown: {
|
|
392
|
+
expired: skippedExpired,
|
|
393
|
+
expires_at_unparseable: skippedUnparseableExpiresAt,
|
|
394
|
+
scope: skippedScope,
|
|
395
|
+
type: skippedType,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
selected_injects: selectedInjects,
|
|
399
|
+
excluded_injects: excludedReasons,
|
|
306
400
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
401
|
+
null,
|
|
402
|
+
2
|
|
403
|
+
);
|
|
310
404
|
},
|
|
311
405
|
});
|
|
312
406
|
}
|
package/src/tools/spec.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
|
|
|
7
7
|
import { nowISO } from "../shared/time";
|
|
8
8
|
import { sha256Hex } from "../shared/hash";
|
|
9
9
|
|
|
10
|
-
export function createAstroSpecGetTool(opts: { ctx: any; config: AstrocodeConfig
|
|
11
|
-
const { ctx, config
|
|
10
|
+
export function createAstroSpecGetTool(opts: { ctx: any; config: AstrocodeConfig }): ToolDefinition {
|
|
11
|
+
const { ctx, config } = opts;
|
|
12
12
|
|
|
13
13
|
return tool({
|
|
14
14
|
description: "Get current project spec stored at .astro/spec.md",
|
|
@@ -34,6 +34,10 @@ export function createAstroSpecSetTool(opts: { ctx: any; config: AstrocodeConfig
|
|
|
34
34
|
spec_md: tool.schema.string().min(1),
|
|
35
35
|
},
|
|
36
36
|
execute: async ({ spec_md }) => {
|
|
37
|
+
if (!db) {
|
|
38
|
+
return "❌ Database not available. Cannot track spec hash. Astrocode is running in limited mode.";
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
const repoRoot = ctx.directory as string;
|
|
38
42
|
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
39
43
|
ensureAstroDirs(paths);
|
package/src/tools/status.ts
CHANGED
|
@@ -35,7 +35,7 @@ function stageIcon(status: string): string {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig; db
|
|
38
|
+
export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig; db?: SqliteDb | null }): ToolDefinition {
|
|
39
39
|
const { config, db } = opts;
|
|
40
40
|
|
|
41
41
|
return tool({
|
|
@@ -45,75 +45,91 @@ export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
45
45
|
include_recent_events: tool.schema.boolean().default(false),
|
|
46
46
|
},
|
|
47
47
|
execute: async ({ include_board, include_recent_events }) => {
|
|
48
|
-
// Check if database is available
|
|
49
48
|
if (!db) {
|
|
50
|
-
return
|
|
49
|
+
return [
|
|
50
|
+
`⚠️ Astrocode not initialized.`,
|
|
51
|
+
``,
|
|
52
|
+
`- Reason: Database not available`,
|
|
53
|
+
``,
|
|
54
|
+
`Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
|
|
55
|
+
].join("\n");
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
try {
|
|
59
|
+
const active = getActiveRun(db);
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
lines.push(`# Astrocode Status`);
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
if (!active) {
|
|
65
|
+
lines.push(`- Active run: *(none)*`);
|
|
66
|
+
const next = decideNextAction(db, config);
|
|
67
|
+
if (next.kind === "idle") lines.push(`- Next: ${next.reason === "no_approved_stories" ? "No approved stories." : "Idle."}`);
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
if (include_board) {
|
|
70
|
+
const counts = db.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state").all() as Array<{ state: string; c: number }>;
|
|
71
|
+
lines.push(``, `## Story board`);
|
|
72
|
+
for (const row of counts) lines.push(`- ${row.state}: ${row.c}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
const story = getStory(db, active.story_key);
|
|
79
|
+
const stageRuns = getStageRuns(db, active.run_id);
|
|
73
80
|
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
lines.push(`- Active run: \`${active.run_id}\` ${statusIcon(active.status)} **${active.status}**`);
|
|
82
|
+
lines.push(`- Story: \`${active.story_key}\` — ${story?.title ?? "(missing)"} (${story?.state ?? "?"})`);
|
|
83
|
+
lines.push(`- Current stage: \`${active.current_stage_key ?? "?"}\``);
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
lines.push(`- Current stage: \`${active.current_stage_key ?? "?"}\``);
|
|
85
|
+
lines.push(``, `## Pipeline`);
|
|
86
|
+
for (const s of stageRuns) lines.push(`- ${stageIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
const next = decideNextAction(db, config);
|
|
89
|
+
lines.push(``, `## Next`, `- ${next.kind}`);
|
|
90
|
+
|
|
91
|
+
if (next.kind === "await_stage_completion") {
|
|
92
|
+
lines.push(`- Awaiting output for stage \`${next.stage_key}\`. When you have it, call **astro_stage_complete** with the stage output text.`);
|
|
93
|
+
} else if (next.kind === "delegate_stage") {
|
|
94
|
+
lines.push(`- Need to run stage \`${next.stage_key}\`. Use **astro_workflow_proceed** (or delegate to the matching stage agent).`);
|
|
95
|
+
} else if (next.kind === "complete_run") {
|
|
96
|
+
lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
|
|
97
|
+
} else if (next.kind === "failed") {
|
|
98
|
+
lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
|
|
99
|
+
}
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
lines.push(`- Awaiting output for stage \`${next.stage_key}\`. When you have it, call **astro_stage_complete** with the stage output text.`);
|
|
92
|
-
} else if (next.kind === "delegate_stage") {
|
|
93
|
-
lines.push(`- Need to run stage \`${next.stage_key}\`. Use **astro_workflow_proceed** (or delegate to the matching stage agent).`);
|
|
94
|
-
} else if (next.kind === "complete_run") {
|
|
95
|
-
lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
|
|
96
|
-
} else if (next.kind === "failed") {
|
|
97
|
-
lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
|
|
98
|
-
}
|
|
101
|
+
if (include_board) {
|
|
102
|
+
const counts = db.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state").all() as Array<{ state: string; c: number }>;
|
|
103
|
+
lines.push(``, `## Story board`);
|
|
104
|
+
for (const row of counts) lines.push(`- ${row.state}: ${row.c}`);
|
|
105
|
+
}
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const row of counts) lines.push(`- ${row.state}: ${row.c}`);
|
|
106
|
-
}
|
|
107
|
+
if (include_recent_events) {
|
|
108
|
+
const evs = db.prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10").all() as Array<{ created_at: string; type: string; run_id: string | null; stage_key: string | null }>;
|
|
109
|
+
lines.push(``, `## Recent events`);
|
|
110
|
+
for (const e of evs) lines.push(`- ${e.created_at} — ${e.type}${e.run_id ? ` (run=${e.run_id})` : ""}${e.stage_key ? ` stage=${e.stage_key}` : ""}`);
|
|
111
|
+
}
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
} catch (e) {
|
|
115
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
116
|
+
if (msg.includes("no such table") || msg.includes("no such column")) {
|
|
117
|
+
return [
|
|
118
|
+
`⚠️ Astrocode not initialized.`,
|
|
119
|
+
``,
|
|
120
|
+
`- Reason: Database present but schema is not initialized or is incompatible`,
|
|
121
|
+
`- Error: ${msg}`,
|
|
122
|
+
``,
|
|
123
|
+
`Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
|
|
124
|
+
].join("\n");
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
`# Astrocode Status`,
|
|
128
|
+
``,
|
|
129
|
+
`⛔ Database error.`,
|
|
130
|
+
`Error: ${msg}`,
|
|
131
|
+
].join("\n");
|
|
114
132
|
}
|
|
115
|
-
|
|
116
|
-
return lines.join("\n");
|
|
117
133
|
},
|
|
118
134
|
});
|
|
119
135
|
}
|
package/src/tools/workflow.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { SqliteDb } from "../state/db";
|
|
|
4
4
|
import { withTx } from "../state/db";
|
|
5
5
|
import type { StageKey } from "../state/types";
|
|
6
6
|
import { buildContextSnapshot } from "../workflow/context";
|
|
7
|
-
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun } from "../workflow/state-machine";
|
|
7
|
+
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
|
|
8
8
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
9
9
|
import { injectChatPrompt } from "../ui/inject";
|
|
10
10
|
import { nowISO } from "../shared/time";
|
|
@@ -383,8 +383,8 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
383
383
|
|
|
384
384
|
// Housekeeping event
|
|
385
385
|
db.prepare(
|
|
386
|
-
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL,
|
|
387
|
-
).run(newEventId(), JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
|
|
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());
|
|
388
388
|
|
|
389
389
|
const active = getActiveRun(db);
|
|
390
390
|
const lines: string[] = [];
|