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.
Files changed (54) hide show
  1. package/README.md +243 -11
  2. package/dist/agents/prompts.d.ts +1 -0
  3. package/dist/agents/prompts.js +159 -0
  4. package/dist/agents/registry.js +11 -1
  5. package/dist/config/loader.js +34 -0
  6. package/dist/config/schema.d.ts +7 -1
  7. package/dist/config/schema.js +2 -0
  8. package/dist/hooks/continuation-enforcer.d.ts +9 -1
  9. package/dist/hooks/continuation-enforcer.js +2 -1
  10. package/dist/hooks/inject-provider.d.ts +9 -1
  11. package/dist/hooks/inject-provider.js +2 -1
  12. package/dist/hooks/tool-output-truncator.d.ts +9 -1
  13. package/dist/hooks/tool-output-truncator.js +2 -1
  14. package/dist/index.js +228 -45
  15. package/dist/state/adapters/index.d.ts +4 -2
  16. package/dist/state/adapters/index.js +23 -27
  17. package/dist/state/db.d.ts +6 -8
  18. package/dist/state/db.js +106 -45
  19. package/dist/tools/index.d.ts +13 -3
  20. package/dist/tools/index.js +14 -31
  21. package/dist/tools/init.d.ts +10 -1
  22. package/dist/tools/init.js +73 -18
  23. package/dist/tools/injects.js +90 -26
  24. package/dist/tools/spec.d.ts +0 -1
  25. package/dist/tools/spec.js +4 -1
  26. package/dist/tools/status.d.ts +1 -1
  27. package/dist/tools/status.js +70 -52
  28. package/dist/tools/workflow.js +2 -2
  29. package/dist/ui/inject.d.ts +16 -2
  30. package/dist/ui/inject.js +104 -33
  31. package/dist/workflow/directives.d.ts +2 -0
  32. package/dist/workflow/directives.js +34 -19
  33. package/dist/workflow/state-machine.d.ts +46 -3
  34. package/dist/workflow/state-machine.js +249 -92
  35. package/package.json +1 -1
  36. package/src/agents/prompts.ts +160 -0
  37. package/src/agents/registry.ts +16 -1
  38. package/src/config/loader.ts +39 -4
  39. package/src/config/schema.ts +3 -0
  40. package/src/hooks/continuation-enforcer.ts +9 -2
  41. package/src/hooks/inject-provider.ts +9 -2
  42. package/src/hooks/tool-output-truncator.ts +9 -2
  43. package/src/index.ts +260 -56
  44. package/src/state/adapters/index.ts +21 -26
  45. package/src/state/db.ts +114 -58
  46. package/src/tools/index.ts +29 -31
  47. package/src/tools/init.ts +91 -22
  48. package/src/tools/injects.ts +147 -53
  49. package/src/tools/spec.ts +6 -2
  50. package/src/tools/status.ts +71 -55
  51. package/src/tools/workflow.ts +3 -3
  52. package/src/ui/inject.ts +115 -41
  53. package/src/workflow/directives.ts +103 -75
  54. package/src/workflow/state-machine.ts +327 -109
@@ -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 = ['note', 'policy', 'reminder', 'debug'] as const;
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
- // Check if it's a valid ISO 8601 timestamp with Z suffix
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(`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`);
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
- // Additional validation: ensure it's parseable and represents a valid date
28
- const parsed = new Date(timestamp);
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 (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at)
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, type, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
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
- if (scope) { where.push("scope = ?"); params.push(scope); }
112
- if (type) { where.push("type = ?"); params.push(type); }
113
- const sql = `SELECT inject_id, type, title, scope, priority, created_at, updated_at FROM injects ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
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
- if (scope) { where.push("scope = ?"); params.push(scope); }
151
- const sql = `SELECT inject_id, type, title, scope, priority, updated_at FROM injects WHERE ${where.join(" AND ")} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
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(db: SqliteDb, opts: {
174
- nowIso: string;
175
- scopeAllowlist: string[];
176
- typeAllowlist: string[];
177
- limit?: number;
178
- }): InjectRow[] {
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
- // Build placeholders safely
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 = JSON.parse(scopes_json) as string[];
212
- const types = JSON.parse(types_json) as string[];
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 scopes = JSON.parse(scopes_json) as string[];
238
- const types = JSON.parse(types_json) as string[];
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
- // Check expiration
255
- if (inject.expires_at && inject.expires_at <= now) {
256
- reasons.push("expired");
257
- skippedExpired++;
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: 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
- now,
295
- scopes_considered: scopes,
296
- types_considered: types,
297
- summary: {
298
- total_injects: total,
299
- selected_eligible: selected,
300
- excluded_total: total - selected,
301
- skipped_breakdown: {
302
- expired: skippedExpired,
303
- scope: skippedScope,
304
- type: skippedType,
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
- selected_injects: selectedInjects,
308
- excluded_injects: excludedReasons,
309
- }, null, 2);
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; db: SqliteDb }): ToolDefinition {
11
- const { ctx, config, db } = opts;
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);
@@ -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: SqliteDb }): ToolDefinition {
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 "🔄 Astrocode Status\n\n⚠️ Limited Mode: Database not available\nAstrocode is running with reduced functionality";
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
- const active = getActiveRun(db);
58
+ try {
59
+ const active = getActiveRun(db);
54
60
 
55
- const lines: string[] = [];
56
- lines.push(`# Astrocode Status`);
61
+ const lines: string[] = [];
62
+ lines.push(`# Astrocode Status`);
57
63
 
58
- if (!active) {
59
- lines.push(`- Active run: *(none)*`);
60
- const next = decideNextAction(db, config);
61
- if (next.kind === "idle") lines.push(`- Next: ${next.reason === "no_approved_stories" ? "No approved stories." : "Idle."}`);
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
- if (include_board) {
64
- const counts = db
65
- .prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
66
- .all() as Array<{ state: string; c: number }>;
67
- lines.push(``, `## Story board`);
68
- for (const row of counts) lines.push(`- ${row.state}: ${row.c}`);
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
- return lines.join("\n");
72
- }
78
+ const story = getStory(db, active.story_key);
79
+ const stageRuns = getStageRuns(db, active.run_id);
73
80
 
74
- const story = getStory(db, active.story_key);
75
- const stageRuns = getStageRuns(db, active.run_id);
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
- lines.push(`- Active run: \`${active.run_id}\` ${statusIcon(active.status)} **${active.status}**`);
78
- lines.push(`- Story: \`${active.story_key}\` — ${story?.title ?? "(missing)"} (${story?.state ?? "?"})`);
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
- lines.push(``, `## Pipeline`);
82
- for (const s of stageRuns) {
83
- lines.push(`- ${stageIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
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
- const next = decideNextAction(db, config);
87
- lines.push(``, `## Next`);
88
- lines.push(`- ${next.kind}`);
89
-
90
- if (next.kind === "await_stage_completion") {
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
- if (include_board) {
101
- const counts = db
102
- .prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
103
- .all() as Array<{ state: string; c: number }>;
104
- lines.push(``, `## Story board`);
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
- if (include_recent_events) {
109
- const evs = db
110
- .prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10")
111
- .all() as Array<{ created_at: string; type: string; run_id: string | null; stage_key: string | null }>;
112
- lines.push(``, `## Recent events`);
113
- 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}` : ""}`);
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
  }
@@ -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, 'workflow.proceed', ?, ?)"
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[] = [];