astrocode-workflow 0.0.1

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 (133) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +85 -0
  3. package/dist/agents/commands.d.ts +9 -0
  4. package/dist/agents/commands.js +121 -0
  5. package/dist/agents/prompts.d.ts +2 -0
  6. package/dist/agents/prompts.js +27 -0
  7. package/dist/agents/registry.d.ts +6 -0
  8. package/dist/agents/registry.js +223 -0
  9. package/dist/agents/types.d.ts +14 -0
  10. package/dist/agents/types.js +8 -0
  11. package/dist/config/config-handler.d.ts +4 -0
  12. package/dist/config/config-handler.js +46 -0
  13. package/dist/config/defaults.d.ts +3 -0
  14. package/dist/config/defaults.js +3 -0
  15. package/dist/config/loader.d.ts +11 -0
  16. package/dist/config/loader.js +48 -0
  17. package/dist/config/schema.d.ts +176 -0
  18. package/dist/config/schema.js +198 -0
  19. package/dist/hooks/continuation-enforcer.d.ts +26 -0
  20. package/dist/hooks/continuation-enforcer.js +166 -0
  21. package/dist/hooks/tool-output-truncator.d.ts +17 -0
  22. package/dist/hooks/tool-output-truncator.js +56 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +108 -0
  25. package/dist/shared/deep-merge.d.ts +8 -0
  26. package/dist/shared/deep-merge.js +25 -0
  27. package/dist/shared/hash.d.ts +1 -0
  28. package/dist/shared/hash.js +4 -0
  29. package/dist/shared/log.d.ts +7 -0
  30. package/dist/shared/log.js +24 -0
  31. package/dist/shared/model-tuning.d.ts +9 -0
  32. package/dist/shared/model-tuning.js +28 -0
  33. package/dist/shared/paths.d.ts +19 -0
  34. package/dist/shared/paths.js +51 -0
  35. package/dist/shared/text.d.ts +4 -0
  36. package/dist/shared/text.js +19 -0
  37. package/dist/shared/time.d.ts +1 -0
  38. package/dist/shared/time.js +3 -0
  39. package/dist/state/adapters/index.d.ts +39 -0
  40. package/dist/state/adapters/index.js +119 -0
  41. package/dist/state/db.d.ts +17 -0
  42. package/dist/state/db.js +83 -0
  43. package/dist/state/ids.d.ts +8 -0
  44. package/dist/state/ids.js +25 -0
  45. package/dist/state/schema.d.ts +2 -0
  46. package/dist/state/schema.js +247 -0
  47. package/dist/state/types.d.ts +70 -0
  48. package/dist/state/types.js +1 -0
  49. package/dist/tools/artifacts.d.ts +18 -0
  50. package/dist/tools/artifacts.js +71 -0
  51. package/dist/tools/index.d.ts +8 -0
  52. package/dist/tools/index.js +100 -0
  53. package/dist/tools/init.d.ts +8 -0
  54. package/dist/tools/init.js +41 -0
  55. package/dist/tools/injects.d.ts +23 -0
  56. package/dist/tools/injects.js +99 -0
  57. package/dist/tools/repair.d.ts +8 -0
  58. package/dist/tools/repair.js +25 -0
  59. package/dist/tools/run.d.ts +13 -0
  60. package/dist/tools/run.js +54 -0
  61. package/dist/tools/spec.d.ts +13 -0
  62. package/dist/tools/spec.js +41 -0
  63. package/dist/tools/stage.d.ts +23 -0
  64. package/dist/tools/stage.js +284 -0
  65. package/dist/tools/status.d.ts +8 -0
  66. package/dist/tools/status.js +107 -0
  67. package/dist/tools/story.d.ts +23 -0
  68. package/dist/tools/story.js +85 -0
  69. package/dist/tools/workflow.d.ts +8 -0
  70. package/dist/tools/workflow.js +197 -0
  71. package/dist/ui/inject.d.ts +5 -0
  72. package/dist/ui/inject.js +9 -0
  73. package/dist/ui/toasts.d.ts +13 -0
  74. package/dist/ui/toasts.js +39 -0
  75. package/dist/workflow/artifacts.d.ts +24 -0
  76. package/dist/workflow/artifacts.js +45 -0
  77. package/dist/workflow/baton.d.ts +66 -0
  78. package/dist/workflow/baton.js +101 -0
  79. package/dist/workflow/context.d.ts +12 -0
  80. package/dist/workflow/context.js +67 -0
  81. package/dist/workflow/directives.d.ts +37 -0
  82. package/dist/workflow/directives.js +111 -0
  83. package/dist/workflow/repair.d.ts +8 -0
  84. package/dist/workflow/repair.js +99 -0
  85. package/dist/workflow/state-machine.d.ts +43 -0
  86. package/dist/workflow/state-machine.js +127 -0
  87. package/dist/workflow/story-helpers.d.ts +9 -0
  88. package/dist/workflow/story-helpers.js +13 -0
  89. package/package.json +32 -0
  90. package/src/agents/commands.ts +137 -0
  91. package/src/agents/prompts.ts +28 -0
  92. package/src/agents/registry.ts +310 -0
  93. package/src/agents/types.ts +31 -0
  94. package/src/config/config-handler.ts +48 -0
  95. package/src/config/defaults.ts +4 -0
  96. package/src/config/loader.ts +55 -0
  97. package/src/config/schema.ts +236 -0
  98. package/src/hooks/continuation-enforcer.ts +217 -0
  99. package/src/hooks/tool-output-truncator.ts +82 -0
  100. package/src/index.ts +131 -0
  101. package/src/shared/deep-merge.ts +28 -0
  102. package/src/shared/hash.ts +5 -0
  103. package/src/shared/log.ts +30 -0
  104. package/src/shared/model-tuning.ts +48 -0
  105. package/src/shared/paths.ts +70 -0
  106. package/src/shared/text.ts +20 -0
  107. package/src/shared/time.ts +3 -0
  108. package/src/shims.node.d.ts +20 -0
  109. package/src/state/adapters/index.ts +155 -0
  110. package/src/state/db.ts +105 -0
  111. package/src/state/ids.ts +33 -0
  112. package/src/state/schema.ts +249 -0
  113. package/src/state/types.ts +76 -0
  114. package/src/tools/artifacts.ts +83 -0
  115. package/src/tools/index.ts +111 -0
  116. package/src/tools/init.ts +50 -0
  117. package/src/tools/injects.ts +108 -0
  118. package/src/tools/repair.ts +31 -0
  119. package/src/tools/run.ts +62 -0
  120. package/src/tools/spec.ts +50 -0
  121. package/src/tools/stage.ts +361 -0
  122. package/src/tools/status.ts +119 -0
  123. package/src/tools/story.ts +106 -0
  124. package/src/tools/workflow.ts +241 -0
  125. package/src/ui/inject.ts +13 -0
  126. package/src/ui/toasts.ts +48 -0
  127. package/src/workflow/artifacts.ts +69 -0
  128. package/src/workflow/baton.ts +141 -0
  129. package/src/workflow/context.ts +86 -0
  130. package/src/workflow/directives.ts +170 -0
  131. package/src/workflow/repair.ts +138 -0
  132. package/src/workflow/state-machine.ts +194 -0
  133. package/src/workflow/story-helpers.ts +18 -0
