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
@@ -2,9 +2,19 @@ import type { ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
3
  import type { SqliteDb } from "../state/db";
4
4
  import { AgentConfig } from "@opencode-ai/sdk";
5
- export declare function createAstroTools(opts: {
5
+ type RuntimeState = {
6
+ db: SqliteDb | null;
7
+ limitedMode: boolean;
8
+ limitedModeReason: null | {
9
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
10
+ details: any;
11
+ };
12
+ };
13
+ type CreateAstroToolsOptions = {
6
14
  ctx: any;
7
15
  config: AstrocodeConfig;
8
- db: SqliteDb;
9
16
  agents?: Record<string, AgentConfig>;
10
- }): Record<string, ToolDefinition>;
17
+ runtime: RuntimeState;
18
+ };
19
+ export declare function createAstroTools(opts: CreateAstroToolsOptions): Record<string, ToolDefinition>;
20
+ export {};
@@ -9,17 +9,21 @@ import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArt
9
9
  import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool, createAstroInjectEligibleTool, createAstroInjectDebugDueTool } from "./injects";
10
10
  import { createAstroRepairTool } from "./repair";
11
11
  export function createAstroTools(opts) {
12
- const { ctx, config, db, agents } = opts;
13
- const hasDatabase = !!db;
12
+ const { ctx, config, agents, runtime } = opts;
13
+ const { db } = runtime;
14
+ const hasDatabase = db !== null; // Source of truth: DB availability
14
15
  const tools = {};
15
- // Always available tools
16
- tools.astro_status = createAstroStatusTool({ ctx, config, db });
17
- // Always available tools (work without database)
18
- tools.astro_status = createAstroStatusTool({ ctx, config, db });
19
- tools.astro_spec_get = createAstroSpecGetTool({ ctx, config, db });
16
+ // Always available tools (work without database - guaranteed DB-independent)
17
+ tools.astro_status = createAstroStatusTool({ ctx, config });
18
+ tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
19
+ // Recovery tool - available even in limited mode to allow DB initialization
20
+ tools.astro_init = createAstroInitTool({ ctx, config, runtime });
20
21
  // Database-dependent tools
21
22
  if (hasDatabase) {
22
- tools.astro_init = createAstroInitTool({ ctx, config, db });
23
+ // Ensure agents are available for workflow tools that require them
24
+ if (!agents) {
25
+ throw new Error("astro_workflow_proceed requires agents to be provided in normal mode.");
26
+ }
23
27
  tools.astro_story_queue = createAstroStoryQueueTool({ ctx, config, db });
24
28
  tools.astro_story_approve = createAstroStoryApproveTool({ ctx, config, db });
25
29
  tools.astro_story_board = createAstroStoryBoardTool({ ctx, config, db });
@@ -43,29 +47,6 @@ export function createAstroTools(opts) {
43
47
  tools.astro_inject_debug_due = createAstroInjectDebugDueTool({ ctx, config, db });
44
48
  tools.astro_repair = createAstroRepairTool({ ctx, config, db });
45
49
  }
46
- else {
47
- // Limited mode tools - provide helpful messages instead of failing
48
- tools.astro_init = {
49
- description: "Initialize Astrocode (requires database - currently unavailable)",
50
- args: {},
51
- execute: async () => "❌ Database not available. Astrocode is running in limited mode."
52
- };
53
- tools.astro_story_queue = {
54
- description: "Queue a story (requires database - currently unavailable)",
55
- args: {},
56
- execute: async () => "❌ Database not available. Astrocode is running in limited mode."
57
- };
58
- tools.astro_spec_set = {
59
- description: "Set project spec (requires database for hash tracking - currently unavailable)",
60
- args: {},
61
- execute: async () => "❌ Database not available. Astrocode is running in limited mode."
62
- };
63
- tools.astro_workflow_proceed = {
64
- description: "Advance workflow (requires database - currently unavailable)",
65
- args: {},
66
- execute: async () => "❌ Database not available. Astrocode is running in limited mode."
67
- };
68
- }
69
50
  // Create aliases for backward compatibility
70
51
  const aliases = [
71
52
  ["_astro_init", "astro_init"],
@@ -90,6 +71,8 @@ export function createAstroTools(opts) {
90
71
  ["_astro_inject_list", "astro_inject_list"],
91
72
  ["_astro_inject_search", "astro_inject_search"],
92
73
  ["_astro_inject_get", "astro_inject_get"],
74
+ ["_astro_inject_eligible", "astro_inject_eligible"],
75
+ ["_astro_inject_debug_due", "astro_inject_debug_due"],
93
76
  ["_astro_repair", "astro_repair"],
94
77
  ];
95
78
  // Only add aliases for tools that exist
@@ -1,8 +1,17 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
3
  import type { SqliteDb } from "../state/db";
4
+ type RuntimeState = {
5
+ db: SqliteDb | null;
6
+ limitedMode: boolean;
7
+ limitedModeReason: null | {
8
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
9
+ details: any;
10
+ };
11
+ };
4
12
  export declare function createAstroInitTool(opts: {
5
13
  ctx: any;
6
14
  config: AstrocodeConfig;
7
- db: SqliteDb;
15
+ runtime: RuntimeState;
8
16
  }): ToolDefinition;
17
+ export {};
@@ -1,12 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { tool } from "@opencode-ai/plugin/tool";
4
- import { ensureSchema } from "../state/db";
4
+ import { ensureSchema, openSqlite, configurePragmas } from "../state/db";
5
5
  import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
6
6
  import { nowISO } from "../shared/time";
7
7
  import { sha256Hex } from "../shared/hash";
8
8
  export function createAstroInitTool(opts) {
9
- const { ctx, config, db } = opts;
9
+ const { ctx, config, runtime } = opts;
10
10
  return tool({
11
11
  description: "Initialize Astrocode vNext in this repo: create .astro directories, ensure SQLite schema, and create a placeholder spec if missing. Idempotent.",
12
12
  args: {
@@ -17,25 +17,80 @@ export function createAstroInitTool(opts) {
17
17
  const repoRoot = ctx.directory;
18
18
  const paths = getAstroPaths(repoRoot, config.db.path);
19
19
  ensureAstroDirs(paths);
20
- ensureSchema(db, { allowAutoMigrate: config.db.allow_auto_migrate, failOnDowngrade: config.db.fail_on_downgrade });
21
- if (ensure_spec) {
22
- if (!fs.existsSync(paths.specPath)) {
23
- fs.writeFileSync(paths.specPath, spec_placeholder);
24
- const specHash = sha256Hex(spec_placeholder);
20
+ const hadDbAlready = !!runtime.db;
21
+ let db = runtime.db;
22
+ let publishedToRuntime = false;
23
+ try {
24
+ if (!db) {
25
+ try {
26
+ db = openSqlite(paths.dbPath, { busyTimeoutMs: config.db.busy_timeout_ms });
27
+ configurePragmas(db, config.db.pragmas);
28
+ }
29
+ catch (e) {
30
+ const msg = e instanceof Error ? e.message : String(e);
31
+ throw new Error(`❌ Failed to open database at ${paths.dbPath}: ${msg}. Install a SQLite driver (better-sqlite3/bun:sqlite) and check file permissions.`);
32
+ }
33
+ }
34
+ // Source-of-truth for schema + repo_state invariants.
35
+ ensureSchema(db, { allowAutoMigrate: config.db.allow_auto_migrate });
36
+ // Postcondition: repo_state must exist after ensureSchema.
37
+ try {
38
+ db.prepare("SELECT schema_version FROM repo_state WHERE id = 1").get();
39
+ }
40
+ catch (e) {
41
+ const msg = e instanceof Error ? e.message : String(e);
42
+ throw new Error(`❌ Schema initialization incomplete: repo_state missing/unreadable after ensureSchema (${msg})`);
43
+ }
44
+ if (ensure_spec) {
45
+ if (!fs.existsSync(paths.specPath)) {
46
+ fs.writeFileSync(paths.specPath, spec_placeholder);
47
+ }
48
+ const content = fs.readFileSync(paths.specPath, "utf8");
49
+ const specHash = sha256Hex(content);
25
50
  const now = nowISO();
26
- db.prepare("UPDATE repo_state SET spec_hash_after=?, updated_at=? WHERE id=1").run(specHash, now);
51
+ // Update hash; fail with a clear error if schema is mismatched.
52
+ try {
53
+ db.prepare(`
54
+ UPDATE repo_state
55
+ SET spec_hash_after = ?, updated_at = ?
56
+ WHERE id = 1
57
+ `).run(specHash, now);
58
+ }
59
+ catch (e) {
60
+ const msg = e instanceof Error ? e.message : String(e);
61
+ throw new Error(`❌ Failed to update repo_state spec hash (schema mismatch?): ${msg}`);
62
+ }
63
+ }
64
+ // Best-effort: if we recovered DB in-process, publish it so the harness can use it immediately.
65
+ // (If your harness reads runtime at creation-time only, this still helps future tool calls.)
66
+ if (!hadDbAlready && db) {
67
+ runtime.db = db;
68
+ runtime.limitedMode = false;
69
+ runtime.limitedModeReason = null;
70
+ publishedToRuntime = true;
71
+ }
72
+ const stat = db.prepare("SELECT schema_version, created_at, updated_at FROM repo_state WHERE id=1").get();
73
+ return [
74
+ `✅ Astrocode initialized.`,
75
+ ``,
76
+ `- Repo: ${repoRoot}`,
77
+ `- DB: ${path.relative(repoRoot, paths.dbPath)} (schema_version=${stat?.schema_version ?? "?"})`,
78
+ `- Spec: ${path.relative(repoRoot, paths.specPath)}`,
79
+ ``,
80
+ publishedToRuntime
81
+ ? `Next: run /astro-status. (DB recovered in-process.)`
82
+ : `Next: restart the agent/runtime if Astrocode is still in Limited Mode, then run /astro-status.`,
83
+ ].join("\n");
84
+ }
85
+ finally {
86
+ // Only close if this tool opened it AND we did not publish it for ongoing use.
87
+ if (!hadDbAlready && !publishedToRuntime && db && typeof db.close === "function") {
88
+ try {
89
+ db.close();
90
+ }
91
+ catch { }
27
92
  }
28
93
  }
29
- const stat = db.prepare("SELECT schema_version, created_at, updated_at FROM repo_state WHERE id=1").get();
30
- return [
31
- `✅ Astrocode initialized.`,
32
- ``,
33
- `- Repo: ${repoRoot}`,
34
- `- DB: ${path.relative(repoRoot, paths.dbPath)} (schema_version=${stat?.schema_version ?? "?"})`,
35
- `- Spec: ${path.relative(repoRoot, paths.specPath)}`,
36
- ``,
37
- `Next: queue a story (astro_story_queue) and approve it (astro_story_approve), or run /astro-status.`,
38
- ].join("\n");
39
94
  },
40
95
  });
41
96
  }
@@ -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 = ['note', 'policy', 'reminder', 'debug'];
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
- // Check if it's a valid ISO 8601 timestamp with Z suffix
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
- // Additional validation: ensure it's parseable and represents a valid date
21
- const parsed = new Date(timestamp);
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 (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at)
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, type, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
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 = `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 ?`;
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 = `SELECT inject_id, type, title, scope, priority, updated_at FROM injects WHERE ${where.join(" AND ")} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
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
- // Build placeholders safely
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 = JSON.parse(scopes_json);
172
- const types = JSON.parse(types_json);
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 scopes = JSON.parse(scopes_json);
194
- const types = JSON.parse(types_json);
195
- // Get ALL injects to analyze filtering
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
- // Check expiration
207
- if (inject.expires_at && inject.expires_at <= now) {
208
- reasons.push("expired");
209
- skippedExpired++;
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: 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,
@@ -4,7 +4,6 @@ import type { SqliteDb } from "../state/db";
4
4
  export declare function createAstroSpecGetTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
7
- db: SqliteDb;
8
7
  }): ToolDefinition;
9
8
  export declare function createAstroSpecSetTool(opts: {
10
9
  ctx: any;
@@ -5,7 +5,7 @@ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
5
5
  import { nowISO } from "../shared/time";
6
6
  import { sha256Hex } from "../shared/hash";
7
7
  export function createAstroSpecGetTool(opts) {
8
- const { ctx, config, db } = opts;
8
+ const { ctx, config } = opts;
9
9
  return tool({
10
10
  description: "Get current project spec stored at .astro/spec.md",
11
11
  args: {},
@@ -28,6 +28,9 @@ export function createAstroSpecSetTool(opts) {
28
28
  spec_md: tool.schema.string().min(1),
29
29
  },
30
30
  execute: async ({ spec_md }) => {
31
+ if (!db) {
32
+ return "❌ Database not available. Cannot track spec hash. Astrocode is running in limited mode.";
33
+ }
31
34
  const repoRoot = ctx.directory;
32
35
  const paths = getAstroPaths(repoRoot, config.db.path);
33
36
  ensureAstroDirs(paths);
@@ -4,5 +4,5 @@ import type { SqliteDb } from "../state/db";
4
4
  export declare function createAstroStatusTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
7
- db: SqliteDb;
7
+ db?: SqliteDb | null;
8
8
  }): ToolDefinition;
@@ -39,69 +39,87 @@ export function createAstroStatusTool(opts) {
39
39
  include_recent_events: tool.schema.boolean().default(false),
40
40
  },
41
41
  execute: async ({ include_board, include_recent_events }) => {
42
- // Check if database is available
43
42
  if (!db) {
44
- return "🔄 Astrocode Status\n\n⚠️ Limited Mode: Database not available\nAstrocode is running with reduced functionality";
43
+ return [
44
+ `⚠️ Astrocode not initialized.`,
45
+ ``,
46
+ `- Reason: Database not available`,
47
+ ``,
48
+ `Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
49
+ ].join("\n");
45
50
  }
46
- const active = getActiveRun(db);
47
- const lines = [];
48
- lines.push(`# Astrocode Status`);
49
- if (!active) {
50
- lines.push(`- Active run: *(none)*`);
51
+ try {
52
+ const active = getActiveRun(db);
53
+ const lines = [];
54
+ lines.push(`# Astrocode Status`);
55
+ if (!active) {
56
+ lines.push(`- Active run: *(none)*`);
57
+ const next = decideNextAction(db, config);
58
+ if (next.kind === "idle")
59
+ lines.push(`- Next: ${next.reason === "no_approved_stories" ? "No approved stories." : "Idle."}`);
60
+ if (include_board) {
61
+ const counts = db.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state").all();
62
+ lines.push(``, `## Story board`);
63
+ for (const row of counts)
64
+ lines.push(`- ${row.state}: ${row.c}`);
65
+ }
66
+ return lines.join("\n");
67
+ }
68
+ const story = getStory(db, active.story_key);
69
+ const stageRuns = getStageRuns(db, active.run_id);
70
+ lines.push(`- Active run: \`${active.run_id}\` ${statusIcon(active.status)} **${active.status}**`);
71
+ lines.push(`- Story: \`${active.story_key}\` — ${story?.title ?? "(missing)"} (${story?.state ?? "?"})`);
72
+ lines.push(`- Current stage: \`${active.current_stage_key ?? "?"}\``);
73
+ lines.push(``, `## Pipeline`);
74
+ for (const s of stageRuns)
75
+ lines.push(`- ${stageIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
51
76
  const next = decideNextAction(db, config);
52
- if (next.kind === "idle")
53
- lines.push(`- Next: ${next.reason === "no_approved_stories" ? "No approved stories." : "Idle."}`);
77
+ lines.push(``, `## Next`, `- ${next.kind}`);
78
+ if (next.kind === "await_stage_completion") {
79
+ lines.push(`- Awaiting output for stage \`${next.stage_key}\`. When you have it, call **astro_stage_complete** with the stage output text.`);
80
+ }
81
+ else if (next.kind === "delegate_stage") {
82
+ lines.push(`- Need to run stage \`${next.stage_key}\`. Use **astro_workflow_proceed** (or delegate to the matching stage agent).`);
83
+ }
84
+ else if (next.kind === "complete_run") {
85
+ lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
86
+ }
87
+ else if (next.kind === "failed") {
88
+ lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
89
+ }
54
90
  if (include_board) {
55
- const counts = db
56
- .prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
57
- .all();
91
+ const counts = db.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state").all();
58
92
  lines.push(``, `## Story board`);
59
93
  for (const row of counts)
60
94
  lines.push(`- ${row.state}: ${row.c}`);
61
95
  }
96
+ if (include_recent_events) {
97
+ const evs = db.prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10").all();
98
+ lines.push(``, `## Recent events`);
99
+ for (const e of evs)
100
+ lines.push(`- ${e.created_at} — ${e.type}${e.run_id ? ` (run=${e.run_id})` : ""}${e.stage_key ? ` stage=${e.stage_key}` : ""}`);
101
+ }
62
102
  return lines.join("\n");
63
103
  }
64
- const story = getStory(db, active.story_key);
65
- const stageRuns = getStageRuns(db, active.run_id);
66
- lines.push(`- Active run: \`${active.run_id}\` ${statusIcon(active.status)} **${active.status}**`);
67
- lines.push(`- Story: \`${active.story_key}\` — ${story?.title ?? "(missing)"} (${story?.state ?? "?"})`);
68
- lines.push(`- Current stage: \`${active.current_stage_key ?? "?"}\``);
69
- lines.push(``, `## Pipeline`);
70
- for (const s of stageRuns) {
71
- lines.push(`- ${stageIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
72
- }
73
- const next = decideNextAction(db, config);
74
- lines.push(``, `## Next`);
75
- lines.push(`- ${next.kind}`);
76
- if (next.kind === "await_stage_completion") {
77
- lines.push(`- Awaiting output for stage \`${next.stage_key}\`. When you have it, call **astro_stage_complete** with the stage output text.`);
78
- }
79
- else if (next.kind === "delegate_stage") {
80
- lines.push(`- Need to run stage \`${next.stage_key}\`. Use **astro_workflow_proceed** (or delegate to the matching stage agent).`);
81
- }
82
- else if (next.kind === "complete_run") {
83
- lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
84
- }
85
- else if (next.kind === "failed") {
86
- lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
87
- }
88
- if (include_board) {
89
- const counts = db
90
- .prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
91
- .all();
92
- lines.push(``, `## Story board`);
93
- for (const row of counts)
94
- lines.push(`- ${row.state}: ${row.c}`);
95
- }
96
- if (include_recent_events) {
97
- const evs = db
98
- .prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10")
99
- .all();
100
- lines.push(``, `## Recent events`);
101
- for (const e of evs)
102
- lines.push(`- ${e.created_at} — ${e.type}${e.run_id ? ` (run=${e.run_id})` : ""}${e.stage_key ? ` stage=${e.stage_key}` : ""}`);
104
+ catch (e) {
105
+ const msg = e instanceof Error ? e.message : String(e);
106
+ if (msg.includes("no such table") || msg.includes("no such column")) {
107
+ return [
108
+ `⚠️ Astrocode not initialized.`,
109
+ ``,
110
+ `- Reason: Database present but schema is not initialized or is incompatible`,
111
+ `- Error: ${msg}`,
112
+ ``,
113
+ `Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
114
+ ].join("\n");
115
+ }
116
+ return [
117
+ `# Astrocode Status`,
118
+ ``,
119
+ `⛔ Database error.`,
120
+ `Error: ${msg}`,
121
+ ].join("\n");
103
122
  }
104
- return lines.join("\n");
105
123
  },
106
124
  });
107
125
  }
@@ -1,7 +1,7 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
2
  import { withTx } from "../state/db";
3
3
  import { buildContextSnapshot } from "../workflow/context";
4
- import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun } from "../workflow/state-machine";
4
+ import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
5
5
  import { buildStageDirective, directiveHash } from "../workflow/directives";
6
6
  import { injectChatPrompt } from "../ui/inject";
7
7
  import { nowISO } from "../shared/time";
@@ -318,7 +318,7 @@ export function createAstroWorkflowProceedTool(opts) {
318
318
  break;
319
319
  }
320
320
  // Housekeeping event
321
- db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, 'workflow.proceed', ?, ?)").run(newEventId(), JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
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());
322
322
  const active = getActiveRun(db);
323
323
  const lines = [];
324
324
  lines.push(`# astro_workflow_proceed`);