astrocode-workflow 0.3.0 → 0.3.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 (139) 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/config/config-handler.d.ts +4 -0
  13. package/dist/src/config/config-handler.js +46 -0
  14. package/dist/src/config/defaults.d.ts +3 -0
  15. package/dist/src/config/defaults.js +3 -0
  16. package/dist/src/config/loader.d.ts +11 -0
  17. package/dist/src/config/loader.js +82 -0
  18. package/dist/src/config/schema.d.ts +194 -0
  19. package/dist/src/config/schema.js +223 -0
  20. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  21. package/dist/src/hooks/continuation-enforcer.js +190 -0
  22. package/dist/src/hooks/inject-provider.d.ts +22 -0
  23. package/dist/src/hooks/inject-provider.js +120 -0
  24. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  25. package/dist/src/hooks/tool-output-truncator.js +57 -0
  26. package/dist/src/index.d.ts +3 -0
  27. package/dist/src/index.js +308 -0
  28. package/dist/src/shared/deep-merge.d.ts +8 -0
  29. package/dist/src/shared/deep-merge.js +25 -0
  30. package/dist/src/shared/hash.d.ts +1 -0
  31. package/dist/src/shared/hash.js +4 -0
  32. package/dist/src/shared/log.d.ts +7 -0
  33. package/dist/src/shared/log.js +24 -0
  34. package/dist/src/shared/metrics.d.ts +66 -0
  35. package/dist/src/shared/metrics.js +112 -0
  36. package/dist/src/shared/model-tuning.d.ts +9 -0
  37. package/dist/src/shared/model-tuning.js +28 -0
  38. package/dist/src/shared/paths.d.ts +19 -0
  39. package/dist/src/shared/paths.js +64 -0
  40. package/dist/src/shared/text.d.ts +4 -0
  41. package/dist/src/shared/text.js +19 -0
  42. package/dist/src/shared/time.d.ts +1 -0
  43. package/dist/src/shared/time.js +3 -0
  44. package/dist/src/state/adapters/index.d.ts +41 -0
  45. package/dist/src/state/adapters/index.js +115 -0
  46. package/dist/src/state/db.d.ts +16 -0
  47. package/dist/src/state/db.js +225 -0
  48. package/dist/src/state/ids.d.ts +8 -0
  49. package/dist/src/state/ids.js +25 -0
  50. package/dist/src/state/repo-lock.d.ts +3 -0
  51. package/dist/src/state/repo-lock.js +29 -0
  52. package/dist/src/state/schema.d.ts +2 -0
  53. package/dist/src/state/schema.js +251 -0
  54. package/dist/src/state/types.d.ts +71 -0
  55. package/dist/src/state/types.js +1 -0
  56. package/dist/src/tools/artifacts.d.ts +18 -0
  57. package/dist/src/tools/artifacts.js +71 -0
  58. package/dist/src/tools/health.d.ts +8 -0
  59. package/dist/src/tools/health.js +119 -0
  60. package/dist/src/tools/index.d.ts +20 -0
  61. package/dist/src/tools/index.js +94 -0
  62. package/dist/src/tools/init.d.ts +17 -0
  63. package/dist/src/tools/init.js +96 -0
  64. package/dist/src/tools/injects.d.ts +53 -0
  65. package/dist/src/tools/injects.js +325 -0
  66. package/dist/src/tools/metrics.d.ts +7 -0
  67. package/dist/src/tools/metrics.js +61 -0
  68. package/dist/src/tools/repair.d.ts +8 -0
  69. package/dist/src/tools/repair.js +25 -0
  70. package/dist/src/tools/reset.d.ts +8 -0
  71. package/dist/src/tools/reset.js +92 -0
  72. package/dist/src/tools/run.d.ts +13 -0
  73. package/dist/src/tools/run.js +54 -0
  74. package/dist/src/tools/spec.d.ts +12 -0
  75. package/dist/src/tools/spec.js +44 -0
  76. package/dist/src/tools/stage.d.ts +23 -0
  77. package/dist/src/tools/stage.js +371 -0
  78. package/dist/src/tools/status.d.ts +8 -0
  79. package/dist/src/tools/status.js +125 -0
  80. package/dist/src/tools/story.d.ts +23 -0
  81. package/dist/src/tools/story.js +85 -0
  82. package/dist/src/tools/workflow.d.ts +13 -0
  83. package/dist/src/tools/workflow.js +355 -0
  84. package/dist/src/ui/inject.d.ts +12 -0
  85. package/dist/src/ui/inject.js +107 -0
  86. package/dist/src/ui/toasts.d.ts +13 -0
  87. package/dist/src/ui/toasts.js +39 -0
  88. package/dist/src/workflow/artifacts.d.ts +24 -0
  89. package/dist/src/workflow/artifacts.js +45 -0
  90. package/dist/src/workflow/baton.d.ts +72 -0
  91. package/dist/src/workflow/baton.js +166 -0
  92. package/dist/src/workflow/context.d.ts +20 -0
  93. package/dist/src/workflow/context.js +113 -0
  94. package/dist/src/workflow/directives.d.ts +39 -0
  95. package/dist/src/workflow/directives.js +137 -0
  96. package/dist/src/workflow/repair.d.ts +8 -0
  97. package/dist/src/workflow/repair.js +99 -0
  98. package/dist/src/workflow/state-machine.d.ts +86 -0
  99. package/dist/src/workflow/state-machine.js +216 -0
  100. package/dist/src/workflow/story-helpers.d.ts +9 -0
  101. package/dist/src/workflow/story-helpers.js +13 -0
  102. package/dist/state/db.d.ts +1 -0
  103. package/dist/state/db.js +9 -0
  104. package/dist/state/repo-lock.d.ts +3 -0
  105. package/dist/state/repo-lock.js +29 -0
  106. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  107. package/dist/test/integration/db-transactions.test.js +126 -0
  108. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  109. package/dist/test/integration/injection-metrics.test.js +129 -0
  110. package/dist/tools/health.d.ts +8 -0
  111. package/dist/tools/health.js +119 -0
  112. package/dist/tools/index.js +9 -0
  113. package/dist/tools/metrics.d.ts +7 -0
  114. package/dist/tools/metrics.js +61 -0
  115. package/dist/tools/reset.d.ts +8 -0
  116. package/dist/tools/reset.js +92 -0
  117. package/dist/tools/workflow.js +210 -215
  118. package/dist/ui/inject.d.ts +6 -0
  119. package/dist/ui/inject.js +86 -67
  120. package/dist/workflow/state-machine.d.ts +32 -32
  121. package/dist/workflow/state-machine.js +85 -170
  122. package/package.json +6 -3
  123. package/src/index.ts +8 -0
  124. package/src/shared/metrics.ts +148 -0
  125. package/src/state/db.ts +10 -1
  126. package/src/state/repo-lock.ts +158 -0
  127. package/src/tools/health.ts +128 -0
  128. package/src/tools/index.ts +12 -3
  129. package/src/tools/init.ts +26 -14
  130. package/src/tools/metrics.ts +71 -0
  131. package/src/tools/repair.ts +21 -8
  132. package/src/tools/reset.ts +100 -0
  133. package/src/tools/stage.ts +12 -0
  134. package/src/tools/status.ts +17 -3
  135. package/src/tools/story.ts +41 -15
  136. package/src/tools/workflow.ts +123 -121
  137. package/src/ui/inject.ts +113 -79
  138. package/src/workflow/state-machine.ts +123 -227
  139. package/src/tools/workflow.ts.backup +0 -681
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { tool } from "@opencode-ai/plugin/tool";
4
+ import { ensureSchema, openSqlite, configurePragmas } from "../state/db";
5
+ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
6
+ import { nowISO } from "../shared/time";
7
+ import { sha256Hex } from "../shared/hash";
8
+ export function createAstroInitTool(opts) {
9
+ const { ctx, config, runtime } = opts;
10
+ return tool({
11
+ description: "Initialize Astrocode vNext in this repo: create .astro directories, ensure SQLite schema, and create a placeholder spec if missing. Idempotent.",
12
+ args: {
13
+ ensure_spec: tool.schema.boolean().default(true),
14
+ spec_placeholder: tool.schema.string().default("# Project Spec\n\n(Write the spec here.)\n"),
15
+ },
16
+ execute: async ({ ensure_spec, spec_placeholder }) => {
17
+ const repoRoot = ctx.directory;
18
+ const paths = getAstroPaths(repoRoot, config.db.path);
19
+ ensureAstroDirs(paths);
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);
50
+ const now = nowISO();
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 { }
92
+ }
93
+ }
94
+ },
95
+ });
96
+ }
@@ -0,0 +1,53 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ export declare function createAstroInjectPutTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
9
+ export declare function createAstroInjectListTool(opts: {
10
+ ctx: any;
11
+ config: AstrocodeConfig;
12
+ db: SqliteDb;
13
+ }): ToolDefinition;
14
+ export declare function createAstroInjectGetTool(opts: {
15
+ ctx: any;
16
+ config: AstrocodeConfig;
17
+ db: SqliteDb;
18
+ }): ToolDefinition;
19
+ export declare function createAstroInjectSearchTool(opts: {
20
+ ctx: any;
21
+ config: AstrocodeConfig;
22
+ db: SqliteDb;
23
+ }): ToolDefinition;
24
+ export type InjectRow = {
25
+ inject_id: string;
26
+ type: string;
27
+ title: string;
28
+ body_md: string;
29
+ tags_json: string;
30
+ scope: string;
31
+ source: string;
32
+ priority: number;
33
+ expires_at: string | null;
34
+ sha256: string;
35
+ created_at: string;
36
+ updated_at: string;
37
+ };
38
+ export declare function selectEligibleInjects(db: SqliteDb, opts: {
39
+ nowIso: string;
40
+ scopeAllowlist: string[];
41
+ typeAllowlist: string[];
42
+ limit?: number;
43
+ }): InjectRow[];
44
+ export declare function createAstroInjectEligibleTool(opts: {
45
+ ctx: any;
46
+ config: AstrocodeConfig;
47
+ db: SqliteDb;
48
+ }): ToolDefinition;
49
+ export declare function createAstroInjectDebugDueTool(opts: {
50
+ ctx: any;
51
+ config: AstrocodeConfig;
52
+ db: SqliteDb;
53
+ }): ToolDefinition;
@@ -0,0 +1,325 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { withTx } from "../state/db";
3
+ import { nowISO } from "../shared/time";
4
+ import { sha256Hex } from "../shared/hash";
5
+ const VALID_INJECT_TYPES = ["note", "policy", "reminder", "debug"];
6
+ function validateInjectType(type) {
7
+ if (!VALID_INJECT_TYPES.includes(type)) {
8
+ throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(", ")}`);
9
+ }
10
+ return type;
11
+ }
12
+ function validateTimestamp(timestamp) {
13
+ if (!timestamp)
14
+ return null;
15
+ // Strict ISO 8601 UTC with Z suffix, sortable as string.
16
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
17
+ if (!isoRegex.test(timestamp)) {
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
+ }
20
+ const parsed = Date.parse(timestamp);
21
+ if (!Number.isFinite(parsed)) {
22
+ throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
23
+ }
24
+ return timestamp;
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
+ }
40
+ function newInjectId() {
41
+ return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
42
+ }
43
+ export function createAstroInjectPutTool(opts) {
44
+ const { db } = opts;
45
+ return tool({
46
+ description: "Create/update an inject (note/policy) stored in the DB. Useful for persistent rules.",
47
+ args: {
48
+ inject_id: tool.schema.string().optional(),
49
+ type: tool.schema.string().default("note"),
50
+ title: tool.schema.string().min(1),
51
+ body_md: tool.schema.string().min(1),
52
+ tags_json: tool.schema.string().default("[]"),
53
+ scope: tool.schema.string().default("repo"),
54
+ source: tool.schema.string().default("user"),
55
+ priority: tool.schema.number().int().default(50),
56
+ expires_at: tool.schema.string().nullable().optional(),
57
+ },
58
+ execute: async ({ inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at }) => {
59
+ const id = inject_id ?? newInjectId();
60
+ const now = nowISO();
61
+ const sha = sha256Hex(body_md);
62
+ const validatedType = validateInjectType(type);
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
+ }
72
+ return withTx(db, () => {
73
+ const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id);
74
+ if (existing) {
75
+ db.prepare(`
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
+ )
90
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
91
+ ON CONFLICT(inject_id) DO UPDATE SET
92
+ type=excluded.type,
93
+ title=excluded.title,
94
+ body_md=excluded.body_md,
95
+ tags_json=excluded.tags_json,
96
+ scope=excluded.scope,
97
+ source=excluded.source,
98
+ priority=excluded.priority,
99
+ expires_at=excluded.expires_at,
100
+ sha256=excluded.sha256,
101
+ updated_at=excluded.updated_at
102
+ `).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
103
+ return `✅ Updated inject ${id}: ${title}`;
104
+ }
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);
106
+ return `✅ Created inject ${id}: ${title}`;
107
+ });
108
+ },
109
+ });
110
+ }
111
+ export function createAstroInjectListTool(opts) {
112
+ const { db } = opts;
113
+ return tool({
114
+ description: "List injects (optionally filtered by scope/type).",
115
+ args: {
116
+ scope: tool.schema.string().optional(),
117
+ type: tool.schema.string().optional(),
118
+ limit: tool.schema.number().int().positive().default(50),
119
+ },
120
+ execute: async ({ scope, type, limit }) => {
121
+ const where = [];
122
+ const params = [];
123
+ if (scope) {
124
+ where.push("scope = ?");
125
+ params.push(scope);
126
+ }
127
+ if (type) {
128
+ // Keep list tool permissive (debugging), but still prevents obvious garbage if used.
129
+ validateInjectType(type);
130
+ where.push("type = ?");
131
+ params.push(type);
132
+ }
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
+ `;
140
+ const rows = db.prepare(sql).all(...params, limit);
141
+ return JSON.stringify(rows, null, 2);
142
+ },
143
+ });
144
+ }
145
+ export function createAstroInjectGetTool(opts) {
146
+ const { db } = opts;
147
+ return tool({
148
+ description: "Get an inject by id (full body).",
149
+ args: {
150
+ inject_id: tool.schema.string().min(1),
151
+ },
152
+ execute: async ({ inject_id }) => {
153
+ const row = db.prepare("SELECT * FROM injects WHERE inject_id=?").get(inject_id);
154
+ if (!row)
155
+ throw new Error(`Inject not found: ${inject_id}`);
156
+ return JSON.stringify(row, null, 2);
157
+ },
158
+ });
159
+ }
160
+ export function createAstroInjectSearchTool(opts) {
161
+ const { db } = opts;
162
+ return tool({
163
+ description: "Search injects by query substring over title/body/tags. Returns matches ordered by priority/recency.",
164
+ args: {
165
+ q: tool.schema.string().min(1),
166
+ scope: tool.schema.string().optional(),
167
+ limit: tool.schema.number().int().positive().default(20),
168
+ },
169
+ execute: async ({ q, scope, limit }) => {
170
+ const like = `%${q}%`;
171
+ const where = ["(title LIKE ? OR body_md LIKE ? OR tags_json LIKE ?)"];
172
+ const params = [like, like, like];
173
+ if (scope) {
174
+ where.push("scope = ?");
175
+ params.push(scope);
176
+ }
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
+ `;
184
+ const rows = db.prepare(sql).all(...params, limit);
185
+ return JSON.stringify(rows, null, 2);
186
+ },
187
+ });
188
+ }
189
+ export function selectEligibleInjects(db, opts) {
190
+ const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
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).
198
+ const scopeQs = scopeAllowlist.map(() => "?").join(", ");
199
+ const typeQs = typeAllowlist.map(() => "?").join(", ");
200
+ const sql = `
201
+ SELECT *
202
+ FROM injects
203
+ WHERE (expires_at IS NULL OR expires_at > ?)
204
+ AND scope IN (${scopeQs})
205
+ AND type IN (${typeQs})
206
+ ORDER BY priority DESC, updated_at DESC
207
+ LIMIT ?
208
+ `;
209
+ const params = [nowIso, ...scopeAllowlist, ...typeAllowlist, limit];
210
+ return db.prepare(sql).all(...params);
211
+ }
212
+ export function createAstroInjectEligibleTool(opts) {
213
+ const { db } = opts;
214
+ return tool({
215
+ description: "Debug: show which injects are eligible right now for injection.",
216
+ args: {
217
+ scopes_json: tool.schema.string().default('["repo","global"]'),
218
+ types_json: tool.schema.string().default('["note","policy"]'),
219
+ limit: tool.schema.number().int().positive().default(50),
220
+ },
221
+ execute: async ({ scopes_json, types_json, limit }) => {
222
+ const now = nowISO();
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);
228
+ const rows = selectEligibleInjects(db, {
229
+ nowIso: now,
230
+ scopeAllowlist: scopes,
231
+ typeAllowlist: types,
232
+ limit,
233
+ });
234
+ return JSON.stringify({ now, count: rows.length, rows }, null, 2);
235
+ },
236
+ });
237
+ }
238
+ export function createAstroInjectDebugDueTool(opts) {
239
+ const { db } = opts;
240
+ return tool({
241
+ description: "Debug: show comprehensive injection diagnostics - why injects were selected/skipped.",
242
+ args: {
243
+ scopes_json: tool.schema.string().default('["repo","global"]'),
244
+ types_json: tool.schema.string().default('["note","policy"]'),
245
+ },
246
+ execute: async ({ scopes_json, types_json }) => {
247
+ const now = nowISO();
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);
253
+ const allInjects = db.prepare("SELECT * FROM injects").all();
254
+ let total = allInjects.length;
255
+ let selected = 0;
256
+ let skippedExpired = 0;
257
+ let skippedScope = 0;
258
+ let skippedType = 0;
259
+ let skippedUnparseableExpiresAt = 0;
260
+ const excludedReasons = [];
261
+ const selectedInjects = [];
262
+ for (const inject of allInjects) {
263
+ const reasons = [];
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
+ }
275
+ }
276
+ if (!scopes.includes(inject.scope)) {
277
+ reasons.push("scope");
278
+ skippedScope++;
279
+ }
280
+ if (!types.includes(inject.type)) {
281
+ reasons.push("type");
282
+ skippedType++;
283
+ }
284
+ if (reasons.length > 0) {
285
+ excludedReasons.push({
286
+ inject_id: inject.inject_id,
287
+ title: inject.title,
288
+ reasons,
289
+ scope: inject.scope,
290
+ type: inject.type,
291
+ expires_at: inject.expires_at,
292
+ });
293
+ }
294
+ else {
295
+ selected++;
296
+ selectedInjects.push({
297
+ inject_id: inject.inject_id,
298
+ title: inject.title,
299
+ scope: inject.scope,
300
+ type: inject.type,
301
+ expires_at: inject.expires_at,
302
+ });
303
+ }
304
+ }
305
+ return JSON.stringify({
306
+ now,
307
+ scopes_considered: scopes,
308
+ types_considered: types,
309
+ summary: {
310
+ total_injects: total,
311
+ selected_eligible: selected,
312
+ excluded_total: total - selected,
313
+ skipped_breakdown: {
314
+ expired: skippedExpired,
315
+ expires_at_unparseable: skippedUnparseableExpiresAt,
316
+ scope: skippedScope,
317
+ type: skippedType,
318
+ },
319
+ },
320
+ selected_injects: selectedInjects,
321
+ excluded_injects: excludedReasons,
322
+ }, null, 2);
323
+ },
324
+ });
325
+ }
@@ -0,0 +1,7 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ type CreateAstroMetricsToolOptions = {
3
+ ctx: any;
4
+ config: any;
5
+ };
6
+ export declare function createAstroMetricsTool(opts: CreateAstroMetricsToolOptions): ToolDefinition;
7
+ export {};
@@ -0,0 +1,61 @@
1
+ // src/tools/metrics.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import { metrics } from "../shared/metrics";
4
+ export function createAstroMetricsTool(opts) {
5
+ return tool({
6
+ description: "Get performance metrics for Astrocode operations including transaction times, injection success rates, and error statistics.",
7
+ args: {},
8
+ execute: async () => {
9
+ return runMetricsTool();
10
+ },
11
+ });
12
+ }
13
+ function runMetricsTool() {
14
+ const stats = metrics.getMetrics();
15
+ const txStats = metrics.getTransactionStats();
16
+ const injectionStats = metrics.getInjectionStats();
17
+ let output = "# Astrocode Performance Metrics\n\n";
18
+ // Transaction Stats
19
+ if (txStats) {
20
+ output += "## Database Transactions\n\n";
21
+ output += `**Total:** ${txStats.total}\n`;
22
+ output += `**Success Rate:** ${(txStats.successRate * 100).toFixed(1)}% (${txStats.successful}/${txStats.total})\n`;
23
+ output += `**Average Duration:** ${txStats.avgDuration.toFixed(2)}ms\n`;
24
+ output += `**Duration Range:** ${txStats.minDuration}ms - ${txStats.maxDuration}ms\n`;
25
+ output += `**Average Nesting Depth:** ${txStats.avgNestedDepth.toFixed(1)}\n\n`;
26
+ }
27
+ else {
28
+ output += "## Database Transactions\n\nNo transaction data available.\n\n";
29
+ }
30
+ // Injection Stats
31
+ if (injectionStats) {
32
+ output += "## UI Injections\n\n";
33
+ output += `**Total:** ${injectionStats.total}\n`;
34
+ output += `**Success Rate:** ${(injectionStats.successRate * 100).toFixed(1)}% (${injectionStats.successful}/${injectionStats.total})\n`;
35
+ output += `**Average Attempts:** ${injectionStats.avgAttempts.toFixed(1)}\n`;
36
+ output += `**Total Retries:** ${injectionStats.totalRetries}\n`;
37
+ output += `**Average Duration:** ${injectionStats.avgDuration.toFixed(2)}ms\n\n`;
38
+ }
39
+ else {
40
+ output += "## UI Injections\n\nNo injection data available.\n\n";
41
+ }
42
+ // Recent Errors
43
+ if (stats.errors.length > 0) {
44
+ output += "## Recent Errors\n\n";
45
+ const recentErrors = stats.errors.slice(-10); // Last 10 errors
46
+ for (const error of recentErrors) {
47
+ const timestamp = new Date(error.timestamp).toISOString();
48
+ output += `- **[${error.type}]** ${timestamp}: ${error.message}\n`;
49
+ }
50
+ output += "\n";
51
+ }
52
+ else {
53
+ output += "## Recent Errors\n\nNo errors recorded.\n\n";
54
+ }
55
+ // Raw Data Summary
56
+ output += "## Data Summary\n\n";
57
+ output += `**Transactions Tracked:** ${stats.transactions.length}\n`;
58
+ output += `**Injections Tracked:** ${stats.injections.length}\n`;
59
+ output += `**Errors Recorded:** ${stats.errors.length}\n`;
60
+ return output;
61
+ }
@@ -0,0 +1,8 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ export declare function createAstroRepairTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
@@ -0,0 +1,25 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { withTx } from "../state/db";
3
+ import { repairState, formatRepairReport } from "../workflow/repair";
4
+ import { putArtifact } from "../workflow/artifacts";
5
+ import { nowISO } from "../shared/time";
6
+ export function createAstroRepairTool(opts) {
7
+ const { ctx, config, db } = opts;
8
+ return tool({
9
+ description: "Repair Astrocode invariants and recover from inconsistent DB state. Writes a repair report artifact.",
10
+ args: {
11
+ write_report_artifact: tool.schema.boolean().default(true),
12
+ },
13
+ execute: async ({ write_report_artifact }) => {
14
+ const repoRoot = ctx.directory;
15
+ const report = withTx(db, () => repairState(db, config));
16
+ const md = formatRepairReport(report);
17
+ if (write_report_artifact) {
18
+ const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
19
+ const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: md, meta: { kind: "repair" } });
20
+ return md + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
21
+ }
22
+ return md;
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,8 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ export declare function createAstroResetTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
@@ -0,0 +1,92 @@
1
+ // src/tools/reset.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ export function createAstroResetTool(opts) {
6
+ const { ctx, config, db } = opts;
7
+ return tool({
8
+ description: "Reset Astrocode database: safely delete all DB files and WAL/SHM after killing concurrent processes.",
9
+ args: {
10
+ confirm: tool.schema.string().default("").describe("Type 'RESET' to confirm destructive operation"),
11
+ },
12
+ execute: async ({ confirm }) => {
13
+ if (confirm !== "RESET") {
14
+ return [
15
+ "❌ Reset cancelled - confirmation required",
16
+ "",
17
+ "This operation will:",
18
+ "- Delete .astro/astro.db",
19
+ "- Delete .astro/astro.db-wal (if exists)",
20
+ "- Delete .astro/astro.db-shm (if exists)",
21
+ "- Lose all workflow data, stories, runs, artifacts",
22
+ "",
23
+ "To confirm: astro_reset(confirm=\"RESET\")",
24
+ ].join("\n");
25
+ }
26
+ const repoRoot = ctx.directory || process.cwd();
27
+ const dbPath = config.db?.path || ".astro/astro.db";
28
+ const fullDbPath = path.resolve(repoRoot, dbPath);
29
+ const lines = [];
30
+ lines.push("đŸ—‘ī¸ Astrocode Database Reset");
31
+ lines.push(`- Repo: ${repoRoot}`);
32
+ lines.push(`- Target: ${fullDbPath}`);
33
+ // Check for lock file
34
+ const lockPath = `${repoRoot}/.astro/astro.lock`;
35
+ if (fs.existsSync(lockPath)) {
36
+ try {
37
+ const lockContent = fs.readFileSync(lockPath, "utf8").trim();
38
+ const pid = parseInt(lockContent.split(" ")[0]);
39
+ lines.push(`- Lock file found for PID ${pid}`);
40
+ // Try to kill the process
41
+ try {
42
+ process.kill(pid, 'SIGTERM');
43
+ lines.push(`- Sent SIGTERM to PID ${pid}, waiting 2s...`);
44
+ await new Promise(resolve => setTimeout(resolve, 2000));
45
+ }
46
+ catch (e) {
47
+ lines.push(`- Could not kill PID ${pid}: ${String(e)}`);
48
+ }
49
+ }
50
+ catch (e) {
51
+ lines.push(`- Error reading lock file: ${String(e)}`);
52
+ }
53
+ }
54
+ // Delete DB files
55
+ const filesToDelete = [
56
+ fullDbPath,
57
+ `${fullDbPath}-wal`,
58
+ `${fullDbPath}-shm`,
59
+ lockPath,
60
+ ];
61
+ let deletedCount = 0;
62
+ for (const filePath of filesToDelete) {
63
+ try {
64
+ if (fs.existsSync(filePath)) {
65
+ fs.unlinkSync(filePath);
66
+ lines.push(`- Deleted: ${path.relative(repoRoot, filePath)}`);
67
+ deletedCount++;
68
+ }
69
+ else {
70
+ lines.push(`- Skipped: ${path.relative(repoRoot, filePath)} (not found)`);
71
+ }
72
+ }
73
+ catch (e) {
74
+ lines.push(`- Failed to delete ${path.relative(repoRoot, filePath)}: ${String(e)}`);
75
+ }
76
+ }
77
+ lines.push(``);
78
+ if (deletedCount > 0) {
79
+ lines.push(`✅ Reset complete - ${deletedCount} files deleted`);
80
+ lines.push(``);
81
+ lines.push(`Next steps:`);
82
+ lines.push(`1. Run: astro_init`);
83
+ lines.push(`2. Run: astro_status`);
84
+ lines.push(`3. Import your stories and restart workflow`);
85
+ }
86
+ else {
87
+ lines.push(`â„šī¸ No files found to delete`);
88
+ }
89
+ return lines.join("\n");
90
+ },
91
+ });
92
+ }