astrocode-workflow 0.4.4 → 0.4.6

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 (46) hide show
  1. package/dist/src/hooks/continuation-enforcer.d.ts +1 -9
  2. package/dist/src/hooks/continuation-enforcer.js +12 -2
  3. package/dist/src/hooks/inject-provider.d.ts +1 -9
  4. package/dist/src/hooks/inject-provider.js +14 -5
  5. package/dist/src/state/types.d.ts +9 -0
  6. package/dist/src/tools/artifacts.d.ts +4 -4
  7. package/dist/src/tools/artifacts.js +12 -3
  8. package/dist/src/tools/health.d.ts +2 -2
  9. package/dist/src/tools/health.js +18 -11
  10. package/dist/src/tools/index.js +27 -34
  11. package/dist/src/tools/injects.d.ts +8 -8
  12. package/dist/src/tools/injects.js +24 -6
  13. package/dist/src/tools/metrics.d.ts +6 -5
  14. package/dist/src/tools/repair.d.ts +2 -2
  15. package/dist/src/tools/repair.js +5 -1
  16. package/dist/src/tools/reset.d.ts +2 -2
  17. package/dist/src/tools/reset.js +9 -1
  18. package/dist/src/tools/run.d.ts +3 -3
  19. package/dist/src/tools/run.js +8 -2
  20. package/dist/src/tools/spec.d.ts +2 -2
  21. package/dist/src/tools/spec.js +3 -2
  22. package/dist/src/tools/stage.d.ts +5 -5
  23. package/dist/src/tools/stage.js +16 -4
  24. package/dist/src/tools/status.d.ts +2 -2
  25. package/dist/src/tools/status.js +25 -2
  26. package/dist/src/tools/story.d.ts +5 -5
  27. package/dist/src/tools/story.js +16 -4
  28. package/dist/src/tools/workflow.d.ts +2 -2
  29. package/dist/src/tools/workflow.js +5 -1
  30. package/package.json +1 -1
  31. package/src/hooks/continuation-enforcer.ts +11 -9
  32. package/src/hooks/inject-provider.ts +16 -12
  33. package/src/state/types.ts +11 -0
  34. package/src/tools/artifacts.ts +16 -7
  35. package/src/tools/health.ts +22 -13
  36. package/src/tools/index.ts +32 -40
  37. package/src/tools/injects.ts +32 -14
  38. package/src/tools/metrics.ts +3 -6
  39. package/src/tools/repair.ts +8 -4
  40. package/src/tools/reset.ts +11 -3
  41. package/src/tools/run.ts +11 -5
  42. package/src/tools/spec.ts +5 -4
  43. package/src/tools/stage.ts +22 -10
  44. package/src/tools/status.ts +28 -5
  45. package/src/tools/story.ts +21 -9
  46. package/src/tools/workflow.ts +8 -3
