astrocode-workflow 0.4.0 → 0.4.2

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 (144) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/shared/metrics.d.ts +66 -0
  3. package/dist/shared/metrics.js +112 -0
  4. package/dist/src/agents/commands.d.ts +9 -0
  5. package/dist/src/agents/commands.js +121 -0
  6. package/dist/src/agents/prompts.d.ts +3 -0
  7. package/dist/src/agents/prompts.js +232 -0
  8. package/dist/src/agents/registry.d.ts +6 -0
  9. package/dist/src/agents/registry.js +242 -0
  10. package/dist/src/agents/types.d.ts +14 -0
  11. package/dist/src/agents/types.js +8 -0
  12. package/dist/src/astro/workflow-runner.d.ts +11 -0
  13. package/dist/src/astro/workflow-runner.js +14 -0
  14. package/dist/src/config/config-handler.d.ts +4 -0
  15. package/dist/src/config/config-handler.js +46 -0
  16. package/dist/src/config/defaults.d.ts +3 -0
  17. package/dist/src/config/defaults.js +3 -0
  18. package/dist/src/config/loader.d.ts +11 -0
  19. package/dist/src/config/loader.js +82 -0
  20. package/dist/src/config/schema.d.ts +195 -0
  21. package/dist/src/config/schema.js +224 -0
  22. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  23. package/dist/src/hooks/continuation-enforcer.js +190 -0
  24. package/dist/src/hooks/inject-provider.d.ts +27 -0
  25. package/dist/src/hooks/inject-provider.js +189 -0
  26. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  27. package/dist/src/hooks/tool-output-truncator.js +57 -0
  28. package/dist/src/index.d.ts +3 -0
  29. package/dist/src/index.js +307 -0
  30. package/dist/src/shared/deep-merge.d.ts +8 -0
  31. package/dist/src/shared/deep-merge.js +25 -0
  32. package/dist/src/shared/hash.d.ts +1 -0
  33. package/dist/src/shared/hash.js +4 -0
  34. package/dist/src/shared/log.d.ts +7 -0
  35. package/dist/src/shared/log.js +24 -0
  36. package/dist/src/shared/metrics.d.ts +66 -0
  37. package/dist/src/shared/metrics.js +112 -0
  38. package/dist/src/shared/model-tuning.d.ts +9 -0
  39. package/dist/src/shared/model-tuning.js +28 -0
  40. package/dist/src/shared/paths.d.ts +19 -0
  41. package/dist/src/shared/paths.js +64 -0
  42. package/dist/src/shared/text.d.ts +4 -0
  43. package/dist/src/shared/text.js +19 -0
  44. package/dist/src/shared/time.d.ts +1 -0
  45. package/dist/src/shared/time.js +3 -0
  46. package/dist/src/state/adapters/index.d.ts +41 -0
  47. package/dist/src/state/adapters/index.js +115 -0
  48. package/dist/src/state/db.d.ts +16 -0
  49. package/dist/src/state/db.js +225 -0
  50. package/dist/src/state/ids.d.ts +8 -0
  51. package/dist/src/state/ids.js +25 -0
  52. package/dist/src/state/repo-lock.d.ts +67 -0
  53. package/dist/src/state/repo-lock.js +580 -0
  54. package/dist/src/state/schema.d.ts +2 -0
  55. package/dist/src/state/schema.js +258 -0
  56. package/dist/src/state/types.d.ts +71 -0
  57. package/dist/src/state/types.js +1 -0
  58. package/dist/src/state/workflow-repo-lock.d.ts +23 -0
  59. package/dist/src/state/workflow-repo-lock.js +83 -0
  60. package/dist/src/tools/artifacts.d.ts +18 -0
  61. package/dist/src/tools/artifacts.js +71 -0
  62. package/dist/src/tools/health.d.ts +8 -0
  63. package/dist/src/tools/health.js +88 -0
  64. package/dist/src/tools/index.d.ts +20 -0
  65. package/dist/src/tools/index.js +94 -0
  66. package/dist/src/tools/init.d.ts +17 -0
  67. package/dist/src/tools/init.js +96 -0
  68. package/dist/src/tools/injects.d.ts +53 -0
  69. package/dist/src/tools/injects.js +325 -0
  70. package/dist/src/tools/lock.d.ts +4 -0
  71. package/dist/src/tools/lock.js +78 -0
  72. package/dist/src/tools/metrics.d.ts +7 -0
  73. package/dist/src/tools/metrics.js +61 -0
  74. package/dist/src/tools/repair.d.ts +8 -0
  75. package/dist/src/tools/repair.js +26 -0
  76. package/dist/src/tools/reset.d.ts +8 -0
  77. package/dist/src/tools/reset.js +92 -0
  78. package/dist/src/tools/run.d.ts +13 -0
  79. package/dist/src/tools/run.js +54 -0
  80. package/dist/src/tools/spec.d.ts +12 -0
  81. package/dist/src/tools/spec.js +44 -0
  82. package/dist/src/tools/stage.d.ts +23 -0
  83. package/dist/src/tools/stage.js +371 -0
  84. package/dist/src/tools/status.d.ts +8 -0
  85. package/dist/src/tools/status.js +125 -0
  86. package/dist/src/tools/story.d.ts +23 -0
  87. package/dist/src/tools/story.js +85 -0
  88. package/dist/src/tools/workflow.d.ts +13 -0
  89. package/dist/src/tools/workflow.js +345 -0
  90. package/dist/src/ui/inject.d.ts +12 -0
  91. package/dist/src/ui/inject.js +107 -0
  92. package/dist/src/ui/toasts.d.ts +13 -0
  93. package/dist/src/ui/toasts.js +39 -0
  94. package/dist/src/workflow/artifacts.d.ts +24 -0
  95. package/dist/src/workflow/artifacts.js +45 -0
  96. package/dist/src/workflow/baton.d.ts +72 -0
  97. package/dist/src/workflow/baton.js +166 -0
  98. package/dist/src/workflow/context.d.ts +20 -0
  99. package/dist/src/workflow/context.js +113 -0
  100. package/dist/src/workflow/directives.d.ts +39 -0
  101. package/dist/src/workflow/directives.js +137 -0
  102. package/dist/src/workflow/repair.d.ts +8 -0
  103. package/dist/src/workflow/repair.js +99 -0
  104. package/dist/src/workflow/state-machine.d.ts +86 -0
  105. package/dist/src/workflow/state-machine.js +216 -0
  106. package/dist/src/workflow/story-helpers.d.ts +9 -0
  107. package/dist/src/workflow/story-helpers.js +13 -0
  108. package/dist/state/db.d.ts +1 -0
  109. package/dist/state/db.js +9 -0
  110. package/dist/state/repo-lock.d.ts +3 -0
  111. package/dist/state/repo-lock.js +29 -0
  112. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  113. package/dist/test/integration/db-transactions.test.js +126 -0
  114. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  115. package/dist/test/integration/injection-metrics.test.js +129 -0
  116. package/dist/tools/health.d.ts +8 -0
  117. package/dist/tools/health.js +119 -0
  118. package/dist/tools/index.js +9 -0
  119. package/dist/tools/metrics.d.ts +7 -0
  120. package/dist/tools/metrics.js +61 -0
  121. package/dist/tools/reset.d.ts +8 -0
  122. package/dist/tools/reset.js +92 -0
  123. package/dist/tools/workflow.js +178 -168
  124. package/dist/ui/inject.js +21 -9
  125. package/package.json +6 -4
  126. package/src/astro/workflow-runner.ts +16 -0
  127. package/src/config/schema.ts +1 -0
  128. package/src/hooks/inject-provider.ts +94 -14
  129. package/src/index.ts +7 -0
  130. package/src/shared/metrics.ts +148 -0
  131. package/src/state/db.ts +10 -1
  132. package/src/state/schema.ts +8 -1
  133. package/src/tools/health.ts +99 -0
  134. package/src/tools/index.ts +12 -3
  135. package/src/tools/init.ts +7 -6
  136. package/src/tools/metrics.ts +71 -0
  137. package/src/tools/repair.ts +8 -4
  138. package/src/tools/reset.ts +100 -0
  139. package/src/tools/stage.ts +1 -0
  140. package/src/tools/status.ts +2 -1
  141. package/src/tools/story.ts +1 -0
  142. package/src/tools/workflow.ts +2 -0
  143. package/src/ui/inject.ts +21 -9
  144. package/src/workflow/repair.ts +2 -2
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ type ContinuationEnforcer = {
24
24
  type ToolOutputTruncator = (input: any, output: any | null) => Promise<void>;
25
25
  type InjectProvider = {
26
26
  onChatMessage: (input: any) => Promise<void>;
27
+ onToolAfter: (input: any) => Promise<void>;
27
28
  };
28
29
  type ToastManager = {
29
30
  show: (toast: ToastOptions) => Promise<void>;
@@ -294,6 +295,11 @@ const Astrocode: Plugin = async (ctx) => {
294
295
  },
295
296
 
296
297
  "tool.execute.after": async (input: any, output: any) => {
298
+ // Inject eligible injects after tool execution (not just on chat messages)
299
+ if (injectProvider && hookEnabled("inject-provider")) {
300
+ await injectProvider.onToolAfter(input);
301
+ }
302
+
297
303
  // Truncate huge tool outputs to artifacts
298
304
  if (truncatorHook && hookEnabled("tool-output-truncator")) {
299
305
  await truncatorHook(input, output ?? null);
@@ -325,6 +331,7 @@ const Astrocode: Plugin = async (ctx) => {
325
331
 
326
332
  // Best-effort cleanup
327
333
  close: async () => {
334
+ // Close database connection
328
335
  if (db && typeof db.close === "function") {
329
336
  try {
330
337
  db.close();
@@ -0,0 +1,148 @@
1
+ // src/shared/metrics.ts
2
+
3
+ export interface TransactionMetrics {
4
+ startTime: number;
5
+ duration: number;
6
+ success: boolean;
7
+ nestedDepth: number;
8
+ operation?: string;
9
+ }
10
+
11
+ export interface InjectionMetrics {
12
+ sessionId: string;
13
+ attempts: number;
14
+ duration: number;
15
+ success: boolean;
16
+ agent?: string;
17
+ }
18
+
19
+ export interface SystemMetrics {
20
+ transactions: TransactionMetrics[];
21
+ injections: InjectionMetrics[];
22
+ errors: Array<{ type: string; message: string; timestamp: number }>;
23
+ }
24
+
25
+ class MetricsCollector {
26
+ private transactions: TransactionMetrics[] = [];
27
+ private injections: InjectionMetrics[] = [];
28
+ private errors: Array<{ type: string; message: string; timestamp: number }> = [];
29
+ private maxEntries = 1000; // Keep last 1000 entries per type
30
+
31
+ recordTransaction(metrics: TransactionMetrics) {
32
+ this.transactions.push(metrics);
33
+ if (this.transactions.length > this.maxEntries) {
34
+ this.transactions.shift();
35
+ }
36
+ }
37
+
38
+ recordInjection(metrics: InjectionMetrics) {
39
+ this.injections.push(metrics);
40
+ if (this.injections.length > this.maxEntries) {
41
+ this.injections.shift();
42
+ }
43
+ }
44
+
45
+ recordError(type: string, message: string) {
46
+ this.errors.push({ type, message, timestamp: Date.now() });
47
+ if (this.errors.length > this.maxEntries) {
48
+ this.errors.shift();
49
+ }
50
+ }
51
+
52
+ getMetrics(): SystemMetrics {
53
+ return {
54
+ transactions: [...this.transactions],
55
+ injections: [...this.injections],
56
+ errors: [...this.errors],
57
+ };
58
+ }
59
+
60
+ getTransactionStats() {
61
+ const txs = this.transactions;
62
+ if (txs.length === 0) return null;
63
+
64
+ const successful = txs.filter(t => t.success);
65
+ const failed = txs.filter(t => !t.success);
66
+
67
+ return {
68
+ total: txs.length,
69
+ successful: successful.length,
70
+ failed: failed.length,
71
+ successRate: successful.length / txs.length,
72
+ avgDuration: txs.reduce((sum, t) => sum + t.duration, 0) / txs.length,
73
+ avgNestedDepth: txs.reduce((sum, t) => sum + t.nestedDepth, 0) / txs.length,
74
+ minDuration: Math.min(...txs.map(t => t.duration)),
75
+ maxDuration: Math.max(...txs.map(t => t.duration)),
76
+ };
77
+ }
78
+
79
+ getInjectionStats() {
80
+ const injections = this.injections;
81
+ if (injections.length === 0) return null;
82
+
83
+ const successful = injections.filter(i => i.success);
84
+ const failed = injections.filter(i => !i.success);
85
+
86
+ return {
87
+ total: injections.length,
88
+ successful: successful.length,
89
+ failed: failed.length,
90
+ successRate: successful.length / injections.length,
91
+ avgAttempts: injections.reduce((sum, i) => sum + i.attempts, 0) / injections.length,
92
+ avgDuration: injections.reduce((sum, i) => sum + i.duration, 0) / injections.length,
93
+ totalRetries: injections.reduce((sum, i) => sum + Math.max(0, i.attempts - 1), 0),
94
+ };
95
+ }
96
+
97
+ clear() {
98
+ this.transactions = [];
99
+ this.injections = [];
100
+ this.errors = [];
101
+ }
102
+ }
103
+
104
+ // Global singleton
105
+ export const metrics = new MetricsCollector();
106
+
107
+ // Convenience functions
108
+ export function recordTransaction(metricsData: Omit<TransactionMetrics, 'startTime' | 'duration' | 'success'>) {
109
+ return {
110
+ start() {
111
+ return {
112
+ ...metricsData,
113
+ startTime: Date.now(),
114
+ };
115
+ },
116
+ end(startData: ReturnType<ReturnType<typeof recordTransaction>['start']>, success: boolean) {
117
+ const duration = Date.now() - startData.startTime;
118
+ metrics.recordTransaction({
119
+ ...startData,
120
+ duration,
121
+ success,
122
+ });
123
+ },
124
+ };
125
+ }
126
+
127
+ export function recordInjection(metricsData: Omit<InjectionMetrics, 'duration' | 'success'>) {
128
+ return {
129
+ start() {
130
+ return {
131
+ ...metricsData,
132
+ startTime: Date.now(),
133
+ };
134
+ },
135
+ end(startData: any, success: boolean) {
136
+ const duration = Date.now() - startData.startTime;
137
+ metrics.recordInjection({
138
+ ...startData,
139
+ duration,
140
+ success,
141
+ });
142
+ },
143
+ };
144
+ }
145
+
146
+ export function recordError(type: string, message: string) {
147
+ metrics.recordError(type, message);
148
+ }
package/src/state/db.ts CHANGED
@@ -5,6 +5,7 @@ import { SCHEMA_SQL, SCHEMA_VERSION } from "./schema";
5
5
  import { nowISO } from "../shared/time";
6
6
  import { info, warn } from "../shared/log";
7
7
  import { createDatabaseAdapter, DatabaseConnection } from "./adapters";
8
+ import { recordTransaction } from "../shared/metrics";
8
9
 
9
10
  export type SqliteDb = DatabaseConnection;
10
11
 
@@ -74,7 +75,7 @@ function savepointName(depth: number): string {
74
75
  }
75
76
 
76
77
  /** BEGIN IMMEDIATE transaction helper (re-entrant). */
77
- export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean }): T {
78
+ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean; operation?: string }): T {
78
79
  const adapter = createDatabaseAdapter();
79
80
  const available = adapter.isAvailable();
80
81
 
@@ -84,13 +85,17 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
84
85
  }
85
86
 
86
87
  const depth = getDepth(db);
88
+ const isNested = depth > 0;
89
+ const txRecorder = recordTransaction({ nestedDepth: depth, operation: opts?.operation });
87
90
 
88
91
  if (depth === 0) {
92
+ const txStart = txRecorder.start();
89
93
  db.exec("BEGIN IMMEDIATE");
90
94
  setDepth(db, 1);
91
95
  try {
92
96
  const out = fn();
93
97
  db.exec("COMMIT");
98
+ txRecorder.end(txStart, true);
94
99
  return out;
95
100
  } catch (e) {
96
101
  try {
@@ -98,6 +103,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
98
103
  } catch {
99
104
  // ignore
100
105
  }
106
+ txRecorder.end(txStart, false);
101
107
  throw e;
102
108
  } finally {
103
109
  setDepth(db, 0);
@@ -107,6 +113,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
107
113
  // Nested: use SAVEPOINT
108
114
  const nextDepth = depth + 1;
109
115
  const sp = savepointName(nextDepth);
116
+ const txStart = txRecorder.start();
110
117
 
111
118
  db.exec(`SAVEPOINT ${sp}`);
112
119
  setDepth(db, nextDepth);
@@ -114,6 +121,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
114
121
  try {
115
122
  const out = fn();
116
123
  db.exec(`RELEASE SAVEPOINT ${sp}`);
124
+ txRecorder.end(txStart, true);
117
125
  return out;
118
126
  } catch (e) {
119
127
  try {
@@ -126,6 +134,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
126
134
  } catch {
127
135
  // ignore
128
136
  }
137
+ txRecorder.end(txStart, false);
129
138
  throw e;
130
139
  } finally {
131
140
  setDepth(db, depth);
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Source of truth: SQLite file at .astro/astro.db
6
6
 
7
- export const SCHEMA_VERSION = 2;
7
+ export const SCHEMA_VERSION = 3; // v3: Added advisory lock support + database constraints
8
8
 
9
9
  export const SCHEMA_SQL = `
10
10
  PRAGMA foreign_keys = ON;
@@ -235,6 +235,13 @@ CREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_ke
235
235
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);
236
236
  CREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);
237
237
  CREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);
238
+
239
+ -- CONSTRAINT: Only one running run at a time (partial unique index)
240
+ -- This provides database-level safety when using advisory locks
241
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_single_running_run ON runs(status) WHERE status = 'running';
242
+
243
+ -- CONSTRAINT: Only one run can lock a story at a time (partial unique index)
244
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_single_story_lock ON stories(in_progress) WHERE in_progress = 1;
238
245
  CREATE INDEX IF NOT EXISTS idx_injects_scope_type_priority_updated ON injects(scope, type, priority DESC, updated_at DESC);
239
246
  CREATE INDEX IF NOT EXISTS idx_injects_expires ON injects(expires_at) WHERE expires_at IS NOT NULL;
240
247
  CREATE INDEX IF NOT EXISTS idx_injects_sha256 ON injects(sha256) WHERE sha256 IS NOT NULL;
@@ -0,0 +1,99 @@
1
+ // src/tools/health.ts
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import type { AstrocodeConfig } from "../config/schema";
4
+ import type { SqliteDb } from "../state/db";
5
+ import { getSchemaVersion } from "../state/db";
6
+ import { getActiveRun } from "../workflow/state-machine";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ export function createAstroHealthTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
11
+ const { ctx, config, db } = opts;
12
+
13
+ return tool({
14
+ description: "Check Astrocode health: DB status, locks, schema, active runs, recent events.",
15
+ args: {},
16
+ execute: async () => {
17
+ const lines: string[] = [];
18
+ const repoRoot = (ctx as any).directory || process.cwd();
19
+ const dbPath = config.db?.path || ".astro/astro.db";
20
+ const fullDbPath = path.resolve(repoRoot, dbPath);
21
+
22
+ // System info
23
+ lines.push("# Astrocode Health Check");
24
+ lines.push(`- PID: ${(process as any).pid || "unknown"}`);
25
+ lines.push(`- Repo: ${repoRoot}`);
26
+ lines.push(`- DB Path: ${fullDbPath}`);
27
+
28
+ // DB file status
29
+ const dbExists = fs.existsSync(fullDbPath);
30
+ const walExists = fs.existsSync(`${fullDbPath}-wal`);
31
+ const shmExists = fs.existsSync(`${fullDbPath}-shm`);
32
+
33
+ lines.push(`- DB Files:`);
34
+ lines.push(` - Main: ${dbExists ? "EXISTS" : "MISSING"}`);
35
+ lines.push(` - WAL: ${walExists ? "EXISTS" : "MISSING"}`);
36
+ lines.push(` - SHM: ${shmExists ? "EXISTS" : "MISSING"}`);
37
+
38
+ if (!dbExists) {
39
+ lines.push(`- STATUS: DB MISSING - run astro_init first`);
40
+ return lines.join("\n");
41
+ }
42
+
43
+ // Schema version
44
+ try {
45
+ const schemaVersion = getSchemaVersion(db);
46
+ lines.push(`- Schema Version: ${schemaVersion}`);
47
+ } catch (e) {
48
+ lines.push(`- Schema Version: ERROR (${String(e)})`);
49
+ lines.push(`- STATUS: DB CORRUPTED`);
50
+ return lines.join("\n");
51
+ }
52
+
53
+ // Active run
54
+ try {
55
+ const activeRun = getActiveRun(db);
56
+ if (activeRun) {
57
+ lines.push(`- Active Run: ${activeRun.run_id} (${activeRun.status})`);
58
+ lines.push(` - Story: ${activeRun.story_key}`);
59
+ lines.push(` - Stage: ${activeRun.current_stage_key || "none"}`);
60
+ lines.push(` - Started: ${activeRun.started_at}`);
61
+ } else {
62
+ lines.push(`- Active Run: NONE`);
63
+ }
64
+ } catch (e) {
65
+ lines.push(`- Active Run: ERROR (${String(e)})`);
66
+ }
67
+
68
+ // Recent events
69
+ try {
70
+ const events = db.prepare(`
71
+ SELECT event_id, run_id, stage_key, type, created_at
72
+ FROM events
73
+ ORDER BY created_at DESC
74
+ LIMIT 10
75
+ `).all() as any[];
76
+
77
+ lines.push(`- Recent Events (${events.length}):`);
78
+ for (const event of events) {
79
+ const stage = event.stage_key ? `/${event.stage_key}` : "";
80
+ lines.push(` - ${event.created_at}: ${event.type} (${event.run_id || "global"}${stage})`);
81
+ }
82
+ } catch (e) {
83
+ lines.push(`- Recent Events: ERROR (${String(e)})`);
84
+ }
85
+
86
+ // Status summary
87
+ lines.push(``);
88
+ lines.push(`## Status`);
89
+ lines.push(`✅ DB accessible`);
90
+ lines.push(`✅ Schema valid`);
91
+
92
+ if (walExists || shmExists) {
93
+ lines.push(`âš ī¸ WAL/SHM files present - indicates unclean shutdown or active transaction`);
94
+ }
95
+
96
+ return lines.join("\n");
97
+ },
98
+ });
99
+ }
@@ -12,6 +12,9 @@ import { createAstroStageStartTool, createAstroStageCompleteTool, createAstroSta
12
12
  import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArtifactGetTool } from "./artifacts";
13
13
  import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool, createAstroInjectEligibleTool, createAstroInjectDebugDueTool } from "./injects";
14
14
  import { createAstroRepairTool } from "./repair";
15
+ import { createAstroHealthTool } from "./health";
16
+ import { createAstroResetTool } from "./reset";
17
+ import { createAstroMetricsTool } from "./metrics";
15
18
 
16
19
  import { AgentConfig } from "@opencode-ai/sdk";
17
20
 
@@ -35,9 +38,12 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
35
38
 
36
39
  const tools: Record<string, ToolDefinition> = {};
37
40
 
38
- // Always available tools (work without database - guaranteed DB-independent)
39
- tools.astro_status = createAstroStatusTool({ ctx, config });
40
- tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
41
+ // Always available tools (work without database - guaranteed DB-independent)
42
+ tools.astro_status = createAstroStatusTool({ ctx, config });
43
+ tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
44
+ tools.astro_health = createAstroHealthTool({ ctx, config, db });
45
+ tools.astro_reset = createAstroResetTool({ ctx, config, db });
46
+ tools.astro_metrics = createAstroMetricsTool({ ctx, config });
41
47
 
42
48
  // Recovery tool - available even in limited mode to allow DB initialization
43
49
  tools.astro_init = createAstroInitTool({ ctx, config, runtime });
@@ -100,6 +106,9 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
100
106
  ["_astro_inject_eligible", "astro_inject_eligible"],
101
107
  ["_astro_inject_debug_due", "astro_inject_debug_due"],
102
108
  ["_astro_repair", "astro_repair"],
109
+ ["_astro_health", "astro_health"],
110
+ ["_astro_reset", "astro_reset"],
111
+ ["_astro_metrics", "astro_metrics"],
103
112
  ];
104
113
 
105
114
  // Only add aliases for tools that exist
package/src/tools/init.ts CHANGED
@@ -8,6 +8,7 @@ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
8
8
  import { nowISO } from "../shared/time";
9
9
  import { sha256Hex } from "../shared/hash";
10
10
 
11
+
11
12
  type RuntimeState = {
12
13
  db: SqliteDb | null;
13
14
  limitedMode: boolean;
@@ -29,14 +30,14 @@ export function createAstroInitTool(opts: { ctx: any; config: AstrocodeConfig; r
29
30
  },
30
31
  execute: async ({ ensure_spec, spec_placeholder }) => {
31
32
  const repoRoot = ctx.directory as string;
32
- const paths = getAstroPaths(repoRoot, config.db.path);
33
- ensureAstroDirs(paths);
33
+ const paths = getAstroPaths(repoRoot, config.db.path);
34
+ ensureAstroDirs(paths);
34
35
 
35
- const hadDbAlready = !!runtime.db;
36
- let db: SqliteDb | null = runtime.db;
37
- let publishedToRuntime = false;
36
+ const hadDbAlready = !!runtime.db;
37
+ let db: SqliteDb | null = runtime.db;
38
+ let publishedToRuntime = false;
38
39
 
39
- try {
40
+ try {
40
41
  if (!db) {
41
42
  try {
42
43
  db = openSqlite(paths.dbPath, { busyTimeoutMs: config.db.busy_timeout_ms });
@@ -0,0 +1,71 @@
1
+ // src/tools/metrics.ts
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import { metrics } from "../shared/metrics";
4
+
5
+ type CreateAstroMetricsToolOptions = {
6
+ ctx: any;
7
+ config: any;
8
+ };
9
+
10
+ export function createAstroMetricsTool(opts: CreateAstroMetricsToolOptions): ToolDefinition {
11
+ return tool({
12
+ description: "Get performance metrics for Astrocode operations including transaction times, injection success rates, and error statistics.",
13
+ args: {},
14
+ execute: async () => {
15
+ return runMetricsTool();
16
+ },
17
+ });
18
+ }
19
+
20
+ function runMetricsTool(): string {
21
+ const stats = metrics.getMetrics();
22
+ const txStats = metrics.getTransactionStats();
23
+ const injectionStats = metrics.getInjectionStats();
24
+
25
+ let output = "# Astrocode Performance Metrics\n\n";
26
+
27
+ // Transaction Stats
28
+ if (txStats) {
29
+ output += "## Database Transactions\n\n";
30
+ output += `**Total:** ${txStats.total}\n`;
31
+ output += `**Success Rate:** ${(txStats.successRate * 100).toFixed(1)}% (${txStats.successful}/${txStats.total})\n`;
32
+ output += `**Average Duration:** ${txStats.avgDuration.toFixed(2)}ms\n`;
33
+ output += `**Duration Range:** ${txStats.minDuration}ms - ${txStats.maxDuration}ms\n`;
34
+ output += `**Average Nesting Depth:** ${txStats.avgNestedDepth.toFixed(1)}\n\n`;
35
+ } else {
36
+ output += "## Database Transactions\n\nNo transaction data available.\n\n";
37
+ }
38
+
39
+ // Injection Stats
40
+ if (injectionStats) {
41
+ output += "## UI Injections\n\n";
42
+ output += `**Total:** ${injectionStats.total}\n`;
43
+ output += `**Success Rate:** ${(injectionStats.successRate * 100).toFixed(1)}% (${injectionStats.successful}/${injectionStats.total})\n`;
44
+ output += `**Average Attempts:** ${injectionStats.avgAttempts.toFixed(1)}\n`;
45
+ output += `**Total Retries:** ${injectionStats.totalRetries}\n`;
46
+ output += `**Average Duration:** ${injectionStats.avgDuration.toFixed(2)}ms\n\n`;
47
+ } else {
48
+ output += "## UI Injections\n\nNo injection data available.\n\n";
49
+ }
50
+
51
+ // Recent Errors
52
+ if (stats.errors.length > 0) {
53
+ output += "## Recent Errors\n\n";
54
+ const recentErrors = stats.errors.slice(-10); // Last 10 errors
55
+ for (const error of recentErrors) {
56
+ const timestamp = new Date(error.timestamp).toISOString();
57
+ output += `- **[${error.type}]** ${timestamp}: ${error.message}\n`;
58
+ }
59
+ output += "\n";
60
+ } else {
61
+ output += "## Recent Errors\n\nNo errors recorded.\n\n";
62
+ }
63
+
64
+ // Raw Data Summary
65
+ output += "## Data Summary\n\n";
66
+ output += `**Transactions Tracked:** ${stats.transactions.length}\n`;
67
+ output += `**Injections Tracked:** ${stats.injections.length}\n`;
68
+ output += `**Errors Recorded:** ${stats.errors.length}\n`;
69
+
70
+ return output;
71
+ }
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
2
3
  import type { AstrocodeConfig } from "../config/schema";
3
4
  import type { SqliteDb } from "../state/db";
@@ -6,6 +7,7 @@ import { repairState, formatRepairReport } from "../workflow/repair";
6
7
  import { putArtifact } from "../workflow/artifacts";
7
8
  import { nowISO } from "../shared/time";
8
9
 
10
+
9
11
  export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
10
12
  const { ctx, config, db } = opts;
11
13
 
@@ -16,16 +18,18 @@ export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig;
16
18
  },
17
19
  execute: async ({ write_report_artifact }) => {
18
20
  const repoRoot = ctx.directory as string;
21
+
22
+ // Repair database state
19
23
  const report = withTx(db, () => repairState(db, config));
20
- const md = formatRepairReport(report);
24
+ const fullMd = formatRepairReport(report);
21
25
 
22
26
  if (write_report_artifact) {
23
27
  const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
24
- const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: md, meta: { kind: "repair" } });
25
- return md + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
28
+ const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: fullMd, meta: { kind: "repair" } });
29
+ return fullMd + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
26
30
  }
27
31
 
28
- return md;
32
+ return fullMd;
29
33
  },
30
34
  });
31
35
  }
@@ -0,0 +1,100 @@
1
+ // src/tools/reset.ts
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import type { AstrocodeConfig } from "../config/schema";
4
+ import type { SqliteDb } from "../state/db";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ export function createAstroResetTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
9
+ const { ctx, config, db } = opts;
10
+
11
+ return tool({
12
+ description: "Reset Astrocode database: safely delete all DB files and WAL/SHM after killing concurrent processes.",
13
+ args: {
14
+ confirm: tool.schema.string().default("").describe("Type 'RESET' to confirm destructive operation"),
15
+ },
16
+ execute: async ({ confirm }) => {
17
+ if (confirm !== "RESET") {
18
+ return [
19
+ "❌ Reset cancelled - confirmation required",
20
+ "",
21
+ "This operation will:",
22
+ "- Delete .astro/astro.db",
23
+ "- Delete .astro/astro.db-wal (if exists)",
24
+ "- Delete .astro/astro.db-shm (if exists)",
25
+ "- Lose all workflow data, stories, runs, artifacts",
26
+ "",
27
+ "To confirm: astro_reset(confirm=\"RESET\")",
28
+ ].join("\n");
29
+ }
30
+
31
+ const repoRoot = (ctx as any).directory || process.cwd();
32
+ const dbPath = config.db?.path || ".astro/astro.db";
33
+ const fullDbPath = path.resolve(repoRoot, dbPath);
34
+
35
+ const lines: string[] = [];
36
+ lines.push("đŸ—‘ī¸ Astrocode Database Reset");
37
+ lines.push(`- Repo: ${repoRoot}`);
38
+ lines.push(`- Target: ${fullDbPath}`);
39
+
40
+ // Check for lock file
41
+ const lockPath = `${repoRoot}/.astro/astro.lock`;
42
+ if (fs.existsSync(lockPath)) {
43
+ try {
44
+ const lockContent = fs.readFileSync(lockPath, "utf8").trim();
45
+ const pid = parseInt(lockContent.split(" ")[0]);
46
+
47
+ lines.push(`- Lock file found for PID ${pid}`);
48
+
49
+ // Try to kill the process
50
+ try {
51
+ (process as any).kill(pid, 'SIGTERM');
52
+ lines.push(`- Sent SIGTERM to PID ${pid}, waiting 2s...`);
53
+ await new Promise(resolve => setTimeout(resolve, 2000));
54
+ } catch (e) {
55
+ lines.push(`- Could not kill PID ${pid}: ${String(e)}`);
56
+ }
57
+ } catch (e) {
58
+ lines.push(`- Error reading lock file: ${String(e)}`);
59
+ }
60
+ }
61
+
62
+ // Delete DB files
63
+ const filesToDelete = [
64
+ fullDbPath,
65
+ `${fullDbPath}-wal`,
66
+ `${fullDbPath}-shm`,
67
+ lockPath,
68
+ ];
69
+
70
+ let deletedCount = 0;
71
+ for (const filePath of filesToDelete) {
72
+ try {
73
+ if (fs.existsSync(filePath)) {
74
+ fs.unlinkSync(filePath);
75
+ lines.push(`- Deleted: ${path.relative(repoRoot, filePath)}`);
76
+ deletedCount++;
77
+ } else {
78
+ lines.push(`- Skipped: ${path.relative(repoRoot, filePath)} (not found)`);
79
+ }
80
+ } catch (e) {
81
+ lines.push(`- Failed to delete ${path.relative(repoRoot, filePath)}: ${String(e)}`);
82
+ }
83
+ }
84
+
85
+ lines.push(``);
86
+ if (deletedCount > 0) {
87
+ lines.push(`✅ Reset complete - ${deletedCount} files deleted`);
88
+ lines.push(``);
89
+ lines.push(`Next steps:`);
90
+ lines.push(`1. Run: astro_init`);
91
+ lines.push(`2. Run: astro_status`);
92
+ lines.push(`3. Import your stories and restart workflow`);
93
+ } else {
94
+ lines.push(`â„šī¸ No files found to delete`);
95
+ }
96
+
97
+ return lines.join("\n");
98
+ },
99
+ });
100
+ }
@@ -14,6 +14,7 @@ import { failRun, getActiveRun, getStageRuns, startStage, completeRun } from "..
14
14
  import { newEventId, newId } from "../state/ids";
