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
@@ -0,0 +1,258 @@
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
+ export const SCHEMA_VERSION = 3; // v3: Added advisory lock support + database constraints
7
+ export const SCHEMA_SQL = `
8
+ PRAGMA foreign_keys = ON;
9
+
10
+ CREATE TABLE IF NOT EXISTS repo_state (
11
+ id INTEGER PRIMARY KEY CHECK (id = 1),
12
+ schema_version INTEGER NOT NULL,
13
+ created_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL,
15
+ spec_hash_before TEXT,
16
+ spec_hash_after TEXT,
17
+ last_run_id TEXT,
18
+ last_story_key TEXT,
19
+ last_event_at TEXT
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS settings (
23
+ key TEXT PRIMARY KEY,
24
+ value TEXT NOT NULL,
25
+ updated_at TEXT NOT NULL
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS epics (
29
+ epic_key TEXT PRIMARY KEY,
30
+ title TEXT NOT NULL,
31
+ body_md TEXT NOT NULL DEFAULT '',
32
+ state TEXT NOT NULL DEFAULT 'active',
33
+ priority INTEGER NOT NULL DEFAULT 0,
34
+ created_at TEXT NOT NULL,
35
+ updated_at TEXT NOT NULL
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS story_drafts (
39
+ draft_id TEXT PRIMARY KEY,
40
+ title TEXT NOT NULL,
41
+ body_md TEXT NOT NULL DEFAULT '',
42
+ meta_json TEXT NOT NULL DEFAULT '{}',
43
+ created_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS story_keyseq (
48
+ id INTEGER PRIMARY KEY CHECK (id = 1),
49
+ next_story_num INTEGER NOT NULL
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS stories (
53
+ story_key TEXT PRIMARY KEY,
54
+ epic_key TEXT,
55
+ title TEXT NOT NULL,
56
+ body_md TEXT NOT NULL DEFAULT '',
57
+ state TEXT NOT NULL DEFAULT 'queued', -- queued|approved|in_progress|done|blocked|archived
58
+ priority INTEGER NOT NULL DEFAULT 0,
59
+ approved_at TEXT,
60
+ locked_by_run_id TEXT,
61
+ locked_at TEXT,
62
+ in_progress INTEGER NOT NULL DEFAULT 0,
63
+ created_at TEXT NOT NULL,
64
+ updated_at TEXT NOT NULL,
65
+ FOREIGN KEY (epic_key) REFERENCES epics(epic_key)
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS runs (
69
+ run_id TEXT PRIMARY KEY,
70
+ story_key TEXT NOT NULL,
71
+ status TEXT NOT NULL DEFAULT 'created', -- created|running|completed|failed|aborted
72
+ pipeline_stages_json TEXT NOT NULL DEFAULT '[]',
73
+ current_stage_key TEXT,
74
+ created_at TEXT NOT NULL,
75
+ started_at TEXT,
76
+ completed_at TEXT,
77
+ updated_at TEXT NOT NULL,
78
+ error_text TEXT,
79
+ FOREIGN KEY (story_key) REFERENCES stories(story_key)
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS stage_runs (
83
+ stage_run_id TEXT PRIMARY KEY,
84
+ run_id TEXT NOT NULL,
85
+ stage_key TEXT NOT NULL,
86
+ stage_index INTEGER NOT NULL,
87
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|skipped
88
+ created_at TEXT NOT NULL,
89
+ subagent_type TEXT,
90
+ subagent_session_id TEXT,
91
+ started_at TEXT,
92
+ completed_at TEXT,
93
+ updated_at TEXT NOT NULL,
94
+ baton_path TEXT,
95
+ summary_md TEXT,
96
+ output_json TEXT,
97
+ error_text TEXT,
98
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
99
+ );
100
+
101
+ CREATE TABLE IF NOT EXISTS artifacts (
102
+ artifact_id TEXT PRIMARY KEY,
103
+ run_id TEXT,
104
+ stage_key TEXT,
105
+ type TEXT NOT NULL, -- plan|baton|evidence|diff|log|summary|commit|tool_output|snapshot
106
+ path TEXT NOT NULL,
107
+ sha256 TEXT,
108
+ meta_json TEXT NOT NULL DEFAULT '{}',
109
+ created_at TEXT NOT NULL,
110
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
111
+ );
112
+
113
+ CREATE TABLE IF NOT EXISTS tool_runs (
114
+ tool_run_id TEXT PRIMARY KEY,
115
+ run_id TEXT,
116
+ stage_key TEXT,
117
+ tool_name TEXT NOT NULL,
118
+ args_json TEXT NOT NULL DEFAULT '{}',
119
+ output_summary TEXT NOT NULL DEFAULT '',
120
+ output_artifact_id TEXT,
121
+ created_at TEXT NOT NULL,
122
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
123
+ );
124
+
125
+ CREATE TABLE IF NOT EXISTS events (
126
+ event_id TEXT PRIMARY KEY,
127
+ run_id TEXT,
128
+ stage_key TEXT,
129
+ type TEXT NOT NULL,
130
+ body_json TEXT NOT NULL DEFAULT '{}',
131
+ created_at TEXT NOT NULL,
132
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS injects (
136
+ inject_id TEXT PRIMARY KEY,
137
+ type TEXT NOT NULL DEFAULT 'note',
138
+ title TEXT NOT NULL,
139
+ body_md TEXT NOT NULL,
140
+ tags_json TEXT NOT NULL DEFAULT '[]',
141
+ scope TEXT NOT NULL DEFAULT 'repo', -- repo|run:<id>|story:<key>|global
142
+ source TEXT NOT NULL DEFAULT 'user', -- user|tool|agent|import
143
+ priority INTEGER NOT NULL DEFAULT 50,
144
+ expires_at TEXT,
145
+ sha256 TEXT,
146
+ created_at TEXT NOT NULL,
147
+ updated_at TEXT NOT NULL
148
+ );
149
+
150
+ CREATE TABLE IF NOT EXISTS running_batches (
151
+ batch_id TEXT PRIMARY KEY,
152
+ run_id TEXT,
153
+ session_id TEXT,
154
+ status TEXT NOT NULL DEFAULT 'running', -- running|completed|failed|aborted
155
+ created_at TEXT NOT NULL,
156
+ updated_at TEXT NOT NULL,
157
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
158
+ );
159
+
160
+ CREATE TABLE IF NOT EXISTS workflow_metrics (
161
+ metric_id TEXT PRIMARY KEY,
162
+ run_id TEXT,
163
+ stage_key TEXT,
164
+ name TEXT NOT NULL,
165
+ value_num REAL,
166
+ value_text TEXT,
167
+ created_at TEXT NOT NULL,
168
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
169
+ );
170
+
171
+ CREATE TABLE IF NOT EXISTS template_intents (
172
+ intent_key TEXT PRIMARY KEY,
173
+ body_md TEXT NOT NULL,
174
+ updated_at TEXT NOT NULL
175
+ );
176
+
177
+ -- vNext tables
178
+
179
+ CREATE TABLE IF NOT EXISTS story_relations (
180
+ parent_story_key TEXT NOT NULL,
181
+ child_story_key TEXT NOT NULL,
182
+ relation_type TEXT NOT NULL DEFAULT 'split',
183
+ reason TEXT NOT NULL DEFAULT '',
184
+ created_at TEXT NOT NULL,
185
+ PRIMARY KEY (parent_story_key, child_story_key),
186
+ FOREIGN KEY (parent_story_key) REFERENCES stories(story_key),
187
+ FOREIGN KEY (child_story_key) REFERENCES stories(story_key)
188
+ );
189
+
190
+ CREATE TABLE IF NOT EXISTS continuations (
191
+ continuation_id INTEGER PRIMARY KEY AUTOINCREMENT,
192
+ session_id TEXT NOT NULL,
193
+ run_id TEXT,
194
+ directive_hash TEXT NOT NULL,
195
+ kind TEXT NOT NULL, -- continue|stage|blocked|repair
196
+ reason TEXT NOT NULL DEFAULT '',
197
+ created_at TEXT NOT NULL,
198
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
199
+ );
200
+
201
+ CREATE INDEX IF NOT EXISTS idx_continuations_session_created ON continuations(session_id, created_at DESC);
202
+ CREATE INDEX IF NOT EXISTS idx_continuations_run_created ON continuations(run_id, created_at DESC);
203
+
204
+ CREATE TABLE IF NOT EXISTS context_snapshots (
205
+ snapshot_id TEXT PRIMARY KEY,
206
+ run_id TEXT NOT NULL,
207
+ stage_key TEXT NOT NULL,
208
+ summary_md TEXT NOT NULL,
209
+ created_at TEXT NOT NULL,
210
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
211
+ );
212
+
213
+ CREATE INDEX IF NOT EXISTS idx_context_snapshots_run_created ON context_snapshots(run_id, created_at DESC);
214
+
215
+ CREATE TABLE IF NOT EXISTS agent_sessions (
216
+ session_id TEXT PRIMARY KEY,
217
+ parent_session_id TEXT,
218
+ agent_name TEXT NOT NULL,
219
+ run_id TEXT,
220
+ stage_key TEXT,
221
+ status TEXT NOT NULL DEFAULT 'active',
222
+ created_at TEXT NOT NULL,
223
+ updated_at TEXT NOT NULL
224
+ );
225
+
226
+ -- Indexes
227
+
228
+ CREATE INDEX IF NOT EXISTS idx_stories_state ON stories(state);
229
+ CREATE INDEX IF NOT EXISTS idx_runs_story ON runs(story_key);
230
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
231
+ CREATE INDEX IF NOT EXISTS idx_stage_runs_run ON stage_runs(run_id, stage_index);
232
+ CREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_key, created_at DESC);
233
+ CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);
234
+ CREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);
235
+ CREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);
236
+
237
+ -- CONSTRAINT: Only one running run at a time (partial unique index)
238
+ -- This provides database-level safety when using advisory locks
239
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_single_running_run ON runs(status) WHERE status = 'running';
240
+
241
+ -- CONSTRAINT: Only one run can lock a story at a time (partial unique index)
242
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_single_story_lock ON stories(in_progress) WHERE in_progress = 1;
243
+ CREATE INDEX IF NOT EXISTS idx_injects_scope_type_priority_updated ON injects(scope, type, priority DESC, updated_at DESC);
244
+ CREATE INDEX IF NOT EXISTS idx_injects_expires ON injects(expires_at) WHERE expires_at IS NOT NULL;
245
+ CREATE INDEX IF NOT EXISTS idx_injects_sha256 ON injects(sha256) WHERE sha256 IS NOT NULL;
246
+
247
+ -- Stronger invariants (SQLite partial indexes)
248
+ -- Only one run may be 'running' at a time (single-repo harness by default).
249
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_single_running_run
250
+ ON runs(status)
251
+ WHERE status = 'running';
252
+
253
+ -- Only one story may be in_progress=1 at a time (pairs with single running run).
254
+ CREATE UNIQUE INDEX IF NOT EXISTS uniq_single_in_progress_story
255
+ ON stories(in_progress)
256
+ WHERE in_progress = 1;
257
+
258
+ `;
@@ -0,0 +1,71 @@
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
+ export type StageKey = "frame" | "plan" | "spec" | "implement" | "review" | "verify" | "close";
5
+ export type StoryRow = {
6
+ story_key: string;
7
+ epic_key: string | null;
8
+ title: string;
9
+ body_md: string;
10
+ state: StoryState;
11
+ priority: number;
12
+ approved_at: string | null;
13
+ locked_by_run_id: string | null;
14
+ locked_at: string | null;
15
+ in_progress: 0 | 1;
16
+ created_at: string;
17
+ updated_at: string;
18
+ };
19
+ export type RunRow = {
20
+ run_id: string;
21
+ story_key: string;
22
+ status: RunStatus;
23
+ pipeline_stages_json: string;
24
+ current_stage_key: string | null;
25
+ created_at: string;
26
+ started_at: string | null;
27
+ completed_at: string | null;
28
+ updated_at: string;
29
+ error_text: string | null;
30
+ };
31
+ export type StageRunRow = {
32
+ stage_run_id: string;
33
+ run_id: string;
34
+ stage_key: StageKey;
35
+ stage_index: number;
36
+ status: StageStatus;
37
+ created_at: string;
38
+ subagent_type: string | null;
39
+ subagent_session_id: string | null;
40
+ started_at: string | null;
41
+ completed_at: string | null;
42
+ updated_at: string;
43
+ baton_path: string | null;
44
+ summary_md: string | null;
45
+ output_json: string | null;
46
+ error_text: string | null;
47
+ };
48
+ export type ArtifactRow = {
49
+ artifact_id: string;
50
+ run_id: string | null;
51
+ stage_key: string | null;
52
+ type: string;
53
+ path: string;
54
+ sha256: string | null;
55
+ meta_json: string;
56
+ created_at: string;
57
+ };
58
+ export type InjectRow = {
59
+ inject_id: string;
60
+ type: string;
61
+ title: string;
62
+ body_md: string;
63
+ tags_json: string;
64
+ scope: string;
65
+ source: string;
66
+ priority: number;
67
+ expires_at: string | null;
68
+ sha256: string | null;
69
+ created_at: string;
70
+ updated_at: string;
71
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { acquireRepoLock } from "./repo-lock";
2
+ type RepoLockAcquire = typeof acquireRepoLock;
3
+ /**
4
+ * Acquire ONCE per workflow/session in this process.
5
+ * Nested calls reuse the same held lock (no reacquire, no churn).
6
+ *
7
+ * ADVISORY LOCK MODE:
8
+ * - Creates lock file to signal other sessions
9
+ * - If lock held by another session: WARN and proceed anyway
10
+ * - Database constraints provide actual safety (single running run)
11
+ * - Better UX: no blocking, just helpful warnings
12
+ */
13
+ export declare function workflowRepoLock<T>(deps: {
14
+ acquireRepoLock: RepoLockAcquire;
15
+ }, opts: {
16
+ lockPath: string;
17
+ repoRoot: string;
18
+ sessionId?: string;
19
+ owner?: string;
20
+ fn: () => Promise<T>;
21
+ advisory?: boolean;
22
+ }): Promise<T>;
23
+ export {};
@@ -0,0 +1,83 @@
1
+ const HELD_BY_KEY = new Map();
2
+ function key(lockPath, sessionId) {
3
+ return `${lockPath}::${sessionId ?? ""}`;
4
+ }
5
+ /**
6
+ * Acquire ONCE per workflow/session in this process.
7
+ * Nested calls reuse the same held lock (no reacquire, no churn).
8
+ *
9
+ * ADVISORY LOCK MODE:
10
+ * - Creates lock file to signal other sessions
11
+ * - If lock held by another session: WARN and proceed anyway
12
+ * - Database constraints provide actual safety (single running run)
13
+ * - Better UX: no blocking, just helpful warnings
14
+ */
15
+ export async function workflowRepoLock(deps, opts) {
16
+ const k = key(opts.lockPath, opts.sessionId);
17
+ const existing = HELD_BY_KEY.get(k);
18
+ if (existing) {
19
+ existing.depth += 1;
20
+ try {
21
+ return await opts.fn();
22
+ }
23
+ finally {
24
+ existing.depth -= 1;
25
+ if (existing.depth <= 0) {
26
+ HELD_BY_KEY.delete(k);
27
+ existing.release();
28
+ }
29
+ }
30
+ }
31
+ // IMPORTANT: this is tuned for "hold for whole workflow".
32
+ let handle = null;
33
+ try {
34
+ handle = await deps.acquireRepoLock({
35
+ lockPath: opts.lockPath,
36
+ repoRoot: opts.repoRoot,
37
+ sessionId: opts.sessionId,
38
+ owner: opts.owner,
39
+ retryMs: opts.advisory ? 1000 : 30_000, // Advisory: fail fast, hard: wait longer
40
+ staleMs: 30_000, // Reduced from 2 minutes to 30 seconds for faster stale lock recovery
41
+ heartbeatMs: 200,
42
+ minWriteMs: 800,
43
+ pollMs: 20,
44
+ pollMaxMs: 250,
45
+ });
46
+ }
47
+ catch (err) {
48
+ // Lock acquisition failed - check if advisory mode
49
+ if (opts.advisory) {
50
+ // Advisory mode: warn and proceed without lock
51
+ // eslint-disable-next-line no-console
52
+ console.warn(`⚠️ [Astrocode] Another session may be active. Proceeding anyway (advisory lock mode).`);
53
+ // eslint-disable-next-line no-console
54
+ console.warn(` ${err.message}`);
55
+ // Proceed without lock - database constraints will ensure safety
56
+ try {
57
+ return await opts.fn();
58
+ }
59
+ catch (dbErr) {
60
+ // Check if this is a concurrency error
61
+ if (dbErr.message?.includes('UNIQUE constraint') || dbErr.message?.includes('SQLITE_BUSY')) {
62
+ throw new Error(`Another session is actively working on this story. Database prevented concurrent modification. ` +
63
+ `Please wait for the other session to complete, or work on a different story.`);
64
+ }
65
+ throw dbErr;
66
+ }
67
+ }
68
+ // Hard lock mode: propagate error
69
+ throw err;
70
+ }
71
+ const held = { release: handle.release, depth: 1 };
72
+ HELD_BY_KEY.set(k, held);
73
+ try {
74
+ return await opts.fn();
75
+ }
76
+ finally {
77
+ held.depth -= 1;
78
+ if (held.depth <= 0) {
79
+ HELD_BY_KEY.delete(k);
80
+ held.release();
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,18 @@
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 createAstroArtifactPutTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
9
+ export declare function createAstroArtifactListTool(opts: {
10
+ ctx: any;
11
+ config: AstrocodeConfig;
12
+ db: SqliteDb;
13
+ }): ToolDefinition;
14
+ export declare function createAstroArtifactGetTool(opts: {
15
+ ctx: any;
16
+ config: AstrocodeConfig;
17
+ db: SqliteDb;
18
+ }): ToolDefinition;
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { tool } from "@opencode-ai/plugin/tool";
4
+ import { putArtifact, listArtifacts, getArtifact } from "../workflow/artifacts";
5
+ export function createAstroArtifactPutTool(opts) {
6
+ const { ctx, db } = opts;
7
+ return tool({
8
+ description: "Write an artifact file under .astro and record it in the DB.",
9
+ args: {
10
+ run_id: tool.schema.string().nullable().optional(),
11
+ stage_key: tool.schema.string().nullable().optional(),
12
+ type: tool.schema.string().default("log"),
13
+ rel_path: tool.schema.string().min(1),
14
+ content: tool.schema.string().default(""),
15
+ meta_json: tool.schema.string().default("{}"),
16
+ },
17
+ execute: async ({ run_id, stage_key, type, rel_path, content, meta_json }) => {
18
+ const repoRoot = ctx.directory;
19
+ const meta = JSON.parse(meta_json || "{}");
20
+ const res = putArtifact({
21
+ repoRoot,
22
+ db,
23
+ run_id: run_id ?? null,
24
+ stage_key: stage_key ?? null,
25
+ type,
26
+ rel_path,
27
+ content,
28
+ meta,
29
+ });
30
+ return `✅ Wrote artifact ${res.artifact_id} (${type}) at ${rel_path} (sha256=${res.sha256})`;
31
+ },
32
+ });
33
+ }
34
+ export function createAstroArtifactListTool(opts) {
35
+ const { db } = opts;
36
+ return tool({
37
+ description: "List artifacts (optionally filtered by run_id, stage_key, type).",
38
+ args: {
39
+ run_id: tool.schema.string().optional(),
40
+ stage_key: tool.schema.string().optional(),
41
+ type: tool.schema.string().optional(),
42
+ },
43
+ execute: async ({ run_id, stage_key, type }) => {
44
+ const rows = listArtifacts(db, { run_id, stage_key, type });
45
+ return JSON.stringify(rows, null, 2);
46
+ },
47
+ });
48
+ }
49
+ export function createAstroArtifactGetTool(opts) {
50
+ const { ctx, config, db } = opts;
51
+ return tool({
52
+ description: "Get artifact metadata (and optionally file contents).",
53
+ args: {
54
+ artifact_id: tool.schema.string().min(1),
55
+ include_body: tool.schema.boolean().default(false),
56
+ max_body_chars: tool.schema.number().int().positive().default(50_000),
57
+ },
58
+ execute: async ({ artifact_id, include_body, max_body_chars }) => {
59
+ const row = getArtifact(db, artifact_id);
60
+ if (!row)
61
+ throw new Error(`Artifact not found: ${artifact_id}`);
62
+ if (!include_body)
63
+ return JSON.stringify(row, null, 2);
64
+ const repoRoot = ctx.directory;
65
+ const abs = path.join(repoRoot, row.path);
66
+ const body = fs.existsSync(abs) ? fs.readFileSync(abs, "utf-8") : "(missing file)";
67
+ const clipped = body.length > max_body_chars ? body.slice(0, max_body_chars) + "\n…(truncated)" : body;
68
+ return JSON.stringify({ ...row, body: clipped }, null, 2);
69
+ },
70
+ });
71
+ }
@@ -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 createAstroHealthTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
@@ -0,0 +1,88 @@
1
+ // src/tools/health.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import { getSchemaVersion } from "../state/db";
4
+ import { getActiveRun } from "../workflow/state-machine";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ export function createAstroHealthTool(opts) {
8
+ const { ctx, config, db } = opts;
9
+ return tool({
10
+ description: "Check Astrocode health: DB status, locks, schema, active runs, recent events.",
11
+ args: {},
12
+ execute: async () => {
13
+ const lines = [];
14
+ const repoRoot = ctx.directory || process.cwd();
15
+ const dbPath = config.db?.path || ".astro/astro.db";
16
+ const fullDbPath = path.resolve(repoRoot, dbPath);
17
+ // System info
18
+ lines.push("# Astrocode Health Check");
19
+ lines.push(`- PID: ${process.pid || "unknown"}`);
20
+ lines.push(`- Repo: ${repoRoot}`);
21
+ lines.push(`- DB Path: ${fullDbPath}`);
22
+ // DB file status
23
+ const dbExists = fs.existsSync(fullDbPath);
24
+ const walExists = fs.existsSync(`${fullDbPath}-wal`);
25
+ const shmExists = fs.existsSync(`${fullDbPath}-shm`);
26
+ lines.push(`- DB Files:`);
27
+ lines.push(` - Main: ${dbExists ? "EXISTS" : "MISSING"}`);
28
+ lines.push(` - WAL: ${walExists ? "EXISTS" : "MISSING"}`);
29
+ lines.push(` - SHM: ${shmExists ? "EXISTS" : "MISSING"}`);
30
+ if (!dbExists) {
31
+ lines.push(`- STATUS: DB MISSING - run astro_init first`);
32
+ return lines.join("\n");
33
+ }
34
+ // Schema version
35
+ try {
36
+ const schemaVersion = getSchemaVersion(db);
37
+ lines.push(`- Schema Version: ${schemaVersion}`);
38
+ }
39
+ catch (e) {
40
+ lines.push(`- Schema Version: ERROR (${String(e)})`);
41
+ lines.push(`- STATUS: DB CORRUPTED`);
42
+ return lines.join("\n");
43
+ }
44
+ // Active run
45
+ try {
46
+ const activeRun = getActiveRun(db);
47
+ if (activeRun) {
48
+ lines.push(`- Active Run: ${activeRun.run_id} (${activeRun.status})`);
49
+ lines.push(` - Story: ${activeRun.story_key}`);
50
+ lines.push(` - Stage: ${activeRun.current_stage_key || "none"}`);
51
+ lines.push(` - Started: ${activeRun.started_at}`);
52
+ }
53
+ else {
54
+ lines.push(`- Active Run: NONE`);
55
+ }
56
+ }
57
+ catch (e) {
58
+ lines.push(`- Active Run: ERROR (${String(e)})`);
59
+ }
60
+ // Recent events
61
+ try {
62
+ const events = db.prepare(`
63
+ SELECT event_id, run_id, stage_key, type, created_at
64
+ FROM events
65
+ ORDER BY created_at DESC
66
+ LIMIT 10
67
+ `).all();
68
+ lines.push(`- Recent Events (${events.length}):`);
69
+ for (const event of events) {
70
+ const stage = event.stage_key ? `/${event.stage_key}` : "";
71
+ lines.push(` - ${event.created_at}: ${event.type} (${event.run_id || "global"}${stage})`);
72
+ }
73
+ }
74
+ catch (e) {
75
+ lines.push(`- Recent Events: ERROR (${String(e)})`);
76
+ }
77
+ // Status summary
78
+ lines.push(``);
79
+ lines.push(`## Status`);
80
+ lines.push(`✅ DB accessible`);
81
+ lines.push(`✅ Schema valid`);
82
+ if (walExists || shmExists) {
83
+ lines.push(`⚠️ WAL/SHM files present - indicates unclean shutdown or active transaction`);
84
+ }
85
+ return lines.join("\n");
86
+ },
87
+ });
88
+ }
@@ -0,0 +1,20 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ import { AgentConfig } from "@opencode-ai/sdk";
5
+ type RuntimeState = {
6
+ db: SqliteDb | null;
7
+ limitedMode: boolean;
8
+ limitedModeReason: null | {
9
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
10
+ details: any;
11
+ };
12
+ };
13
+ type CreateAstroToolsOptions = {
14
+ ctx: any;
15
+ config: AstrocodeConfig;
16
+ agents?: Record<string, AgentConfig>;
17
+ runtime: RuntimeState;
18
+ };
19
+ export declare function createAstroTools(opts: CreateAstroToolsOptions): Record<string, ToolDefinition>;
20
+ export {};