@@ -3,7 +3,7 @@ import { tool } from "@opencode-ai/plugin/tool";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  export function createAstroResetTool(opts) {
6
- const { ctx, config, db } = opts;
6
+ const { ctx, config, runtime } = opts;
7
7
  return tool({
8
8
  description: "Reset Astrocode database: safely delete all DB files and WAL/SHM after killing concurrent processes.",
9
9
  args: {
@@ -23,6 +23,14 @@ export function createAstroResetTool(opts) {
23
23
  "To confirm: astro_reset(confirm=\"RESET\")",
24
24
  ].join("\n");
25
25
  }
26
+ // Close DB connection if open
27
+ if (runtime.db) {
28
+ try {
29
+ runtime.db.close();
30
+ }
31
+ catch { /* ignore */ }
32
+ runtime.db = null;
33
+ }
26
34
  const repoRoot = ctx.directory || process.cwd();
27
35
  const dbPath = config.db?.path || ".astro/astro.db";
28
36
  const fullDbPath = path.resolve(repoRoot, dbPath);
@@ -1,13 +1,13 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
- import type { SqliteDb } from "../state/db";
3
+ import type { RuntimeState } from "../state/types";
4
4
  export declare function createAstroRunGetTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
7
- db: SqliteDb;
7
+ runtime: RuntimeState;
8
8
  }): ToolDefinition;
9
9
  export declare function createAstroRunAbortTool(opts: {
10
10
  ctx: any;
11
11
  config: AstrocodeConfig;
12
- db: SqliteDb;
12
+ runtime: RuntimeState;
13
13
  }): ToolDefinition;
@@ -1,7 +1,7 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
2
  import { abortRun, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
3
3
  export function createAstroRunGetTool(opts) {
4
- const { db } = opts;
4
+ const { runtime } = opts;
5
5
  return tool({
6
6
  description: "Get run details (and stage run statuses). Defaults to active run if run_id omitted.",
7
7
  args: {
@@ -9,6 +9,9 @@ export function createAstroRunGetTool(opts) {
9
9
  include_stage_summaries: tool.schema.boolean().default(false),
10
10
  },
11
11
  execute: async ({ run_id, include_stage_summaries }) => {
12
+ const { db } = runtime;
13
+ if (!db)
14
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
12
15
  const active = getActiveRun(db);
13
16
  const rid = run_id ?? active?.run_id;
14
17
  if (!rid)
@@ -35,7 +38,7 @@ export function createAstroRunGetTool(opts) {
35
38
  });
36
39
  }
37
40
  export function createAstroRunAbortTool(opts) {
38
- const { db } = opts;
41
+ const { runtime } = opts;
39
42
  return tool({
40
43
  description: "Abort a run and unlock its story (returns story to approved). Defaults to active run if run_id omitted.",
41
44
  args: {
@@ -43,6 +46,9 @@ export function createAstroRunAbortTool(opts) {
43
46
  reason: tool.schema.string().default("aborted by user"),
44
47
  },
45
48
  execute: async ({ run_id, reason }) => {
49
+ const { db } = runtime;
50
+ if (!db)
51
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
46
52
  const active = getActiveRun(db);
47
53
  const rid = run_id ?? active?.run_id;
48
54
  if (!rid)
@@ -1,6 +1,6 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
- import type { SqliteDb } from "../state/db";
3
+ import type { RuntimeState } from "../state/types";
4
4
  export declare function createAstroSpecGetTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
@@ -8,5 +8,5 @@ export declare function createAstroSpecGetTool(opts: {
8
8
  export declare function createAstroSpecSetTool(opts: {
9
9
  ctx: any;
10
10
  config: AstrocodeConfig;
11
- db: SqliteDb;
11
+ runtime: RuntimeState;
12
12
  }): ToolDefinition;
@@ -21,15 +21,16 @@ export function createAstroSpecGetTool(opts) {
21
21
  });
22
22
  }
23
23
  export function createAstroSpecSetTool(opts) {
24
- const { ctx, config, db } = opts;
24
+ const { ctx, config, runtime } = opts;
25
25
  return tool({
26
26
  description: "Set/replace the project spec at .astro/spec.md and record its hash in the DB.",
27
27
  args: {
28
28
  spec_md: tool.schema.string().min(1),
29
29
  },
30
30
  execute: async ({ spec_md }) => {
31
+ const { db } = runtime;
31
32
  if (!db) {
32
- return "❌ Database not available. Cannot track spec hash. Astrocode is running in limited mode.";
33
+ return "❌ Database not available. Cannot track spec hash. Astrocode is running in limited mode. Run **astro_init** first.";
33
34
  }
34
35
  const repoRoot = ctx.directory;
35
36
  const paths = getAstroPaths(repoRoot, config.db.path);
@@ -1,23 +1,23 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
- import type { SqliteDb } from "../state/db";
3
+ import type { RuntimeState } from "../state/types";
4
4
  export declare function createAstroStageStartTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
7
- db: SqliteDb;
7
+ runtime: RuntimeState;
8
8
  }): ToolDefinition;
9
9
  export declare function createAstroStageCompleteTool(opts: {
10
10
  ctx: any;
11
11
  config: AstrocodeConfig;
12
- db: SqliteDb;
12
+ runtime: RuntimeState;
13
13
  }): ToolDefinition;
14
14
  export declare function createAstroStageFailTool(opts: {
15
15
  ctx: any;
16
16
  config: AstrocodeConfig;
17
- db: SqliteDb;
17
+ runtime: RuntimeState;
18
18
  }): ToolDefinition;
19
19
  export declare function createAstroStageResetTool(opts: {
20
20
  ctx: any;
21
21
  config: AstrocodeConfig;
22
- db: SqliteDb;
22
+ runtime: RuntimeState;
23
23
  }): ToolDefinition;
@@ -54,7 +54,7 @@ function splitTasksIntoStories(db, tasks, run, now, newStoryKeys, relationReason
54
54
  }
55
55
  }
56
56
  export function createAstroStageStartTool(opts) {
57
- const { config, db } = opts;
57
+ const { runtime } = opts;
58
58
  return tool({
59
59
  description: "Start a stage for a run (sets stage_run.status=running). Usually called by astro_workflow_proceed.",
60
60
  args: {
@@ -64,6 +64,9 @@ export function createAstroStageStartTool(opts) {
64
64
  subagent_session_id: tool.schema.string().optional(),
65
65
  },
66
66
  execute: async ({ run_id, stage_key, subagent_type, subagent_session_id }) => {
67
+ const { db } = runtime;
68
+ if (!db)
69
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
67
70
  const active = getActiveRun(db);
68
71
  const rid = run_id ?? active?.run_id;
69
72
  if (!rid)
@@ -80,7 +83,7 @@ export function createAstroStageStartTool(opts) {
80
83
  });
81
84
  }
82
85
  export function createAstroStageCompleteTool(opts) {
83
- const { ctx, config, db } = opts;
86
+ const { ctx, config, runtime } = opts;
84
87
  return tool({
85
88
  description: "Complete a stage from stage-agent output text. Writes baton artifacts, updates stage_runs, advances pipeline, and can auto-queue split stories.",
86
89
  args: {
@@ -92,6 +95,9 @@ export function createAstroStageCompleteTool(opts) {
92
95
  relation_reason: tool.schema.string().default("split from stage output"),
93
96
  },
94
97
  execute: async ({ run_id, stage_key, output_text, allow_new_stories, relation_reason }) => {
98
+ const { db } = runtime;
99
+ if (!db)
100
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
95
101
  const repoRoot = ctx.directory;
96
102
  const paths = getAstroPaths(repoRoot, config.db.path);
97
103
  ensureAstroDirs(paths);
@@ -305,7 +311,7 @@ Ensure JSON has required fields (stage_key, status) and valid syntax.`;
305
311
  });
306
312
  }
307
313
  export function createAstroStageFailTool(opts) {
308
- const { db } = opts;
314
+ const { runtime } = opts;
309
315
  return tool({
310
316
  description: "Manually fail a stage and mark run failed.",
311
317
  args: {
@@ -314,6 +320,9 @@ export function createAstroStageFailTool(opts) {
314
320
  error_text: tool.schema.string().min(1),
315
321
  },
316
322
  execute: async ({ run_id, stage_key, error_text }) => {
323
+ const { db } = runtime;
324
+ if (!db)
325
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
317
326
  const active = getActiveRun(db);
318
327
  const rid = run_id ?? active?.run_id;
319
328
  if (!rid)
@@ -339,7 +348,7 @@ export function createAstroStageFailTool(opts) {
339
348
  });
340
349
  }
341
350
  export function createAstroStageResetTool(opts) {
342
- const { db } = opts;
351
+ const { runtime } = opts;
343
352
  return tool({
344
353
  description: "Admin: reset a stage (and later stages) back to pending for a run. Re-opens run as running. Use carefully.",
345
354
  args: {
@@ -348,6 +357,9 @@ export function createAstroStageResetTool(opts) {
348
357
  note: tool.schema.string().default("reset by user"),
349
358
  },
350
359
  execute: async ({ run_id, stage_key, note }) => {
360
+ const { db } = runtime;
361
+ if (!db)
362
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
351
363
  const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(run_id);
352
364
  if (!run)
353
365
  throw new Error(`Run not found: ${run_id}`);
@@ -1,8 +1,8 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
- import type { SqliteDb } from "../state/db";
3
+ import type { RuntimeState } from "../state/types";
4
4
  export declare function createAstroStatusTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
7
- db?: SqliteDb | null;
7
+ runtime: RuntimeState;
8
8
  }): ToolDefinition;
@@ -1,5 +1,8 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
2
  import { decideNextAction, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
3
+ import { openSqlite, configurePragmas, ensureSchema } from "../state/db";
4
+ import { getAstroPaths } from "../shared/paths";
5
+ import fs from "node:fs";
3
6
  function statusIcon(status) {
4
7
  switch (status) {
5
8
  case "running":
@@ -31,7 +34,7 @@ function stageIcon(status) {
31
34
  }
32
35
  }
33
36
  export function createAstroStatusTool(opts) {
34
- const { ctx, config, db } = opts;
37
+ const { ctx, config, runtime } = opts;
35
38
  return tool({
36
39
  description: "Show a compact Astrocode status dashboard: active run/stage, pipeline, story board counts, and next action.",
37
40
  args: {
@@ -39,17 +42,37 @@ export function createAstroStatusTool(opts) {
39
42
  include_recent_events: tool.schema.boolean().default(false),
40
43
  },
41
44
  execute: async ({ include_board, include_recent_events }) => {
45
+ // Lazy initialization: if DB is missing but file exists, try to connect
46
+ if (!runtime.db) {
47
+ const repoRoot = ctx.directory || process.cwd();
48
+ const paths = getAstroPaths(repoRoot, config.db.path);
49
+ if (fs.existsSync(paths.dbPath)) {
50
+ try {
51
+ const db = openSqlite(paths.dbPath, { busyTimeoutMs: config.db.busy_timeout_ms });
52
+ configurePragmas(db, config.db.pragmas);
53
+ ensureSchema(db, { allowAutoMigrate: config.db.allow_auto_migrate, silent: true });
54
+ runtime.db = db;
55
+ runtime.limitedMode = false;
56
+ runtime.limitedModeReason = null;
57
+ }
58
+ catch {
59
+ // Ignore lazy init failures, will fall through to "not initialized" message
60
+ }
61
+ }
62
+ }
63
+ const { db } = runtime;
42
64
  if (!db) {
43
65
  return [
44
66
  `⚠️ Astrocode not initialized.`,
45
67
  ``,
46
68
  `- Reason: Database not available`,
47
69
  ``,
48
- `Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
70
+ `Next: run **astro_init**, then run /astro-status again.`,
49
71
  ].join("\n");
50
72
  }
51
73
  try {
52
74
  const active = getActiveRun(db);
75
+ // ... rest of existing logic ...
53
76
  const lines = [];
54
77
  lines.push(`# Astrocode Status`);
55
78
  if (!active) {
@@ -1,23 +1,23 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
- import type { SqliteDb } from "../state/db";
3
+ import type { RuntimeState } from "../state/types";
4
4
  export declare function createAstroStoryQueueTool(opts: {
5
5
  ctx: any;
6
6
  config: AstrocodeConfig;
7
- db: SqliteDb;
7
+ runtime: RuntimeState;
8
8
  }): ToolDefinition;
9
9
  export declare function createAstroStoryApproveTool(opts: {
10
10
  ctx: any;
11
11
  config: AstrocodeConfig;
12
- db: SqliteDb;
12
+ runtime: RuntimeState;
13
13
  }): ToolDefinition;
14
14
  export declare function createAstroStoryBoardTool(opts: {
15
15
  ctx: any;
16
16
  config: AstrocodeConfig;
17
- db: SqliteDb;
17
+ runtime: RuntimeState;
18
18
  }): ToolDefinition;
19
19
  export declare function createAstroStorySetStateTool(opts: {
20
20
  ctx: any;
21
21
  config: AstrocodeConfig;
22
- db: SqliteDb;
22
+ runtime: RuntimeState;
23
23
  }): ToolDefinition;
@@ -3,7 +3,7 @@ import { withTx } from "../state/db";
3
3
  import { nowISO } from "../shared/time";
4
4
  import { insertStory } from "../workflow/story-helpers";
5
5
  export function createAstroStoryQueueTool(opts) {
6
- const { db } = opts;
6
+ const { runtime } = opts;
7
7
  return tool({
8
8
  description: "Create a queued story (ticket) in Astrocode. Returns story_key.",
9
9
  args: {
@@ -13,6 +13,9 @@ export function createAstroStoryQueueTool(opts) {
13
13
  priority: tool.schema.number().int().default(0),
14
14
  },
15
15
  execute: async ({ title, body_md, epic_key, priority }) => {
16
+ const { db } = runtime;
17
+ if (!db)
18
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
16
19
  const story_key = withTx(db, () => {
17
20
  const key = insertStory(db, { title, body_md, epic_key: epic_key ?? null, priority: priority ?? 0, state: 'queued' });
18
21
  return key;
@@ -22,13 +25,16 @@ export function createAstroStoryQueueTool(opts) {
22
25
  });
23
26
  }
24
27
  export function createAstroStoryApproveTool(opts) {
25
- const { db } = opts;
28
+ const { runtime } = opts;
26
29
  return tool({
27
30
  description: "Approve a story so it becomes eligible to run.",
28
31
  args: {
29
32
  story_key: tool.schema.string().min(1),
30
33
  },
31
34
  execute: async ({ story_key }) => {
35
+ const { db } = runtime;
36
+ if (!db)
37
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
32
38
  const now = nowISO();
33
39
  const row = db.prepare("SELECT story_key, state, title FROM stories WHERE story_key=?").get(story_key);
34
40
  if (!row)
@@ -41,13 +47,16 @@ export function createAstroStoryApproveTool(opts) {
41
47
  });
42
48
  }
43
49
  export function createAstroStoryBoardTool(opts) {
44
- const { db } = opts;
50
+ const { runtime } = opts;
45
51
  return tool({
46
52
  description: "Show stories grouped by state (a compact board).",
47
53
  args: {
48
54
  limit_per_state: tool.schema.number().int().positive().default(20),
49
55
  },
50
56
  execute: async ({ limit_per_state }) => {
57
+ const { db } = runtime;
58
+ if (!db)
59
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
51
60
  const states = ["queued", "approved", "in_progress", "blocked", "done", "archived"];
52
61
  const lines = [];
53
62
  lines.push("# Story board");
@@ -64,7 +73,7 @@ export function createAstroStoryBoardTool(opts) {
64
73
  });
65
74
  }
66
75
  export function createAstroStorySetStateTool(opts) {
67
- const { db } = opts;
76
+ const { runtime } = opts;
68
77
  return tool({
69
78
  description: "Admin: set story state manually (queued|approved|in_progress|done|blocked|archived). Use carefully.",
70
79
  args: {
@@ -73,6 +82,9 @@ export function createAstroStorySetStateTool(opts) {
73
82
  note: tool.schema.string().default(""),
74
83
  },
75
84
  execute: async ({ story_key, state, note }) => {
85
+ const { db } = runtime;
86
+ if (!db)
87
+ return "⚠️ Astrocode not initialized. Run **astro_init** first.";
76
88
  const now = nowISO();
77
89
  const row = db.prepare("SELECT story_key, title, state FROM stories WHERE story_key=?").get(story_key);
78
90
  if (!row)
@@ -1,6 +1,6 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
- import type { SqliteDb } from "../state/db";
3
+ import type { RuntimeState } from "../state/types";
4
4
  import type { StageKey } from "../state/types";
5
5
  import type { AgentConfig } from "@opencode-ai/sdk";
6
6
  export declare const STAGE_TO_AGENT_MAP: Record<string, string>;
@@ -8,6 +8,6 @@ export declare function resolveAgentName(stageKey: StageKey, config: AstrocodeCo
8
8
  export declare function createAstroWorkflowProceedTool(opts: {
9
9
  ctx: any;
10
10
  config: AstrocodeConfig;
11
- db: SqliteDb;
11
+ runtime: RuntimeState;
12
12
  agents?: Record<string, AgentConfig>;
13
13
  }): ToolDefinition;
@@ -143,7 +143,7 @@ function buildUiMessage(e) {
143
143
  }
144
144
  }
145
145
  export function createAstroWorkflowProceedTool(opts) {
146
- const { ctx, config, db, agents } = opts;
146
+ const { ctx, config, runtime, agents } = opts;
147
147
  const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
148
148
  return tool({
149
149
  description: "Deterministic harness: advances the DB-driven pipeline by one step (or loops bounded). Stops when LLM work is required (delegation/await).",
@@ -152,6 +152,10 @@ export function createAstroWorkflowProceedTool(opts) {
152
152
  max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
153
153
  },
154
154
  execute: async ({ mode, max_steps }) => {
155
+ const { db } = runtime;
156
+ if (!db) {
157
+ return "⚠️ Cannot proceed: Astrocode is not initialized. Run **astro_init** first.";
158
+ }
155
159
  const sessionId = ctx.sessionID;
156
160
  const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
157
161
  const actions = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,5 +1,5 @@
1
1
  import type { AstrocodeConfig } from "../config/schema";
2
- import type { SqliteDb } from "../state/db";
2
+ import type { RuntimeState } from "../state/types";
3
3
  import { buildContextSnapshot } from "../workflow/context";
4
4
  import { decideNextAction, getActiveRun } from "../workflow/state-machine";
5
5
  import { buildContinueDirective, type BuiltDirective } from "../workflow/directives";
@@ -35,19 +35,12 @@ function msFromIso(iso: string): number {
35
35
  return Number.isFinite(t) ? t : 0;
36
36
  }
37
37
 
38
- type RuntimeState = {
39
- db: SqliteDb | null;
40
- limitedMode: boolean;
41
- limitedModeReason: null | { code: "db_init_failed"|"schema_too_old"|"schema_downgrade"|"schema_migration_failed"; details: any };
42
- };
43
-
44
38
  export function createContinuationEnforcer(opts: {
45
39
  ctx: any;
46
40
  config: AstrocodeConfig;
47
41
  runtime: RuntimeState;
48
42
  }) {
49
43
  const { ctx, config, runtime } = opts;
50
- const { db } = runtime;
51
44
 
52
45
  const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
53
46
 
@@ -106,6 +99,9 @@ export function createContinuationEnforcer(opts: {
106
99
  }
107
100
 
108
101
  function shouldDedupe(sessionId: string, directive: BuiltDirective): boolean {
102
+ const { db } = runtime;
103
+ if (!db) return false;
104
+
109
105
  const s = getState(sessionId);
110
106
  const now = Date.now();
111
107
 
@@ -128,6 +124,9 @@ export function createContinuationEnforcer(opts: {
128
124
  }
129
125
 
130
126
  async function recordContinuation(sessionId: string, runId: string | null, directive: BuiltDirective, reason: string) {
127
+ const { db } = runtime;
128
+ if (!db) return;
129
+
131
130
  db.prepare(
132
131
  "INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, ?, ?, ?)"
133
132
  ).run(sessionId, runId, directive.hash, directive.kind, reason, nowISO());
@@ -163,7 +162,8 @@ export function createContinuationEnforcer(opts: {
163
162
  }
164
163
 
165
164
  async function maybeInjectContinue(sessionId: string, reason: string) {
166
- if (!config.continuation.enabled) return;
165
+ const { db } = runtime;
166
+ if (!config.continuation.enabled || !db) return;
167
167
 
168
168
  // Require active run
169
169
  const active = getActiveRun(db);
@@ -213,6 +213,7 @@ export function createContinuationEnforcer(opts: {
213
213
  async onToolAfter(input: ToolExecuteAfterInput) {
214
214
  const sessionId = input.sessionID ?? (ctx as any).sessionID;
215
215
  if (!sessionId) return;
216
+ if (!config.continuation.enabled) return;
216
217
  if (!config.continuation.inject_on_tool_done_if_run_active) return;
217
218
 
218
219
  // Inject continuation immediately after any tool execution
@@ -222,6 +223,7 @@ export function createContinuationEnforcer(opts: {
222
223
  },
223
224
 
224
225
  async onChatMessage(_input: ChatMessageInput) {
226
+ if (!config.continuation.enabled) return;
225
227
  if (!config.continuation.inject_on_message_done_if_run_active) return;
226
228
  scheduleIdleInjection(_input.sessionID);
227
229
  },
@@ -1,5 +1,5 @@
1
1
  import type { AstrocodeConfig } from "../config/schema";
2
- import type { SqliteDb } from "../state/db";
2
+ import type { RuntimeState } from "../state/types";
3
3
  import { selectEligibleInjects } from "../tools/injects";
4
4
  import { injectChatPrompt } from "../ui/inject";
5
5
  import { nowISO } from "../shared/time";
@@ -15,19 +15,12 @@ type ToolExecuteAfterInput = {
15
15
  sessionID?: string;
16
16
  };
17
17
 
18
- type RuntimeState = {
19
- db: SqliteDb | null;
20
- limitedMode: boolean;
21
- limitedModeReason: null | { code: "db_init_failed"|"schema_too_old"|"schema_downgrade"|"schema_migration_failed"; details: any };
22
- };
23
-
24
18
  export function createInjectProvider(opts: {
25
19
  ctx: any;
26
20
  config: AstrocodeConfig;
27
21
  runtime: RuntimeState;
28
22
  }) {
29
23
  const { ctx, config, runtime } = opts;
30
- const { db } = runtime;
31
24
 
32
25
  // Cache to avoid re-injecting the same injects repeatedly
33
26
  // Map of inject_id -> last injected timestamp
@@ -57,6 +50,9 @@ export function createInjectProvider(opts: {
57
50
  }
58
51
 
59
52
  function getInjectionDiagnostics(nowIso: string, scopeAllowlist: string[], typeAllowlist: string[]): any {
53
+ const { db } = runtime;
54
+ if (!db) return null;
55
+
60
56
  // Get ALL injects to analyze filtering
61
57
  const allInjects = db.prepare("SELECT * FROM injects").all() as any[];
62
58
 
@@ -107,6 +103,9 @@ export function createInjectProvider(opts: {
107
103
  }
108
104
 
109
105
  async function injectEligibleInjects(sessionId: string, context?: string) {
106
+ const { db } = runtime;
107
+ if (!db) return;
108
+
110
109
  const now = nowISO();
111
110
  const nowMs = Date.now();
112
111
 
@@ -130,7 +129,7 @@ export function createInjectProvider(opts: {
130
129
 
131
130
  if (eligibleInjects.length === 0) {
132
131
  // Log when no injects are eligible
133
- if (EMIT_TELEMETRY) {
132
+ if (EMIT_TELEMETRY && diagnostics) {
134
133
  // eslint-disable-next-line no-console
135
134
  console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=0 injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
136
135
  }
@@ -145,7 +144,7 @@ export function createInjectProvider(opts: {
145
144
  }
146
145
 
147
146
  // Format as injection message
148
- const formattedText = `[Inject: ${inject.title}]\n\n${inject.body_md}`;
147
+ const formattedText = `### 🔖 ASTROCODE: INJECTED NOTE\n**Title:** ${inject.title}\n\n${inject.body_md}`;
149
148
 
150
149
  try {
151
150
  await injectChatPrompt({
@@ -165,7 +164,7 @@ export function createInjectProvider(opts: {
165
164
  }
166
165
 
167
166
  // Log diagnostic summary
168
- if (EMIT_TELEMETRY || injected > 0) {
167
+ if ((EMIT_TELEMETRY || injected > 0) && diagnostics) {
169
168
  // eslint-disable-next-line no-console
170
169
  console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
171
170
  }
@@ -184,7 +183,8 @@ export function createInjectProvider(opts: {
184
183
 
185
184
  // Auto-approve queued stories if enabled
186
185
  async function maybeAutoApprove(sessionId: string) {
187
- if (!config.inject?.auto_approve_queued_stories) return;
186
+ const { db } = runtime;
187
+ if (!config.inject?.auto_approve_queued_stories || !db) return;
188
188
 
189
189
  try {
190
190
  // Get all queued stories
@@ -219,6 +219,9 @@ export function createInjectProvider(opts: {
219
219
 
220
220
  // Inject eligible injects before processing the user's message
221
221
  await injectEligibleInjects(input.sessionID, 'chat_message');
222
+
223
+ // Also inject Workflow Pulse on chat messages if a run is active
224
+ await maybeInjectWorkflowPulse(input.sessionID);
222
225
  },
223
226
 
224
227
  async onToolAfter(input: ToolExecuteAfterInput) {
@@ -243,6 +246,7 @@ export function createInjectProvider(opts: {
243
246
  };
244
247
 
245
248
  async function maybeInjectWorkflowPulse(sessionId: string) {
249
+ const { db } = runtime;
246
250
  if (!db) return;
247
251
  try {
248
252
  const active = getActiveRun(db);
@@ -75,3 +75,14 @@ export type InjectRow = {
75
75
  created_at: string;
76
76
  updated_at: string;
77
77
  };
78
+
79
+ import { SqliteDb } from "./db";
80
+
81
+ export type RuntimeState = {
82
+ db: SqliteDb | null;
83
+ limitedMode: boolean;
84
+ limitedModeReason: null | {
85
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
86
+ details: any;
87
+ };
88
+ };