15
15
  import { insertStory } from "../workflow/story-helpers";
16
16
 
17
+
17
18
  function nextStageKey(pipeline: StageKey[], current: StageKey): StageKey | null {
18
19
  const i = pipeline.indexOf(current);
19
20
  if (i === -1) return null;
@@ -3,6 +3,7 @@ import type { AstrocodeConfig } from "../config/schema";
3
3
  import type { SqliteDb } from "../state/db";
4
4
  import { decideNextAction, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
5
5
 
6
+
6
7
  function statusIcon(status: string): string {
7
8
  switch (status) {
8
9
  case "running":
@@ -36,7 +37,7 @@ function stageIcon(status: string): string {
36
37
  }
37
38
 
38
39
  export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig; db?: SqliteDb | null }): ToolDefinition {
39
- const { config, db } = opts;
40
+ const { ctx, config, db } = opts;
40
41
 
41
42
  return tool({
42
43
  description: "Show a compact Astrocode status dashboard: active run/stage, pipeline, story board counts, and next action.",
@@ -8,6 +8,7 @@ import type { StoryState } from "../state/types";
8
8
  import { insertStory } from "../workflow/story-helpers";
9
9
 
10
10
 
11
+
11
12
  export function createAstroStoryQueueTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
12
13
  const { db } = opts;
13
14