astrocode-workflow 0.3.1 â 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.
- package/dist/index.js +6 -0
- package/dist/shared/metrics.d.ts +66 -0
- package/dist/shared/metrics.js +112 -0
- package/dist/src/agents/commands.d.ts +9 -0
- package/dist/src/agents/commands.js +121 -0
- package/dist/src/agents/prompts.d.ts +3 -0
- package/dist/src/agents/prompts.js +232 -0
- package/dist/src/agents/registry.d.ts +6 -0
- package/dist/src/agents/registry.js +242 -0
- package/dist/src/agents/types.d.ts +14 -0
- package/dist/src/agents/types.js +8 -0
- package/dist/src/config/config-handler.d.ts +4 -0
- package/dist/src/config/config-handler.js +46 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/loader.d.ts +11 -0
- package/dist/src/config/loader.js +82 -0
- package/dist/src/config/schema.d.ts +194 -0
- package/dist/src/config/schema.js +223 -0
- package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
- package/dist/src/hooks/continuation-enforcer.js +190 -0
- package/dist/src/hooks/inject-provider.d.ts +22 -0
- package/dist/src/hooks/inject-provider.js +120 -0
- package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
- package/dist/src/hooks/tool-output-truncator.js +57 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +308 -0
- package/dist/src/shared/deep-merge.d.ts +8 -0
- package/dist/src/shared/deep-merge.js +25 -0
- package/dist/src/shared/hash.d.ts +1 -0
- package/dist/src/shared/hash.js +4 -0
- package/dist/src/shared/log.d.ts +7 -0
- package/dist/src/shared/log.js +24 -0
- package/dist/src/shared/metrics.d.ts +66 -0
- package/dist/src/shared/metrics.js +112 -0
- package/dist/src/shared/model-tuning.d.ts +9 -0
- package/dist/src/shared/model-tuning.js +28 -0
- package/dist/src/shared/paths.d.ts +19 -0
- package/dist/src/shared/paths.js +64 -0
- package/dist/src/shared/text.d.ts +4 -0
- package/dist/src/shared/text.js +19 -0
- package/dist/src/shared/time.d.ts +1 -0
- package/dist/src/shared/time.js +3 -0
- package/dist/src/state/adapters/index.d.ts +41 -0
- package/dist/src/state/adapters/index.js +115 -0
- package/dist/src/state/db.d.ts +16 -0
- package/dist/src/state/db.js +225 -0
- package/dist/src/state/ids.d.ts +8 -0
- package/dist/src/state/ids.js +25 -0
- package/dist/src/state/repo-lock.d.ts +3 -0
- package/dist/src/state/repo-lock.js +29 -0
- package/dist/src/state/schema.d.ts +2 -0
- package/dist/src/state/schema.js +251 -0
- package/dist/src/state/types.d.ts +71 -0
- package/dist/src/state/types.js +1 -0
- package/dist/src/tools/artifacts.d.ts +18 -0
- package/dist/src/tools/artifacts.js +71 -0
- package/dist/src/tools/health.d.ts +8 -0
- package/dist/src/tools/health.js +119 -0
- package/dist/src/tools/index.d.ts +20 -0
- package/dist/src/tools/index.js +94 -0
- package/dist/src/tools/init.d.ts +17 -0
- package/dist/src/tools/init.js +96 -0
- package/dist/src/tools/injects.d.ts +53 -0
- package/dist/src/tools/injects.js +325 -0
- package/dist/src/tools/metrics.d.ts +7 -0
- package/dist/src/tools/metrics.js +61 -0
- package/dist/src/tools/repair.d.ts +8 -0
- package/dist/src/tools/repair.js +25 -0
- package/dist/src/tools/reset.d.ts +8 -0
- package/dist/src/tools/reset.js +92 -0
- package/dist/src/tools/run.d.ts +13 -0
- package/dist/src/tools/run.js +54 -0
- package/dist/src/tools/spec.d.ts +12 -0
- package/dist/src/tools/spec.js +44 -0
- package/dist/src/tools/stage.d.ts +23 -0
- package/dist/src/tools/stage.js +371 -0
- package/dist/src/tools/status.d.ts +8 -0
- package/dist/src/tools/status.js +125 -0
- package/dist/src/tools/story.d.ts +23 -0
- package/dist/src/tools/story.js +85 -0
- package/dist/src/tools/workflow.d.ts +13 -0
- package/dist/src/tools/workflow.js +355 -0
- package/dist/src/ui/inject.d.ts +12 -0
- package/dist/src/ui/inject.js +107 -0
- package/dist/src/ui/toasts.d.ts +13 -0
- package/dist/src/ui/toasts.js +39 -0
- package/dist/src/workflow/artifacts.d.ts +24 -0
- package/dist/src/workflow/artifacts.js +45 -0
- package/dist/src/workflow/baton.d.ts +72 -0
- package/dist/src/workflow/baton.js +166 -0
- package/dist/src/workflow/context.d.ts +20 -0
- package/dist/src/workflow/context.js +113 -0
- package/dist/src/workflow/directives.d.ts +39 -0
- package/dist/src/workflow/directives.js +137 -0
- package/dist/src/workflow/repair.d.ts +8 -0
- package/dist/src/workflow/repair.js +99 -0
- package/dist/src/workflow/state-machine.d.ts +86 -0
- package/dist/src/workflow/state-machine.js +216 -0
- package/dist/src/workflow/story-helpers.d.ts +9 -0
- package/dist/src/workflow/story-helpers.js +13 -0
- package/dist/state/db.d.ts +1 -0
- package/dist/state/db.js +9 -0
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/test/integration/db-transactions.test.d.ts +1 -0
- package/dist/test/integration/db-transactions.test.js +126 -0
- package/dist/test/integration/injection-metrics.test.d.ts +1 -0
- package/dist/test/integration/injection-metrics.test.js +129 -0
- package/dist/tools/health.d.ts +8 -0
- package/dist/tools/health.js +119 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/metrics.d.ts +7 -0
- package/dist/tools/metrics.js +61 -0
- package/dist/tools/reset.d.ts +8 -0
- package/dist/tools/reset.js +92 -0
- package/dist/tools/workflow.js +178 -168
- package/dist/ui/inject.js +21 -9
- package/package.json +6 -3
- package/src/index.ts +8 -0
- package/src/shared/metrics.ts +148 -0
- package/src/state/db.ts +10 -1
- package/src/state/repo-lock.ts +158 -0
- package/src/tools/health.ts +128 -0
- package/src/tools/index.ts +12 -3
- package/src/tools/init.ts +26 -14
- package/src/tools/metrics.ts +71 -0
- package/src/tools/repair.ts +21 -8
- package/src/tools/reset.ts +100 -0
- package/src/tools/stage.ts +12 -0
- package/src/tools/status.ts +17 -3
- package/src/tools/story.ts +41 -15
- package/src/tools/workflow.ts +15 -1
- package/src/ui/inject.ts +21 -9
package/src/state/db.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { SCHEMA_SQL, SCHEMA_VERSION } from "./schema";
|
|
|
5
5
|
import { nowISO } from "../shared/time";
|
|
6
6
|
import { info, warn } from "../shared/log";
|
|
7
7
|
import { createDatabaseAdapter, DatabaseConnection } from "./adapters";
|
|
8
|
+
import { recordTransaction } from "../shared/metrics";
|
|
8
9
|
|
|
9
10
|
export type SqliteDb = DatabaseConnection;
|
|
10
11
|
|
|
@@ -74,7 +75,7 @@ function savepointName(depth: number): string {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
77
|
-
export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean }): T {
|
|
78
|
+
export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean; operation?: string }): T {
|
|
78
79
|
const adapter = createDatabaseAdapter();
|
|
79
80
|
const available = adapter.isAvailable();
|
|
80
81
|
|
|
@@ -84,13 +85,17 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const depth = getDepth(db);
|
|
88
|
+
const isNested = depth > 0;
|
|
89
|
+
const txRecorder = recordTransaction({ nestedDepth: depth, operation: opts?.operation });
|
|
87
90
|
|
|
88
91
|
if (depth === 0) {
|
|
92
|
+
const txStart = txRecorder.start();
|
|
89
93
|
db.exec("BEGIN IMMEDIATE");
|
|
90
94
|
setDepth(db, 1);
|
|
91
95
|
try {
|
|
92
96
|
const out = fn();
|
|
93
97
|
db.exec("COMMIT");
|
|
98
|
+
txRecorder.end(txStart, true);
|
|
94
99
|
return out;
|
|
95
100
|
} catch (e) {
|
|
96
101
|
try {
|
|
@@ -98,6 +103,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
|
|
|
98
103
|
} catch {
|
|
99
104
|
// ignore
|
|
100
105
|
}
|
|
106
|
+
txRecorder.end(txStart, false);
|
|
101
107
|
throw e;
|
|
102
108
|
} finally {
|
|
103
109
|
setDepth(db, 0);
|
|
@@ -107,6 +113,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
|
|
|
107
113
|
// Nested: use SAVEPOINT
|
|
108
114
|
const nextDepth = depth + 1;
|
|
109
115
|
const sp = savepointName(nextDepth);
|
|
116
|
+
const txStart = txRecorder.start();
|
|
110
117
|
|
|
111
118
|
db.exec(`SAVEPOINT ${sp}`);
|
|
112
119
|
setDepth(db, nextDepth);
|
|
@@ -114,6 +121,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
|
|
|
114
121
|
try {
|
|
115
122
|
const out = fn();
|
|
116
123
|
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
124
|
+
txRecorder.end(txStart, true);
|
|
117
125
|
return out;
|
|
118
126
|
} catch (e) {
|
|
119
127
|
try {
|
|
@@ -126,6 +134,7 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
|
|
|
126
134
|
} catch {
|
|
127
135
|
// ignore
|
|
128
136
|
}
|
|
137
|
+
txRecorder.end(txStart, false);
|
|
129
138
|
throw e;
|
|
130
139
|
} finally {
|
|
131
140
|
setDepth(db, depth);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/state/repo-lock.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
type LockFile = {
|
|
6
|
+
pid: number;
|
|
7
|
+
created_at: string;
|
|
8
|
+
updated_at: string;
|
|
9
|
+
repo_root: string;
|
|
10
|
+
session_id?: string;
|
|
11
|
+
owner?: string; // optional human-readable owner
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function nowISO(): string {
|
|
15
|
+
return new Date().toISOString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sleep(ms: number) {
|
|
19
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isPidAlive(pid: number): boolean {
|
|
23
|
+
try {
|
|
24
|
+
// Signal 0 checks existence without killing.
|
|
25
|
+
(process as any).kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readLock(lockPath: string): LockFile | null {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(lockPath, "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw) as LockFile;
|
|
36
|
+
if (!parsed || typeof parsed.pid !== "number") return null;
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeLock(lockPath: string, lock: LockFile) {
|
|
44
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
45
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeUnlink(lockPath: string) {
|
|
49
|
+
try {
|
|
50
|
+
fs.unlinkSync(lockPath);
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Acquire a repo-scoped lock with:
|
|
58
|
+
* - Re-entrant behavior for SAME PID (your own process can call tools repeatedly)
|
|
59
|
+
* - Stale lock eviction for dead PIDs
|
|
60
|
+
* - Best-effort contention retry
|
|
61
|
+
*/
|
|
62
|
+
export async function acquireRepoLock(opts: {
|
|
63
|
+
lockPath: string;
|
|
64
|
+
repoRoot: string;
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
owner?: string;
|
|
67
|
+
retryMs?: number; // default 2000
|
|
68
|
+
pollMs?: number; // default 100
|
|
69
|
+
}): Promise<{ release: () => void }> {
|
|
70
|
+
const { lockPath, repoRoot, sessionId, owner } = opts;
|
|
71
|
+
const retryMs = opts.retryMs ?? 2000;
|
|
72
|
+
const pollMs = opts.pollMs ?? 100;
|
|
73
|
+
|
|
74
|
+
const myPid = (process as any).pid;
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
|
|
77
|
+
while (true) {
|
|
78
|
+
const existing = readLock(lockPath);
|
|
79
|
+
|
|
80
|
+
// No lock -> take it.
|
|
81
|
+
if (!existing) {
|
|
82
|
+
const now = nowISO();
|
|
83
|
+
writeLock(lockPath, {
|
|
84
|
+
pid: myPid,
|
|
85
|
+
created_at: now,
|
|
86
|
+
updated_at: now,
|
|
87
|
+
repo_root: repoRoot,
|
|
88
|
+
session_id: sessionId,
|
|
89
|
+
owner,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Verify we actually own it (race safety)
|
|
93
|
+
const verify = readLock(lockPath);
|
|
94
|
+
if (verify && verify.pid === myPid) {
|
|
95
|
+
return {
|
|
96
|
+
release: () => {
|
|
97
|
+
const cur = readLock(lockPath);
|
|
98
|
+
// Only the owner PID removes the lock.
|
|
99
|
+
if (cur && cur.pid === myPid) safeUnlink(lockPath);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Race lost; retry.
|
|
105
|
+
} else {
|
|
106
|
+
// Re-entrant: SAME PID owns lock -> refresh timestamp and proceed.
|
|
107
|
+
if (existing.pid === myPid) {
|
|
108
|
+
const now = nowISO();
|
|
109
|
+
writeLock(lockPath, { ...existing, updated_at: now, session_id: sessionId ?? existing.session_id, owner: owner ?? existing.owner });
|
|
110
|
+
return {
|
|
111
|
+
release: () => {
|
|
112
|
+
const cur = readLock(lockPath);
|
|
113
|
+
if (cur && cur.pid === myPid) safeUnlink(lockPath);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Another PID: if dead -> evict stale lock
|
|
119
|
+
if (!isPidAlive(existing.pid)) {
|
|
120
|
+
safeUnlink(lockPath);
|
|
121
|
+
// loop back and acquire
|
|
122
|
+
} else {
|
|
123
|
+
// Alive and not us -> wait bounded
|
|
124
|
+
if (Date.now() - startedAt > retryMs) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Astrocode lock is already held (${lockPath}). pid=${existing.pid} (alive). ` +
|
|
127
|
+
`Close other opencode processes or wait.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
await sleep(pollMs);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Helper wrapper: always releases lock.
|
|
138
|
+
*/
|
|
139
|
+
export async function withRepoLock<T>(opts: {
|
|
140
|
+
lockPath: string;
|
|
141
|
+
repoRoot: string;
|
|
142
|
+
sessionId?: string;
|
|
143
|
+
owner?: string;
|
|
144
|
+
fn: () => Promise<T>;
|
|
145
|
+
}): Promise<T> {
|
|
146
|
+
const handle = await acquireRepoLock({
|
|
147
|
+
lockPath: opts.lockPath,
|
|
148
|
+
repoRoot: opts.repoRoot,
|
|
149
|
+
sessionId: opts.sessionId,
|
|
150
|
+
owner: opts.owner,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
return await opts.fn();
|
|
155
|
+
} finally {
|
|
156
|
+
handle.release();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/tools/health.ts
|
|
2
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
3
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
4
|
+
import type { SqliteDb } from "../state/db";
|
|
5
|
+
import { getSchemaVersion } from "../state/db";
|
|
6
|
+
import { getActiveRun } from "../workflow/state-machine";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
export function createAstroHealthTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
|
|
11
|
+
const { ctx, config, db } = opts;
|
|
12
|
+
|
|
13
|
+
return tool({
|
|
14
|
+
description: "Check Astrocode health: DB status, locks, schema, active runs, recent events.",
|
|
15
|
+
args: {},
|
|
16
|
+
execute: async () => {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
const repoRoot = (ctx as any).directory || process.cwd();
|
|
19
|
+
const dbPath = config.db?.path || ".astro/astro.db";
|
|
20
|
+
const fullDbPath = path.resolve(repoRoot, dbPath);
|
|
21
|
+
|
|
22
|
+
// System info
|
|
23
|
+
lines.push("# Astrocode Health Check");
|
|
24
|
+
lines.push(`- PID: ${(process as any).pid || "unknown"}`);
|
|
25
|
+
lines.push(`- Repo: ${repoRoot}`);
|
|
26
|
+
lines.push(`- DB Path: ${fullDbPath}`);
|
|
27
|
+
|
|
28
|
+
// Lock status
|
|
29
|
+
const lockPath = `${repoRoot}/.astro/astro.lock`;
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(lockPath)) {
|
|
32
|
+
const lockContent = fs.readFileSync(lockPath, "utf8").trim();
|
|
33
|
+
const parts = lockContent.split(" ");
|
|
34
|
+
if (parts.length >= 2) {
|
|
35
|
+
const pid = parseInt(parts[0]);
|
|
36
|
+
const startedAt = parts[1];
|
|
37
|
+
|
|
38
|
+
// Check if PID is still running
|
|
39
|
+
try {
|
|
40
|
+
(process as any).kill(pid, 0); // Signal 0 just checks if process exists
|
|
41
|
+
lines.push(`- Lock: HELD by PID ${pid} (started ${startedAt})`);
|
|
42
|
+
} catch {
|
|
43
|
+
lines.push(`- Lock: STALE (PID ${pid} not running, started ${startedAt})`);
|
|
44
|
+
lines.push(` â Run: rm "${lockPath}"`);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
lines.push(`- Lock: MALFORMED (${lockContent})`);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
lines.push(`- Lock: NONE (no lock file)`);
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
lines.push(`- Lock: ERROR (${String(e)})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// DB file status
|
|
57
|
+
const dbExists = fs.existsSync(fullDbPath);
|
|
58
|
+
const walExists = fs.existsSync(`${fullDbPath}-wal`);
|
|
59
|
+
const shmExists = fs.existsSync(`${fullDbPath}-shm`);
|
|
60
|
+
|
|
61
|
+
lines.push(`- DB Files:`);
|
|
62
|
+
lines.push(` - Main: ${dbExists ? "EXISTS" : "MISSING"}`);
|
|
63
|
+
lines.push(` - WAL: ${walExists ? "EXISTS" : "MISSING"}`);
|
|
64
|
+
lines.push(` - SHM: ${shmExists ? "EXISTS" : "MISSING"}`);
|
|
65
|
+
|
|
66
|
+
if (!dbExists) {
|
|
67
|
+
lines.push(`- STATUS: DB MISSING - run astro_init first`);
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Schema version
|
|
72
|
+
try {
|
|
73
|
+
const schemaVersion = getSchemaVersion(db);
|
|
74
|
+
lines.push(`- Schema Version: ${schemaVersion}`);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
lines.push(`- Schema Version: ERROR (${String(e)})`);
|
|
77
|
+
lines.push(`- STATUS: DB CORRUPTED`);
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Active run
|
|
82
|
+
try {
|
|
83
|
+
const activeRun = getActiveRun(db);
|
|
84
|
+
if (activeRun) {
|
|
85
|
+
lines.push(`- Active Run: ${activeRun.run_id} (${activeRun.status})`);
|
|
86
|
+
lines.push(` - Story: ${activeRun.story_key}`);
|
|
87
|
+
lines.push(` - Stage: ${activeRun.current_stage_key || "none"}`);
|
|
88
|
+
lines.push(` - Started: ${activeRun.started_at}`);
|
|
89
|
+
} else {
|
|
90
|
+
lines.push(`- Active Run: NONE`);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
lines.push(`- Active Run: ERROR (${String(e)})`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Recent events
|
|
97
|
+
try {
|
|
98
|
+
const events = db.prepare(`
|
|
99
|
+
SELECT event_id, run_id, stage_key, type, created_at
|
|
100
|
+
FROM events
|
|
101
|
+
ORDER BY created_at DESC
|
|
102
|
+
LIMIT 10
|
|
103
|
+
`).all() as any[];
|
|
104
|
+
|
|
105
|
+
lines.push(`- Recent Events (${events.length}):`);
|
|
106
|
+
for (const event of events) {
|
|
107
|
+
const stage = event.stage_key ? `/${event.stage_key}` : "";
|
|
108
|
+
lines.push(` - ${event.created_at}: ${event.type} (${event.run_id || "global"}${stage})`);
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
lines.push(`- Recent Events: ERROR (${String(e)})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Status summary
|
|
115
|
+
lines.push(``);
|
|
116
|
+
lines.push(`## Status`);
|
|
117
|
+
lines.push(`â
DB accessible`);
|
|
118
|
+
lines.push(`â
Schema valid`);
|
|
119
|
+
lines.push(`â
Lock file checked`);
|
|
120
|
+
|
|
121
|
+
if (walExists || shmExists) {
|
|
122
|
+
lines.push(`â ī¸ WAL/SHM files present - indicates unclean shutdown or active transaction`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { createAstroStageStartTool, createAstroStageCompleteTool, createAstroSta
|
|
|
12
12
|
import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArtifactGetTool } from "./artifacts";
|
|
13
13
|
import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool, createAstroInjectEligibleTool, createAstroInjectDebugDueTool } from "./injects";
|
|
14
14
|
import { createAstroRepairTool } from "./repair";
|
|
15
|
+
import { createAstroHealthTool } from "./health";
|
|
16
|
+
import { createAstroResetTool } from "./reset";
|
|
17
|
+
import { createAstroMetricsTool } from "./metrics";
|
|
15
18
|
|
|
16
19
|
import { AgentConfig } from "@opencode-ai/sdk";
|
|
17
20
|
|
|
@@ -35,9 +38,12 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
|
|
|
35
38
|
|
|
36
39
|
const tools: Record<string, ToolDefinition> = {};
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// Always available tools (work without database - guaranteed DB-independent)
|
|
42
|
+
tools.astro_status = createAstroStatusTool({ ctx, config });
|
|
43
|
+
tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
|
|
44
|
+
tools.astro_health = createAstroHealthTool({ ctx, config, db });
|
|
45
|
+
tools.astro_reset = createAstroResetTool({ ctx, config, db });
|
|
46
|
+
tools.astro_metrics = createAstroMetricsTool({ ctx, config });
|
|
41
47
|
|
|
42
48
|
// Recovery tool - available even in limited mode to allow DB initialization
|
|
43
49
|
tools.astro_init = createAstroInitTool({ ctx, config, runtime });
|
|
@@ -100,6 +106,9 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
|
|
|
100
106
|
["_astro_inject_eligible", "astro_inject_eligible"],
|
|
101
107
|
["_astro_inject_debug_due", "astro_inject_debug_due"],
|
|
102
108
|
["_astro_repair", "astro_repair"],
|
|
109
|
+
["_astro_health", "astro_health"],
|
|
110
|
+
["_astro_reset", "astro_reset"],
|
|
111
|
+
["_astro_metrics", "astro_metrics"],
|
|
103
112
|
];
|
|
104
113
|
|
|
105
114
|
// Only add aliases for tools that exist
|
package/src/tools/init.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ensureSchema, openSqlite, configurePragmas } from "../state/db";
|
|
|
7
7
|
import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
|
|
8
8
|
import { nowISO } from "../shared/time";
|
|
9
9
|
import { sha256Hex } from "../shared/hash";
|
|
10
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
10
11
|
|
|
11
12
|
type RuntimeState = {
|
|
12
13
|
db: SqliteDb | null;
|
|
@@ -29,14 +30,23 @@ export function createAstroInitTool(opts: { ctx: any; config: AstrocodeConfig; r
|
|
|
29
30
|
},
|
|
30
31
|
execute: async ({ ensure_spec, spec_placeholder }) => {
|
|
31
32
|
const repoRoot = ctx.directory as string;
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
34
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
return withRepoLock({
|
|
37
|
+
lockPath,
|
|
38
|
+
repoRoot,
|
|
39
|
+
sessionId,
|
|
40
|
+
owner: "astro_init",
|
|
41
|
+
fn: async () => {
|
|
42
|
+
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
43
|
+
ensureAstroDirs(paths);
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
const hadDbAlready = !!runtime.db;
|
|
46
|
+
let db: SqliteDb | null = runtime.db;
|
|
47
|
+
let publishedToRuntime = false;
|
|
48
|
+
|
|
49
|
+
try {
|
|
40
50
|
if (!db) {
|
|
41
51
|
try {
|
|
42
52
|
db = openSqlite(paths.dbPath, { busyTimeoutMs: config.db.busy_timeout_ms });
|
|
@@ -106,14 +116,16 @@ export function createAstroInitTool(opts: { ctx: any; config: AstrocodeConfig; r
|
|
|
106
116
|
? `Next: run /astro-status. (DB recovered in-process.)`
|
|
107
117
|
: `Next: restart the agent/runtime if Astrocode is still in Limited Mode, then run /astro-status.`,
|
|
108
118
|
].join("\n");
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
} finally {
|
|
120
|
+
// Only close if this tool opened it AND we did not publish it for ongoing use.
|
|
121
|
+
if (!hadDbAlready && !publishedToRuntime && db && typeof db.close === "function") {
|
|
122
|
+
try {
|
|
123
|
+
db.close();
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
117
129
|
},
|
|
118
130
|
});
|
|
119
131
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/tools/metrics.ts
|
|
2
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
3
|
+
import { metrics } from "../shared/metrics";
|
|
4
|
+
|
|
5
|
+
type CreateAstroMetricsToolOptions = {
|
|
6
|
+
ctx: any;
|
|
7
|
+
config: any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function createAstroMetricsTool(opts: CreateAstroMetricsToolOptions): ToolDefinition {
|
|
11
|
+
return tool({
|
|
12
|
+
description: "Get performance metrics for Astrocode operations including transaction times, injection success rates, and error statistics.",
|
|
13
|
+
args: {},
|
|
14
|
+
execute: async () => {
|
|
15
|
+
return runMetricsTool();
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function runMetricsTool(): string {
|
|
21
|
+
const stats = metrics.getMetrics();
|
|
22
|
+
const txStats = metrics.getTransactionStats();
|
|
23
|
+
const injectionStats = metrics.getInjectionStats();
|
|
24
|
+
|
|
25
|
+
let output = "# Astrocode Performance Metrics\n\n";
|
|
26
|
+
|
|
27
|
+
// Transaction Stats
|
|
28
|
+
if (txStats) {
|
|
29
|
+
output += "## Database Transactions\n\n";
|
|
30
|
+
output += `**Total:** ${txStats.total}\n`;
|
|
31
|
+
output += `**Success Rate:** ${(txStats.successRate * 100).toFixed(1)}% (${txStats.successful}/${txStats.total})\n`;
|
|
32
|
+
output += `**Average Duration:** ${txStats.avgDuration.toFixed(2)}ms\n`;
|
|
33
|
+
output += `**Duration Range:** ${txStats.minDuration}ms - ${txStats.maxDuration}ms\n`;
|
|
34
|
+
output += `**Average Nesting Depth:** ${txStats.avgNestedDepth.toFixed(1)}\n\n`;
|
|
35
|
+
} else {
|
|
36
|
+
output += "## Database Transactions\n\nNo transaction data available.\n\n";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Injection Stats
|
|
40
|
+
if (injectionStats) {
|
|
41
|
+
output += "## UI Injections\n\n";
|
|
42
|
+
output += `**Total:** ${injectionStats.total}\n`;
|
|
43
|
+
output += `**Success Rate:** ${(injectionStats.successRate * 100).toFixed(1)}% (${injectionStats.successful}/${injectionStats.total})\n`;
|
|
44
|
+
output += `**Average Attempts:** ${injectionStats.avgAttempts.toFixed(1)}\n`;
|
|
45
|
+
output += `**Total Retries:** ${injectionStats.totalRetries}\n`;
|
|
46
|
+
output += `**Average Duration:** ${injectionStats.avgDuration.toFixed(2)}ms\n\n`;
|
|
47
|
+
} else {
|
|
48
|
+
output += "## UI Injections\n\nNo injection data available.\n\n";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Recent Errors
|
|
52
|
+
if (stats.errors.length > 0) {
|
|
53
|
+
output += "## Recent Errors\n\n";
|
|
54
|
+
const recentErrors = stats.errors.slice(-10); // Last 10 errors
|
|
55
|
+
for (const error of recentErrors) {
|
|
56
|
+
const timestamp = new Date(error.timestamp).toISOString();
|
|
57
|
+
output += `- **[${error.type}]** ${timestamp}: ${error.message}\n`;
|
|
58
|
+
}
|
|
59
|
+
output += "\n";
|
|
60
|
+
} else {
|
|
61
|
+
output += "## Recent Errors\n\nNo errors recorded.\n\n";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Raw Data Summary
|
|
65
|
+
output += "## Data Summary\n\n";
|
|
66
|
+
output += `**Transactions Tracked:** ${stats.transactions.length}\n`;
|
|
67
|
+
output += `**Injections Tracked:** ${stats.injections.length}\n`;
|
|
68
|
+
output += `**Errors Recorded:** ${stats.errors.length}\n`;
|
|
69
|
+
|
|
70
|
+
return output;
|
|
71
|
+
}
|
package/src/tools/repair.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
4
|
import type { SqliteDb } from "../state/db";
|
|
4
5
|
import { withTx } from "../state/db";
|
|
5
6
|
import { repairState, formatRepairReport } from "../workflow/repair";
|
|
6
7
|
import { putArtifact } from "../workflow/artifacts";
|
|
7
8
|
import { nowISO } from "../shared/time";
|
|
9
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
8
10
|
|
|
9
11
|
export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
|
|
10
12
|
const { ctx, config, db } = opts;
|
|
@@ -16,16 +18,27 @@ export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
16
18
|
},
|
|
17
19
|
execute: async ({ write_report_artifact }) => {
|
|
18
20
|
const repoRoot = ctx.directory as string;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
22
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return withRepoLock({
|
|
25
|
+
lockPath,
|
|
26
|
+
repoRoot,
|
|
27
|
+
sessionId,
|
|
28
|
+
owner: "astro_repair",
|
|
29
|
+
fn: async () => {
|
|
30
|
+
const report = withTx(db, () => repairState(db, config));
|
|
31
|
+
const md = formatRepairReport(report);
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
if (write_report_artifact) {
|
|
34
|
+
const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
|
|
35
|
+
const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: md, meta: { kind: "repair" } });
|
|
36
|
+
return md + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return md;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
29
42
|
},
|
|
30
43
|
});
|
|
31
44
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/tools/reset.ts
|
|
2
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
3
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
4
|
+
import type { SqliteDb } from "../state/db";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export function createAstroResetTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
|
|
9
|
+
const { ctx, config, db } = opts;
|
|
10
|
+
|
|
11
|
+
return tool({
|
|
12
|
+
description: "Reset Astrocode database: safely delete all DB files and WAL/SHM after killing concurrent processes.",
|
|
13
|
+
args: {
|
|
14
|
+
confirm: tool.schema.string().default("").describe("Type 'RESET' to confirm destructive operation"),
|
|
15
|
+
},
|
|
16
|
+
execute: async ({ confirm }) => {
|
|
17
|
+
if (confirm !== "RESET") {
|
|
18
|
+
return [
|
|
19
|
+
"â Reset cancelled - confirmation required",
|
|
20
|
+
"",
|
|
21
|
+
"This operation will:",
|
|
22
|
+
"- Delete .astro/astro.db",
|
|
23
|
+
"- Delete .astro/astro.db-wal (if exists)",
|
|
24
|
+
"- Delete .astro/astro.db-shm (if exists)",
|
|
25
|
+
"- Lose all workflow data, stories, runs, artifacts",
|
|
26
|
+
"",
|
|
27
|
+
"To confirm: astro_reset(confirm=\"RESET\")",
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const repoRoot = (ctx as any).directory || process.cwd();
|
|
32
|
+
const dbPath = config.db?.path || ".astro/astro.db";
|
|
33
|
+
const fullDbPath = path.resolve(repoRoot, dbPath);
|
|
34
|
+
|
|
35
|
+
const lines: string[] = [];
|
|
36
|
+
lines.push("đī¸ Astrocode Database Reset");
|
|
37
|
+
lines.push(`- Repo: ${repoRoot}`);
|
|
38
|
+
lines.push(`- Target: ${fullDbPath}`);
|
|
39
|
+
|
|
40
|
+
// Check for lock file
|
|
41
|
+
const lockPath = `${repoRoot}/.astro/astro.lock`;
|
|
42
|
+
if (fs.existsSync(lockPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const lockContent = fs.readFileSync(lockPath, "utf8").trim();
|
|
45
|
+
const pid = parseInt(lockContent.split(" ")[0]);
|
|
46
|
+
|
|
47
|
+
lines.push(`- Lock file found for PID ${pid}`);
|
|
48
|
+
|
|
49
|
+
// Try to kill the process
|
|
50
|
+
try {
|
|
51
|
+
(process as any).kill(pid, 'SIGTERM');
|
|
52
|
+
lines.push(`- Sent SIGTERM to PID ${pid}, waiting 2s...`);
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
54
|
+
} catch (e) {
|
|
55
|
+
lines.push(`- Could not kill PID ${pid}: ${String(e)}`);
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
lines.push(`- Error reading lock file: ${String(e)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Delete DB files
|
|
63
|
+
const filesToDelete = [
|
|
64
|
+
fullDbPath,
|
|
65
|
+
`${fullDbPath}-wal`,
|
|
66
|
+
`${fullDbPath}-shm`,
|
|
67
|
+
lockPath,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
let deletedCount = 0;
|
|
71
|
+
for (const filePath of filesToDelete) {
|
|
72
|
+
try {
|
|
73
|
+
if (fs.existsSync(filePath)) {
|
|
74
|
+
fs.unlinkSync(filePath);
|
|
75
|
+
lines.push(`- Deleted: ${path.relative(repoRoot, filePath)}`);
|
|
76
|
+
deletedCount++;
|
|
77
|
+
} else {
|
|
78
|
+
lines.push(`- Skipped: ${path.relative(repoRoot, filePath)} (not found)`);
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
lines.push(`- Failed to delete ${path.relative(repoRoot, filePath)}: ${String(e)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines.push(``);
|
|
86
|
+
if (deletedCount > 0) {
|
|
87
|
+
lines.push(`â
Reset complete - ${deletedCount} files deleted`);
|
|
88
|
+
lines.push(``);
|
|
89
|
+
lines.push(`Next steps:`);
|
|
90
|
+
lines.push(`1. Run: astro_init`);
|
|
91
|
+
lines.push(`2. Run: astro_status`);
|
|
92
|
+
lines.push(`3. Import your stories and restart workflow`);
|
|
93
|
+
} else {
|
|
94
|
+
lines.push(`âšī¸ No files found to delete`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|