@@ -0,0 +1,249 @@
1
+ // src/state/schema.ts
2
+ // NOTE: This schema is intentionally additive over the original Astrocode schema.
3
+ // vNext adds continuation/snapshot/session tables and stronger indexes.
4
+ //
5
+ // Source of truth: SQLite file at .astro/astro.db
6
+
7
+ export const SCHEMA_VERSION = 2;
8
+
9
+ export const SCHEMA_SQL = `
10
+ PRAGMA foreign_keys = ON;
11
+
12
+ CREATE TABLE IF NOT EXISTS repo_state (
13
+ id INTEGER PRIMARY KEY CHECK (id = 1),
14
+ schema_version INTEGER NOT NULL,
15
+ created_at TEXT NOT NULL,
16
+ updated_at TEXT NOT NULL,
17
+ spec_hash_before TEXT,
18
+ spec_hash_after TEXT,
19
+ last_run_id TEXT,
20
+ last_story_key TEXT,
21
+ last_event_at TEXT
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS settings (
25
+ key TEXT PRIMARY KEY,
26
+ value TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS epics (
31
+ epic_key TEXT PRIMARY KEY,
32
+ title TEXT NOT NULL,
33
+ body_md TEXT NOT NULL DEFAULT '',
34
+ state TEXT NOT NULL DEFAULT 'active',
35
+ priority INTEGER NOT NULL DEFAULT 0,
36
+ created_at TEXT NOT NULL,
37
+ updated_at TEXT NOT NULL
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS story_drafts (
41
+ draft_id TEXT PRIMARY KEY,
42
+ title TEXT NOT NULL,
43
+ body_md TEXT NOT NULL DEFAULT '',
44
+ meta_json TEXT NOT NULL DEFAULT '{}',
45
+ created_at TEXT NOT NULL,
46
+ updated_at TEXT NOT NULL
47
+ );
48
+
49
+ CREATE TABLE IF NOT EXISTS story_keyseq (
50
+ id INTEGER PRIMARY KEY CHECK (id = 1),
51
+ next_story_num INTEGER NOT NULL
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS stories (
55
+ story_key TEXT PRIMARY KEY,
56
+ epic_key TEXT,
57
+ title TEXT NOT NULL,
58
+ body_md TEXT NOT NULL DEFAULT '',
59
+ state TEXT NOT NULL DEFAULT 'queued', -- queued|approved|in_progress|done|blocked|archived
60
+ priority INTEGER NOT NULL DEFAULT 0,
61
+ approved_at TEXT,
62
+ locked_by_run_id TEXT,
63
+ locked_at TEXT,
64
+ in_progress INTEGER NOT NULL DEFAULT 0,
65
+ created_at TEXT NOT NULL,
66
+ updated_at TEXT NOT NULL,
67
+ FOREIGN KEY (epic_key) REFERENCES epics(epic_key)
68
+ );
69
+
70
+ CREATE TABLE IF NOT EXISTS runs (
71
+ run_id TEXT PRIMARY KEY,
72
+ story_key TEXT NOT NULL,
73
+ status TEXT NOT NULL DEFAULT 'created', -- created|running|completed|failed|aborted
74
+ pipeline_stages_json TEXT NOT NULL DEFAULT '[]',
75
+ current_stage_key TEXT,
76
+ created_at TEXT NOT NULL,
77
+ started_at TEXT,
78
+ completed_at TEXT,
79
+ updated_at TEXT NOT NULL,
80
+ error_text TEXT,
81
+ FOREIGN KEY (story_key) REFERENCES stories(story_key)
82
+ );
83
+
84
+ CREATE TABLE IF NOT EXISTS stage_runs (
85
+ stage_run_id TEXT PRIMARY KEY,
86
+ run_id TEXT NOT NULL,
87
+ stage_key TEXT NOT NULL,
88
+ stage_index INTEGER NOT NULL,
89
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|skipped
90
+ subagent_type TEXT,
91
+ subagent_session_id TEXT,
92
+ started_at TEXT,
93
+ completed_at TEXT,
94
+ updated_at TEXT NOT NULL,
95
+ baton_path TEXT,
96
+ summary_md TEXT,
97
+ output_json TEXT,
98
+ error_text TEXT,
99
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS artifacts (
103
+ artifact_id TEXT PRIMARY KEY,
104
+ run_id TEXT,
105
+ stage_key TEXT,
106
+ type TEXT NOT NULL, -- plan|baton|evidence|diff|log|summary|commit|tool_output|snapshot
107
+ path TEXT NOT NULL,
108
+ sha256 TEXT,
109
+ meta_json TEXT NOT NULL DEFAULT '{}',
110
+ created_at TEXT NOT NULL,
111
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
112
+ );
113
+
114
+ CREATE TABLE IF NOT EXISTS tool_runs (
115
+ tool_run_id TEXT PRIMARY KEY,
116
+ run_id TEXT,
117
+ stage_key TEXT,
118
+ tool_name TEXT NOT NULL,
119
+ args_json TEXT NOT NULL DEFAULT '{}',
120
+ output_summary TEXT NOT NULL DEFAULT '',
121
+ output_artifact_id TEXT,
122
+ created_at TEXT NOT NULL,
123
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
124
+ );
125
+
126
+ CREATE TABLE IF NOT EXISTS events (
127
+ event_id TEXT PRIMARY KEY,
128
+ run_id TEXT,
129
+ stage_key TEXT,
130
+ type TEXT NOT NULL,
131
+ body_json TEXT NOT NULL DEFAULT '{}',
132
+ created_at TEXT NOT NULL,
133
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
134
+ );
135
+
136
+ CREATE TABLE IF NOT EXISTS injects (
137
+ inject_id TEXT PRIMARY KEY,
138
+ type TEXT NOT NULL DEFAULT 'note',
139
+ title TEXT NOT NULL,
140
+ body_md TEXT NOT NULL,
141
+ tags_json TEXT NOT NULL DEFAULT '[]',
142
+ scope TEXT NOT NULL DEFAULT 'repo', -- repo|run:<id>|story:<key>|global
143
+ source TEXT NOT NULL DEFAULT 'user', -- user|tool|agent|import
144
+ priority INTEGER NOT NULL DEFAULT 50,
145
+ expires_at TEXT,
146
+ sha256 TEXT,
147
+ created_at TEXT NOT NULL,
148
+ updated_at TEXT NOT NULL
149
+ );
150
+
151
+ CREATE TABLE IF NOT EXISTS running_batches (
152
+ batch_id TEXT PRIMARY KEY,
153
+ run_id TEXT,
154
+ session_id TEXT,
155
+ status TEXT NOT NULL DEFAULT 'running', -- running|completed|failed|aborted
156
+ created_at TEXT NOT NULL,
157
+ updated_at TEXT NOT NULL,
158
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
159
+ );
160
+
161
+ CREATE TABLE IF NOT EXISTS workflow_metrics (
162
+ metric_id TEXT PRIMARY KEY,
163
+ run_id TEXT,
164
+ stage_key TEXT,
165
+ name TEXT NOT NULL,
166
+ value_num REAL,
167
+ value_text TEXT,
168
+ created_at TEXT NOT NULL,
169
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
170
+ );
171
+
172
+ CREATE TABLE IF NOT EXISTS template_intents (
173
+ intent_key TEXT PRIMARY KEY,
174
+ body_md TEXT NOT NULL,
175
+ updated_at TEXT NOT NULL
176
+ );
177
+
178
+ -- vNext tables
179
+
180
+ CREATE TABLE IF NOT EXISTS story_relations (
181
+ parent_story_key TEXT NOT NULL,
182
+ child_story_key TEXT NOT NULL,
183
+ relation_type TEXT NOT NULL DEFAULT 'split',
184
+ reason TEXT NOT NULL DEFAULT '',
185
+ created_at TEXT NOT NULL,
186
+ PRIMARY KEY (parent_story_key, child_story_key),
187
+ FOREIGN KEY (parent_story_key) REFERENCES stories(story_key),
188
+ FOREIGN KEY (child_story_key) REFERENCES stories(story_key)
189
+ );
190
+
191
+ CREATE TABLE IF NOT EXISTS continuations (
192
+ continuation_id INTEGER PRIMARY KEY AUTOINCREMENT,
193
+ session_id TEXT NOT NULL,
194
+ run_id TEXT,
195
+ directive_hash TEXT NOT NULL,
196
+ kind TEXT NOT NULL, -- continue|stage|blocked|repair
197
+ reason TEXT NOT NULL DEFAULT '',
198
+ created_at TEXT NOT NULL,
199
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
200
+ );
201
+
202
+ CREATE INDEX IF NOT EXISTS idx_continuations_session_created ON continuations(session_id, created_at DESC);
203
+ CREATE INDEX IF NOT EXISTS idx_continuations_run_created ON continuations(run_id, created_at DESC);
204
+
205
+ CREATE TABLE IF NOT EXISTS context_snapshots (
206
+ snapshot_id TEXT PRIMARY KEY,
207
+ run_id TEXT NOT NULL,
208
+ stage_key TEXT NOT NULL,
209
+ summary_md TEXT NOT NULL,
210
+ created_at TEXT NOT NULL,
211
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
212
+ );
213
+
214
+ CREATE INDEX IF NOT EXISTS idx_context_snapshots_run_created ON context_snapshots(run_id, created_at DESC);
215
+
216
+ CREATE TABLE IF NOT EXISTS agent_sessions (
217
+ session_id TEXT PRIMARY KEY,
218
+ parent_session_id TEXT,
219
+ agent_name TEXT NOT NULL,
220
+ run_id TEXT,
221
+ stage_key TEXT,
222
+ status TEXT NOT NULL DEFAULT 'active',
223
+ created_at TEXT NOT NULL,
224
+ updated_at TEXT NOT NULL
225
+ );
226
+
227
+ -- Indexes
228
+
229
+ CREATE INDEX IF NOT EXISTS idx_stories_state ON stories(state);
230
+ CREATE INDEX IF NOT EXISTS idx_runs_story ON runs(story_key);
231
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
232
+ CREATE INDEX IF NOT EXISTS idx_stage_runs_run ON stage_runs(run_id, stage_index);
233
+ CREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_key, created_at DESC);
234
+ CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);
235
+ CREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);
236
+ CREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);
237
+
238
+ -- Stronger invariants (SQLite partial indexes)
239
+ -- Only one run may be 'running' at a time (single-repo harness by default).
240
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_single_running_run
241
+ ON runs(status)
242
+ WHERE status = 'running';
243
+
244
+ -- Only one story may be in_progress=1 at a time (pairs with single running run).
245
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_single_in_progress_story
246
+ ON stories(in_progress)
247
+ WHERE in_progress = 1;
248
+
249
+ `;
@@ -0,0 +1,76 @@
1
+ export type StoryState = "queued" | "approved" | "in_progress" | "done" | "blocked" | "archived";
2
+ export type RunStatus = "created" | "running" | "completed" | "failed" | "aborted";
3
+ export type StageStatus = "pending" | "running" | "completed" | "failed" | "skipped";
4
+
5
+ export type StageKey = "frame" | "plan" | "spec" | "implement" | "review" | "verify" | "close";
6
+
7
+ export type StoryRow = {
8
+ story_key: string;
9
+ epic_key: string | null;
10
+ title: string;
11
+ body_md: string;
12
+ state: StoryState;
13
+ priority: number;
14
+ approved_at: string | null;
15
+ locked_by_run_id: string | null;
16
+ locked_at: string | null;
17
+ in_progress: 0 | 1;
18
+ created_at: string;
19
+ updated_at: string;
20
+ };
21
+
22
+ export type RunRow = {
23
+ run_id: string;
24
+ story_key: string;
25
+ status: RunStatus;
26
+ pipeline_stages_json: string;
27
+ current_stage_key: string | null;
28
+ created_at: string;
29
+ started_at: string | null;
30
+ completed_at: string | null;
31
+ updated_at: string;
32
+ error_text: string | null;
33
+ };
34
+
35
+ export type StageRunRow = {
36
+ stage_run_id: string;
37
+ run_id: string;
38
+ stage_key: StageKey;
39
+ stage_index: number;
40
+ status: StageStatus;
41
+ subagent_type: string | null;
42
+ subagent_session_id: string | null;
43
+ started_at: string | null;
44
+ completed_at: string | null;
45
+ updated_at: string;
46
+ baton_path: string | null;
47
+ summary_md: string | null;
48
+ output_json: string | null;
49
+ error_text: string | null;
50
+ };
51
+
52
+ export type ArtifactRow = {
53
+ artifact_id: string;
54
+ run_id: string | null;
55
+ stage_key: string | null;
56
+ type: string;
57
+ path: string;
58
+ sha256: string | null;
59
+ meta_json: string;
60
+ created_at: string;
61
+ };
62
+
63
+ export type InjectRow = {
64
+ inject_id: string;
65
+ type: string;
66
+ title: string;
67
+ body_md: string;
68
+ tags_json: string;
69
+ scope: string;
70
+ source: string;
71
+ priority: number;
72
+ expires_at: string | null;
73
+ sha256: string | null;
74
+ created_at: string;
75
+ updated_at: string;
76
+ };
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
4
+ import type { AstrocodeConfig } from "../config/schema";
5
+ import type { SqliteDb } from "../state/db";
6
+ import { getAstroPaths } from "../shared/paths";
7
+ import { putArtifact, listArtifacts, getArtifact } from "../workflow/artifacts";
8
+
9
+ export function createAstroArtifactPutTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
10
+ const { ctx, db } = opts;
11
+
12
+ return tool({
13
+ description: "Write an artifact file under .astro and record it in the DB.",
14
+ args: {
15
+ run_id: tool.schema.string().nullable().optional(),
16
+ stage_key: tool.schema.string().nullable().optional(),
17
+ type: tool.schema.string().default("log"),
18
+ rel_path: tool.schema.string().min(1),
19
+ content: tool.schema.string().default(""),
20
+ meta_json: tool.schema.string().default("{}"),
21
+ },
22
+ execute: async ({ run_id, stage_key, type, rel_path, content, meta_json }) => {
23
+ const repoRoot = ctx.directory as string;
24
+ const meta = JSON.parse(meta_json || "{}") as Record<string, unknown>;
25
+
26
+ const res = putArtifact({
27
+ repoRoot,
28
+ db,
29
+ run_id: run_id ?? null,
30
+ stage_key: stage_key ?? null,
31
+ type,
32
+ rel_path,
33
+ content,
34
+ meta,
35
+ });
36
+
37
+ return `✅ Wrote artifact ${res.artifact_id} (${type}) at ${rel_path} (sha256=${res.sha256})`;
38
+ },
39
+ });
40
+ }
41
+
42
+ export function createAstroArtifactListTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
43
+ const { db } = opts;
44
+
45
+ return tool({
46
+ description: "List artifacts (optionally filtered by run_id, stage_key, type).",
47
+ args: {
48
+ run_id: tool.schema.string().optional(),
49
+ stage_key: tool.schema.string().optional(),
50
+ type: tool.schema.string().optional(),
51
+ },
52
+ execute: async ({ run_id, stage_key, type }) => {
53
+ const rows = listArtifacts(db, { run_id, stage_key, type });
54
+ return JSON.stringify(rows, null, 2);
55
+ },
56
+ });
57
+ }
58
+
59
+ export function createAstroArtifactGetTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
60
+ const { ctx, config, db } = opts;
61
+
62
+ return tool({
63
+ description: "Get artifact metadata (and optionally file contents).",
64
+ args: {
65
+ artifact_id: tool.schema.string().min(1),
66
+ include_body: tool.schema.boolean().default(false),
67
+ max_body_chars: tool.schema.number().int().positive().default(50_000),
68
+ },
69
+ execute: async ({ artifact_id, include_body, max_body_chars }) => {
70
+ const row = getArtifact(db, artifact_id);
71
+ if (!row) throw new Error(`Artifact not found: ${artifact_id}`);
72
+
73
+ if (!include_body) return JSON.stringify(row, null, 2);
74
+
75
+ const repoRoot = ctx.directory as string;
76
+ const abs = path.join(repoRoot, row.path);
77
+ const body = fs.existsSync(abs) ? fs.readFileSync(abs, "utf-8") : "(missing file)";
78
+ const clipped = body.length > max_body_chars ? body.slice(0, max_body_chars) + "\n…(truncated)" : body;
79
+
80
+ return JSON.stringify({ ...row, body: clipped }, null, 2);
81
+ },
82
+ });
83
+ }
@@ -0,0 +1,111 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+
5
+ import { createAstroInitTool } from "./init";
6
+ import { createAstroStatusTool } from "./status";
7
+ import { createAstroStoryQueueTool, createAstroStoryApproveTool, createAstroStoryBoardTool, createAstroStorySetStateTool } from "./story";
8
+ import { createAstroSpecGetTool, createAstroSpecSetTool } from "./spec";
9
+ import { createAstroRunGetTool, createAstroRunAbortTool } from "./run";
10
+ import { createAstroWorkflowProceedTool } from "./workflow";
11
+ import { createAstroStageStartTool, createAstroStageCompleteTool, createAstroStageFailTool, createAstroStageResetTool } from "./stage";
12
+ import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArtifactGetTool } from "./artifacts";
13
+ import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool } from "./injects";
14
+ import { createAstroRepairTool } from "./repair";
15
+
16
+ export function createAstroTools(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): Record<string, ToolDefinition> {
17
+ const { ctx, config, db } = opts;
18
+ const hasDatabase = !!db;
19
+
20
+ const tools: Record<string, ToolDefinition> = {};
21
+
22
+ // Always available tools
23
+ tools.astro_status = createAstroStatusTool({ ctx, config, db });
24
+
25
+ // Always available tools (work without database)
26
+ tools.astro_status = createAstroStatusTool({ ctx, config, db });
27
+ tools.astro_spec_get = createAstroSpecGetTool({ ctx, config, db });
28
+
29
+ // Database-dependent tools
30
+ if (hasDatabase) {
31
+ tools.astro_init = createAstroInitTool({ ctx, config, db });
32
+ tools.astro_story_queue = createAstroStoryQueueTool({ ctx, config, db });
33
+ tools.astro_story_approve = createAstroStoryApproveTool({ ctx, config, db });
34
+ tools.astro_story_board = createAstroStoryBoardTool({ ctx, config, db });
35
+ tools.astro_story_set_state = createAstroStorySetStateTool({ ctx, config, db });
36
+ tools.astro_spec_set = createAstroSpecSetTool({ ctx, config, db });
37
+ tools.astro_run_get = createAstroRunGetTool({ ctx, config, db });
38
+ tools.astro_run_abort = createAstroRunAbortTool({ ctx, config, db });
39
+ tools.astro_workflow_proceed = createAstroWorkflowProceedTool({ ctx, config, db });
40
+ tools.astro_stage_start = createAstroStageStartTool({ ctx, config, db });
41
+ tools.astro_stage_complete = createAstroStageCompleteTool({ ctx, config, db });
42
+ tools.astro_stage_fail = createAstroStageFailTool({ ctx, config, db });
43
+ tools.astro_stage_reset = createAstroStageResetTool({ ctx, config, db });
44
+ tools.astro_artifact_put = createAstroArtifactPutTool({ ctx, config, db });
45
+ tools.astro_artifact_list = createAstroArtifactListTool({ ctx, config, db });
46
+ tools.astro_artifact_get = createAstroArtifactGetTool({ ctx, config, db });
47
+ tools.astro_inject_put = createAstroInjectPutTool({ ctx, config, db });
48
+ tools.astro_inject_list = createAstroInjectListTool({ ctx, config, db });
49
+ tools.astro_inject_search = createAstroInjectSearchTool({ ctx, config, db });
50
+ tools.astro_inject_get = createAstroInjectGetTool({ ctx, config, db });
51
+ tools.astro_repair = createAstroRepairTool({ ctx, config, db });
52
+ } else {
53
+ // Limited mode tools - provide helpful messages instead of failing
54
+ tools.astro_init = {
55
+ description: "Initialize Astrocode (requires database - currently unavailable)",
56
+ args: {},
57
+ execute: async () => "❌ Database not available. Astrocode is running in limited mode."
58
+ };
59
+ tools.astro_story_queue = {
60
+ description: "Queue a story (requires database - currently unavailable)",
61
+ args: {},
62
+ execute: async () => "❌ Database not available. Astrocode is running in limited mode."
63
+ };
64
+ tools.astro_spec_set = {
65
+ description: "Set project spec (requires database for hash tracking - currently unavailable)",
66
+ args: {},
67
+ execute: async () => "❌ Database not available. Astrocode is running in limited mode."
68
+ };
69
+ tools.astro_workflow_proceed = {
70
+ description: "Advance workflow (requires database - currently unavailable)",
71
+ args: {},
72
+ execute: async () => "❌ Database not available. Astrocode is running in limited mode."
73
+ };
74
+ }
75
+
76
+ // Create aliases for backward compatibility
77
+ const aliases: Array<[string, string]> = [
78
+ ["_astro_init", "astro_init"],
79
+ ["_astro_status", "astro_status"],
80
+ ["_astro_story_queue", "astro_story_queue"],
81
+ ["_astro_story_approve", "astro_story_approve"],
82
+ ["_astro_story_board", "astro_story_board"],
83
+ ["_astro_story_set_state", "astro_story_set_state"],
84
+ ["_astro_spec_get", "astro_spec_get"],
85
+ ["_astro_spec_set", "astro_spec_set"],
86
+ ["_astro_run_get", "astro_run_get"],
87
+ ["_astro_run_abort", "astro_run_abort"],
88
+ ["_astro_workflow_proceed", "astro_workflow_proceed"],
89
+ ["_astro_stage_start", "astro_stage_start"],
90
+ ["_astro_stage_complete", "astro_stage_complete"],
91
+ ["_astro_stage_fail", "astro_stage_fail"],
92
+ ["_astro_stage_reset", "astro_stage_reset"],
93
+ ["_astro_artifact_put", "astro_artifact_put"],
94
+ ["_astro_artifact_list", "astro_artifact_list"],
95
+ ["_astro_artifact_get", "astro_artifact_get"],
96
+ ["_astro_inject_put", "astro_inject_put"],
97
+ ["_astro_inject_list", "astro_inject_list"],
98
+ ["_astro_inject_search", "astro_inject_search"],
99
+ ["_astro_inject_get", "astro_inject_get"],
100
+ ["_astro_repair", "astro_repair"],
101
+ ];
102
+
103
+ // Only add aliases for tools that exist
104
+ for (const [alias, target] of aliases) {
105
+ if (tools[target]) {
106
+ tools[alias] = tools[target];
107
+ }
108
+ }
109
+
110
+ return tools;
111
+ }
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
4
+ import type { AstrocodeConfig } from "../config/schema";
5
+ import type { SqliteDb } from "../state/db";
6
+ import { ensureSchema } from "../state/db";
7
+ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
8
+ import { nowISO } from "../shared/time";
9
+ import { sha256Hex } from "../shared/hash";
10
+
11
+ export function createAstroInitTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
12
+ const { ctx, config, db } = opts;
13
+
14
+ return tool({
15
+ description:
16
+ "Initialize Astrocode vNext in this repo: create .astro directories, ensure SQLite schema, and create a placeholder spec if missing. Idempotent.",
17
+ args: {
18
+ ensure_spec: tool.schema.boolean().default(true),
19
+ spec_placeholder: tool.schema.string().default("# Project Spec\n\n(Write the spec here.)\n"),
20
+ },
21
+ execute: async ({ ensure_spec, spec_placeholder }) => {
22
+ const repoRoot = ctx.directory as string;
23
+ const paths = getAstroPaths(repoRoot, config.db.path);
24
+ ensureAstroDirs(paths);
25
+
26
+ ensureSchema(db, { allowAutoMigrate: config.db.allow_auto_migrate, failOnDowngrade: config.db.fail_on_downgrade });
27
+
28
+ if (ensure_spec) {
29
+ if (!fs.existsSync(paths.specPath)) {
30
+ fs.writeFileSync(paths.specPath, spec_placeholder);
31
+ const specHash = sha256Hex(spec_placeholder);
32
+ const now = nowISO();
33
+ db.prepare("UPDATE repo_state SET spec_hash_after=?, updated_at=? WHERE id=1").run(specHash, now);
34
+ }
35
+ }
36
+
37
+ const stat = db.prepare("SELECT schema_version, created_at, updated_at FROM repo_state WHERE id=1").get() as any;
38
+
39
+ return [
40
+ `✅ Astrocode initialized.`,
41
+ ``,
42
+ `- Repo: ${repoRoot}`,
43
+ `- DB: ${path.relative(repoRoot, paths.dbPath)} (schema_version=${stat?.schema_version ?? "?"})`,
44
+ `- Spec: ${path.relative(repoRoot, paths.specPath)}`,
45
+ ``,
46
+ `Next: queue a story (astro_story_queue) and approve it (astro_story_approve), or run /astro-status.`,
47
+ ].join("\n");
48
+ },
49
+ });
50
+ }
@@ -0,0 +1,108 @@
1
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ import { nowISO } from "../shared/time";
5
+ import { sha256Hex } from "../shared/hash";
6
+
7
+ function newInjectId(): string {
8
+ return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
9
+ }
10
+
11
+ export function createAstroInjectPutTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
12
+ const { db } = opts;
13
+
14
+ return tool({
15
+ description: "Create/update an inject (note/policy) stored in the DB. Useful for persistent rules.",
16
+ args: {
17
+ inject_id: tool.schema.string().optional(),
18
+ type: tool.schema.string().default("note"),
19
+ title: tool.schema.string().min(1),
20
+ body_md: tool.schema.string().min(1),
21
+ tags_json: tool.schema.string().default("[]"),
22
+ scope: tool.schema.string().default("repo"),
23
+ source: tool.schema.string().default("user"),
24
+ priority: tool.schema.number().int().default(50),
25
+ expires_at: tool.schema.string().nullable().optional(),
26
+ },
27
+ execute: async ({ inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at }) => {
28
+ const id = inject_id ?? newInjectId();
29
+ const now = nowISO();
30
+ const sha = sha256Hex(body_md);
31
+
32
+ const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id) as any;
33
+
34
+ if (existing) {
35
+ db.prepare(
36
+ "UPDATE injects SET type=?, title=?, body_md=?, tags_json=?, scope=?, source=?, priority=?, expires_at=?, sha256=?, updated_at=? WHERE inject_id=?"
37
+ ).run(type, title, body_md, tags_json, scope, source, priority, expires_at ?? null, sha, now, id);
38
+ return `✅ Updated inject ${id}: ${title}`;
39
+ }
40
+
41
+ db.prepare(
42
+ "INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
43
+ ).run(id, type, title, body_md, tags_json, scope, source, priority, expires_at ?? null, sha, now, now);
44
+
45
+ return `✅ Created inject ${id}: ${title}`;
46
+ },
47
+ });
48
+ }
49
+
50
+ export function createAstroInjectListTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
51
+ const { db } = opts;
52
+
53
+ return tool({
54
+ description: "List injects (optionally filtered by scope/type).",
55
+ args: {
56
+ scope: tool.schema.string().optional(),
57
+ type: tool.schema.string().optional(),
58
+ limit: tool.schema.number().int().positive().default(50),
59
+ },
60
+ execute: async ({ scope, type, limit }) => {
61
+ const where: string[] = [];
62
+ const params: any[] = [];
63
+ if (scope) { where.push("scope = ?"); params.push(scope); }
64
+ if (type) { where.push("type = ?"); params.push(type); }
65
+ const sql = `SELECT inject_id, type, title, scope, priority, created_at, updated_at FROM injects ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
66
+ const rows = db.prepare(sql).all(...params, limit) as any[];
67
+ return JSON.stringify(rows, null, 2);
68
+ },
69
+ });
70
+ }
71
+
72
+ export function createAstroInjectGetTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
73
+ const { db } = opts;
74
+
75
+ return tool({
76
+ description: "Get an inject by id (full body).",
77
+ args: {
78
+ inject_id: tool.schema.string().min(1),
79
+ },
80
+ execute: async ({ inject_id }) => {
81
+ const row = db.prepare("SELECT * FROM injects WHERE inject_id=?").get(inject_id) as any;
82
+ if (!row) throw new Error(`Inject not found: ${inject_id}`);
83
+ return JSON.stringify(row, null, 2);
84
+ },
85
+ });
86
+ }
87
+
88
+ export function createAstroInjectSearchTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
89
+ const { db } = opts;
90
+
91
+ return tool({
92
+ description: "Search injects by query substring over title/body/tags. Returns matches ordered by priority/recency.",
93
+ args: {
94
+ q: tool.schema.string().min(1),
95
+ scope: tool.schema.string().optional(),
96
+ limit: tool.schema.number().int().positive().default(20),
97
+ },
98
+ execute: async ({ q, scope, limit }) => {
99
+ const like = `%${q}%`;
100
+ const where: string[] = ["(title LIKE ? OR body_md LIKE ? OR tags_json LIKE ?)"];
101
+ const params: any[] = [like, like, like];
102
+ if (scope) { where.push("scope = ?"); params.push(scope); }
103
+ const sql = `SELECT inject_id, type, title, scope, priority, updated_at FROM injects WHERE ${where.join(" AND ")} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
104
+ const rows = db.prepare(sql).all(...params, limit) as any[];
105
+ return JSON.stringify(rows, null, 2);
106
+ },
107
+ });
108
+ }