astrocode-workflow 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +210 -215
- package/dist/ui/inject.d.ts +6 -0
- package/dist/ui/inject.js +86 -67
- package/dist/workflow/state-machine.d.ts +32 -32
- package/dist/workflow/state-machine.js +85 -170
- 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 +123 -121
- package/src/ui/inject.ts +113 -79
- package/src/workflow/state-machine.ts +123 -227
- package/src/tools/workflow.ts.backup +0 -681
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
/** Normalize to posix-like separators for DB paths. */
|
|
4
|
+
export function toPosix(p) {
|
|
5
|
+
return p.split(path.sep).join("/");
|
|
6
|
+
}
|
|
7
|
+
export function ensureDir(p) {
|
|
8
|
+
fs.mkdirSync(p, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
export function joinRepo(root, ...parts) {
|
|
11
|
+
return path.join(root, ...parts);
|
|
12
|
+
}
|
|
13
|
+
export function getAstroPaths(repoRoot, dbPathOverride) {
|
|
14
|
+
const astroRoot = joinRepo(repoRoot, ".astro");
|
|
15
|
+
const runsDir = joinRepo(repoRoot, ".astro", "runs");
|
|
16
|
+
const dbPath = dbPathOverride ? joinRepo(repoRoot, dbPathOverride) : joinRepo(repoRoot, ".astro", "astro.db");
|
|
17
|
+
const specPath = joinRepo(repoRoot, ".astro", "spec.md");
|
|
18
|
+
const toolOutputDir = joinRepo(repoRoot, ".astro", "tool_output");
|
|
19
|
+
return {
|
|
20
|
+
repoRoot,
|
|
21
|
+
astroRoot,
|
|
22
|
+
dbPath,
|
|
23
|
+
runsDir,
|
|
24
|
+
specPath,
|
|
25
|
+
toolOutputDir,
|
|
26
|
+
configPathPreferred: joinRepo(repoRoot, ".astro", "astrocode.config.jsonc"),
|
|
27
|
+
configPathFallback: joinRepo(repoRoot, "astrocode.config.jsonc"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function ensureAstroDirs(paths) {
|
|
31
|
+
ensureDir(paths.astroRoot);
|
|
32
|
+
ensureDir(paths.runsDir);
|
|
33
|
+
ensureDir(paths.toolOutputDir);
|
|
34
|
+
}
|
|
35
|
+
export function runDir(paths, runId) {
|
|
36
|
+
return joinRepo(paths.repoRoot, ".astro", "runs", runId);
|
|
37
|
+
}
|
|
38
|
+
export function stageDir(paths, runId, stageKey) {
|
|
39
|
+
return joinRepo(paths.repoRoot, ".astro", "runs", runId, stageKey);
|
|
40
|
+
}
|
|
41
|
+
export function assertInsideAstro(repoRoot, filePath) {
|
|
42
|
+
const absRepo = path.resolve(repoRoot);
|
|
43
|
+
const abs = path.resolve(filePath);
|
|
44
|
+
const astroRoot = path.resolve(path.join(repoRoot, ".astro"));
|
|
45
|
+
// Allow writing certain files in the repo root
|
|
46
|
+
const relPath = path.relative(repoRoot, filePath);
|
|
47
|
+
const allowedOutside = [
|
|
48
|
+
"stories.md",
|
|
49
|
+
"README.md",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"CONTRIBUTING.md",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
".gitignore"
|
|
54
|
+
];
|
|
55
|
+
if (!abs.startsWith(astroRoot + path.sep) && abs !== astroRoot) {
|
|
56
|
+
// Check if it's an allowed file in repo root
|
|
57
|
+
if (!allowedOutside.includes(relPath)) {
|
|
58
|
+
throw new Error(`Refusing to write outside .astro: ${filePath} (relative: ${relPath}, astroRoot: ${astroRoot})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!abs.startsWith(absRepo + path.sep) && abs !== absRepo) {
|
|
62
|
+
throw new Error(`Refusing to write outside repo root: ${filePath}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function normalizeNewlines(s: string): string;
|
|
2
|
+
export declare function clampLines(md: string, maxLines: number): string;
|
|
3
|
+
export declare function clampChars(s: string, maxChars: number): string;
|
|
4
|
+
export declare function stripCodeFences(md: string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function normalizeNewlines(s) {
|
|
2
|
+
return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3
|
+
}
|
|
4
|
+
export function clampLines(md, maxLines) {
|
|
5
|
+
const lines = normalizeNewlines(md).split("\n");
|
|
6
|
+
if (lines.length <= maxLines)
|
|
7
|
+
return md.trimEnd();
|
|
8
|
+
return lines.slice(0, maxLines).join("\n").trimEnd() + "\n…";
|
|
9
|
+
}
|
|
10
|
+
export function clampChars(s, maxChars) {
|
|
11
|
+
if (s.length <= maxChars)
|
|
12
|
+
return s;
|
|
13
|
+
return s.slice(0, maxChars) + "\n…(truncated)";
|
|
14
|
+
}
|
|
15
|
+
export function stripCodeFences(md) {
|
|
16
|
+
// Light helper: remove surrounding triple backticks if present
|
|
17
|
+
const m = md.match(/^```[a-zA-Z0-9_-]*\n([\s\S]*)\n```\s*$/);
|
|
18
|
+
return m ? m[1] : md;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function nowISO(): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface DatabaseConnection {
|
|
2
|
+
pragma(sql: string): void;
|
|
3
|
+
exec(sql: string): void;
|
|
4
|
+
prepare(sql: string): Statement;
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface Statement {
|
|
8
|
+
run(...params: any[]): {
|
|
9
|
+
changes: number;
|
|
10
|
+
lastInsertRowid: any;
|
|
11
|
+
};
|
|
12
|
+
get(...params: any[]): any;
|
|
13
|
+
all(...params: any[]): any[];
|
|
14
|
+
}
|
|
15
|
+
export interface DatabaseAdapter {
|
|
16
|
+
isAvailable(): boolean;
|
|
17
|
+
open(path: string, opts?: {
|
|
18
|
+
busyTimeoutMs?: number;
|
|
19
|
+
}): DatabaseConnection;
|
|
20
|
+
}
|
|
21
|
+
export declare class NullAdapter implements DatabaseAdapter {
|
|
22
|
+
isAvailable(): boolean;
|
|
23
|
+
open(path: string, opts?: {
|
|
24
|
+
busyTimeoutMs?: number;
|
|
25
|
+
}): DatabaseConnection;
|
|
26
|
+
}
|
|
27
|
+
export declare class BunSqliteAdapter implements DatabaseAdapter {
|
|
28
|
+
isAvailable(): boolean;
|
|
29
|
+
open(path: string, opts?: {
|
|
30
|
+
busyTimeoutMs?: number;
|
|
31
|
+
}): DatabaseConnection;
|
|
32
|
+
}
|
|
33
|
+
export declare class BetterSqliteAdapter implements DatabaseAdapter {
|
|
34
|
+
private Database;
|
|
35
|
+
constructor();
|
|
36
|
+
isAvailable(): boolean;
|
|
37
|
+
open(path: string, opts?: {
|
|
38
|
+
busyTimeoutMs?: number;
|
|
39
|
+
}): DatabaseConnection;
|
|
40
|
+
}
|
|
41
|
+
export declare function createDatabaseAdapter(): DatabaseAdapter;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Null adapter for when no database driver is available
|
|
2
|
+
export class NullAdapter {
|
|
3
|
+
isAvailable() {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
open(path, opts) {
|
|
7
|
+
throw new Error("No SQLite driver available");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// Bun SQLite adapter - singleton pattern to avoid multiple initializations
|
|
11
|
+
let bunDatabase = null;
|
|
12
|
+
let bunDatabaseInitialized = false;
|
|
13
|
+
function initializeBunDatabase() {
|
|
14
|
+
if (bunDatabaseInitialized) {
|
|
15
|
+
return bunDatabase;
|
|
16
|
+
}
|
|
17
|
+
bunDatabaseInitialized = true;
|
|
18
|
+
try {
|
|
19
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
20
|
+
if (isBun) {
|
|
21
|
+
const { Database } = require('bun:sqlite');
|
|
22
|
+
bunDatabase = Database;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// bun:sqlite not available
|
|
27
|
+
}
|
|
28
|
+
return bunDatabase;
|
|
29
|
+
}
|
|
30
|
+
// Bun SQLite adapter
|
|
31
|
+
export class BunSqliteAdapter {
|
|
32
|
+
isAvailable() {
|
|
33
|
+
return !!initializeBunDatabase();
|
|
34
|
+
}
|
|
35
|
+
open(path, opts) {
|
|
36
|
+
const Database = initializeBunDatabase();
|
|
37
|
+
if (!Database) {
|
|
38
|
+
throw new Error("bun:sqlite not available");
|
|
39
|
+
}
|
|
40
|
+
const db = new Database(path);
|
|
41
|
+
// Configure database
|
|
42
|
+
if (opts?.busyTimeoutMs) {
|
|
43
|
+
db.exec(`PRAGMA busy_timeout = ${opts.busyTimeoutMs}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
pragma: (sql) => db.exec(`PRAGMA ${sql}`),
|
|
47
|
+
exec: (sql) => db.exec(sql),
|
|
48
|
+
prepare: (sql) => {
|
|
49
|
+
const stmt = db.prepare(sql);
|
|
50
|
+
return {
|
|
51
|
+
run: (...params) => stmt.run(...params),
|
|
52
|
+
get: (...params) => stmt.get(...params),
|
|
53
|
+
all: (...params) => stmt.all(...params)
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
close: () => db.close()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Better SQLite3 adapter
|
|
61
|
+
export class BetterSqliteAdapter {
|
|
62
|
+
Database = null;
|
|
63
|
+
constructor() {
|
|
64
|
+
try {
|
|
65
|
+
this.Database = require("better-sqlite3");
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
// better-sqlite3 not available
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
isAvailable() {
|
|
72
|
+
return !!this.Database;
|
|
73
|
+
}
|
|
74
|
+
open(path, opts) {
|
|
75
|
+
if (!this.Database) {
|
|
76
|
+
throw new Error("better-sqlite3 not available");
|
|
77
|
+
}
|
|
78
|
+
const db = new this.Database(path);
|
|
79
|
+
// Set busy timeout if specified
|
|
80
|
+
if (opts?.busyTimeoutMs) {
|
|
81
|
+
db.pragma(`busy_timeout = ${opts.busyTimeoutMs}`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
pragma: (sql) => db.pragma(sql),
|
|
85
|
+
exec: (sql) => db.exec(sql),
|
|
86
|
+
prepare: (sql) => db.prepare(sql),
|
|
87
|
+
close: () => db.close()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Factory function to create the appropriate adapter
|
|
92
|
+
export function createDatabaseAdapter() {
|
|
93
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
94
|
+
if (isBun) {
|
|
95
|
+
// Prefer bun:sqlite in Bun runtime
|
|
96
|
+
const bunAdapter = new BunSqliteAdapter();
|
|
97
|
+
if (bunAdapter.isAvailable())
|
|
98
|
+
return bunAdapter;
|
|
99
|
+
// Fallback to better-sqlite3
|
|
100
|
+
const betterAdapter = new BetterSqliteAdapter();
|
|
101
|
+
if (betterAdapter.isAvailable())
|
|
102
|
+
return betterAdapter;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Prefer better-sqlite3 in Node.js
|
|
106
|
+
const betterAdapter = new BetterSqliteAdapter();
|
|
107
|
+
if (betterAdapter.isAvailable())
|
|
108
|
+
return betterAdapter;
|
|
109
|
+
// Fallback to bun:sqlite (rare but possible)
|
|
110
|
+
const bunAdapter = new BunSqliteAdapter();
|
|
111
|
+
if (bunAdapter.isAvailable())
|
|
112
|
+
return bunAdapter;
|
|
113
|
+
}
|
|
114
|
+
return new NullAdapter();
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DatabaseConnection } from "./adapters";
|
|
2
|
+
export type SqliteDb = DatabaseConnection;
|
|
3
|
+
export declare function openSqlite(dbPath: string, opts?: {
|
|
4
|
+
busyTimeoutMs?: number;
|
|
5
|
+
}): SqliteDb;
|
|
6
|
+
export declare function configurePragmas(db: SqliteDb, pragmas: Record<string, any>): void;
|
|
7
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
8
|
+
export declare function withTx<T>(db: SqliteDb, fn: () => T, opts?: {
|
|
9
|
+
require?: boolean;
|
|
10
|
+
operation?: string;
|
|
11
|
+
}): T;
|
|
12
|
+
export declare function getSchemaVersion(db: SqliteDb): number;
|
|
13
|
+
export declare function ensureSchema(db: SqliteDb, opts?: {
|
|
14
|
+
allowAutoMigrate?: boolean;
|
|
15
|
+
silent?: boolean;
|
|
16
|
+
}): void;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/state/db.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { SCHEMA_SQL, SCHEMA_VERSION } from "./schema";
|
|
5
|
+
import { nowISO } from "../shared/time";
|
|
6
|
+
import { info, warn } from "../shared/log";
|
|
7
|
+
import { createDatabaseAdapter } from "./adapters";
|
|
8
|
+
import { recordTransaction } from "../shared/metrics";
|
|
9
|
+
/** Ensure directory exists for a file path. */
|
|
10
|
+
function ensureParentDir(filePath) {
|
|
11
|
+
const dir = path.dirname(filePath);
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
function tableExists(db, tableName) {
|
|
15
|
+
try {
|
|
16
|
+
const row = db
|
|
17
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
|
|
18
|
+
.get(tableName);
|
|
19
|
+
return row?.name === tableName;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function openSqlite(dbPath, opts) {
|
|
26
|
+
ensureParentDir(dbPath);
|
|
27
|
+
const adapter = createDatabaseAdapter();
|
|
28
|
+
if (!adapter.isAvailable()) {
|
|
29
|
+
throw new Error("No SQLite driver available (adapter unavailable).");
|
|
30
|
+
}
|
|
31
|
+
return adapter.open(dbPath, opts);
|
|
32
|
+
}
|
|
33
|
+
export function configurePragmas(db, pragmas) {
|
|
34
|
+
if (!pragmas || typeof pragmas !== "object")
|
|
35
|
+
return;
|
|
36
|
+
const known = new Set(["journal_mode", "synchronous", "foreign_keys", "temp_store"]);
|
|
37
|
+
for (const k of Object.keys(pragmas)) {
|
|
38
|
+
if (!known.has(k))
|
|
39
|
+
warn(`[Astrocode] Unknown pragma ignored: ${k}`);
|
|
40
|
+
}
|
|
41
|
+
if (pragmas.journal_mode)
|
|
42
|
+
db.pragma(`journal_mode = ${pragmas.journal_mode}`);
|
|
43
|
+
if (pragmas.synchronous)
|
|
44
|
+
db.pragma(`synchronous = ${pragmas.synchronous}`);
|
|
45
|
+
if (typeof pragmas.foreign_keys === "boolean")
|
|
46
|
+
db.pragma(`foreign_keys = ${pragmas.foreign_keys ? "ON" : "OFF"}`);
|
|
47
|
+
if (pragmas.temp_store)
|
|
48
|
+
db.pragma(`temp_store = ${pragmas.temp_store}`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Re-entrant transaction helper.
|
|
52
|
+
*
|
|
53
|
+
* SQLite rejects BEGIN inside BEGIN. We use:
|
|
54
|
+
* - depth=0: BEGIN IMMEDIATE ... COMMIT/ROLLBACK
|
|
55
|
+
* - depth>0: SAVEPOINT sp_n ... RELEASE / ROLLBACK TO + RELEASE
|
|
56
|
+
*
|
|
57
|
+
* This allows callers to safely nest withTx across layers (tools -> workflow -> state machine)
|
|
58
|
+
* without "cannot start a transaction within a transaction".
|
|
59
|
+
*/
|
|
60
|
+
const TX_DEPTH = new WeakMap();
|
|
61
|
+
function getDepth(db) {
|
|
62
|
+
return TX_DEPTH.get(db) ?? 0;
|
|
63
|
+
}
|
|
64
|
+
function setDepth(db, depth) {
|
|
65
|
+
if (depth <= 0)
|
|
66
|
+
TX_DEPTH.delete(db);
|
|
67
|
+
else
|
|
68
|
+
TX_DEPTH.set(db, depth);
|
|
69
|
+
}
|
|
70
|
+
function savepointName(depth) {
|
|
71
|
+
return `sp_${depth}`;
|
|
72
|
+
}
|
|
73
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
74
|
+
export function withTx(db, fn, opts) {
|
|
75
|
+
const adapter = createDatabaseAdapter();
|
|
76
|
+
const available = adapter.isAvailable();
|
|
77
|
+
if (!available) {
|
|
78
|
+
if (opts?.require)
|
|
79
|
+
throw new Error("Database adapter unavailable; transaction required.");
|
|
80
|
+
return fn();
|
|
81
|
+
}
|
|
82
|
+
const depth = getDepth(db);
|
|
83
|
+
const isNested = depth > 0;
|
|
84
|
+
const txRecorder = recordTransaction({ nestedDepth: depth, operation: opts?.operation });
|
|
85
|
+
if (depth === 0) {
|
|
86
|
+
const txStart = txRecorder.start();
|
|
87
|
+
db.exec("BEGIN IMMEDIATE");
|
|
88
|
+
setDepth(db, 1);
|
|
89
|
+
try {
|
|
90
|
+
const out = fn();
|
|
91
|
+
db.exec("COMMIT");
|
|
92
|
+
txRecorder.end(txStart, true);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
try {
|
|
97
|
+
db.exec("ROLLBACK");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
txRecorder.end(txStart, false);
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
setDepth(db, 0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Nested: use SAVEPOINT
|
|
110
|
+
const nextDepth = depth + 1;
|
|
111
|
+
const sp = savepointName(nextDepth);
|
|
112
|
+
const txStart = txRecorder.start();
|
|
113
|
+
db.exec(`SAVEPOINT ${sp}`);
|
|
114
|
+
setDepth(db, nextDepth);
|
|
115
|
+
try {
|
|
116
|
+
const out = fn();
|
|
117
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
118
|
+
txRecorder.end(txStart, true);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
try {
|
|
123
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore
|
|
133
|
+
}
|
|
134
|
+
txRecorder.end(txStart, false);
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
setDepth(db, depth);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function getSchemaVersion(db) {
|
|
142
|
+
try {
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare("SELECT schema_version FROM repo_state WHERE id = 1")
|
|
145
|
+
.get();
|
|
146
|
+
return row?.schema_version ?? 0;
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
150
|
+
if (msg.includes("no such table"))
|
|
151
|
+
return 0;
|
|
152
|
+
throw new Error(`Failed to read schema version: ${msg}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function migrateStageRunsCreatedAt(db) {
|
|
156
|
+
// v2 requires stage_runs.created_at to be meaningful; never write ''.
|
|
157
|
+
// This migration is best-effort and does not assume constraint rewrite support.
|
|
158
|
+
try {
|
|
159
|
+
const cols = db.prepare("PRAGMA table_info(stage_runs)").all();
|
|
160
|
+
const hasCreatedAt = cols.some((c) => c.name === "created_at");
|
|
161
|
+
if (hasCreatedAt)
|
|
162
|
+
return;
|
|
163
|
+
// Add nullable column first (avoid poison defaults).
|
|
164
|
+
db.exec("ALTER TABLE stage_runs ADD COLUMN created_at TEXT");
|
|
165
|
+
const now = nowISO();
|
|
166
|
+
// Backfill using best available timestamp fields, falling back to now.
|
|
167
|
+
db.prepare(`
|
|
168
|
+
UPDATE stage_runs
|
|
169
|
+
SET created_at =
|
|
170
|
+
COALESCE(
|
|
171
|
+
NULLIF(created_at, ''),
|
|
172
|
+
NULLIF(updated_at, ''),
|
|
173
|
+
NULLIF(started_at, ''),
|
|
174
|
+
?
|
|
175
|
+
)
|
|
176
|
+
WHERE created_at IS NULL OR created_at = ''
|
|
177
|
+
`).run(now);
|
|
178
|
+
info("[Astrocode] Added created_at column to stage_runs table (backfilled)");
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
// Ignore: table may not exist yet, or migration already applied.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function ensureSchema(db, opts) {
|
|
185
|
+
try {
|
|
186
|
+
return withTx(db, () => {
|
|
187
|
+
db.exec(SCHEMA_SQL);
|
|
188
|
+
// Deterministic assertions: if these fail, bootstrap is broken.
|
|
189
|
+
if (!tableExists(db, "repo_state"))
|
|
190
|
+
throw new Error("Schema missing required table repo_state after SCHEMA_SQL");
|
|
191
|
+
if (!tableExists(db, "story_keyseq"))
|
|
192
|
+
throw new Error("Schema missing required table story_keyseq after SCHEMA_SQL");
|
|
193
|
+
// Ensure required singleton rows exist.
|
|
194
|
+
db.prepare("INSERT OR IGNORE INTO story_keyseq (id, next_story_num) VALUES (1, 1)").run();
|
|
195
|
+
// Migrations for existing DBs.
|
|
196
|
+
migrateStageRunsCreatedAt(db);
|
|
197
|
+
const row = db
|
|
198
|
+
.prepare("SELECT schema_version FROM repo_state WHERE id = 1")
|
|
199
|
+
.get();
|
|
200
|
+
if (!row) {
|
|
201
|
+
const now = nowISO();
|
|
202
|
+
db.prepare("INSERT INTO repo_state (id, schema_version, created_at, updated_at) VALUES (1, ?, ?, ?)").run(SCHEMA_VERSION, now, now);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const currentVersion = row.schema_version ?? 0;
|
|
206
|
+
if (currentVersion === SCHEMA_VERSION)
|
|
207
|
+
return;
|
|
208
|
+
if (currentVersion > SCHEMA_VERSION) {
|
|
209
|
+
// Newer schema - no action (policy enforcement elsewhere).
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
213
|
+
if (opts?.allowAutoMigrate ?? true) {
|
|
214
|
+
db.prepare("UPDATE repo_state SET schema_version = ?, updated_at = ? WHERE id = 1").run(SCHEMA_VERSION, nowISO());
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}, { require: true });
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
if (opts?.silent)
|
|
221
|
+
return;
|
|
222
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
223
|
+
throw new Error(`Schema initialization failed: ${msg}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function newId(prefix: string): string;
|
|
2
|
+
export declare function newRunId(): string;
|
|
3
|
+
export declare function newStageRunId(): string;
|
|
4
|
+
export declare function newArtifactId(): string;
|
|
5
|
+
export declare function newToolRunId(): string;
|
|
6
|
+
export declare function newEventId(): string;
|
|
7
|
+
export declare function newSnapshotId(): string;
|
|
8
|
+
export declare function newBatchId(): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function newId(prefix) {
|
|
3
|
+
return `${prefix}_${randomUUID()}`;
|
|
4
|
+
}
|
|
5
|
+
export function newRunId() {
|
|
6
|
+
return newId("run");
|
|
7
|
+
}
|
|
8
|
+
export function newStageRunId() {
|
|
9
|
+
return newId("stage");
|
|
10
|
+
}
|
|
11
|
+
export function newArtifactId() {
|
|
12
|
+
return newId("art");
|
|
13
|
+
}
|
|
14
|
+
export function newToolRunId() {
|
|
15
|
+
return newId("tool");
|
|
16
|
+
}
|
|
17
|
+
export function newEventId() {
|
|
18
|
+
return newId("evt");
|
|
19
|
+
}
|
|
20
|
+
export function newSnapshotId() {
|
|
21
|
+
return newId("snap");
|
|
22
|
+
}
|
|
23
|
+
export function newBatchId() {
|
|
24
|
+
return newId("batch");
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/state/repo-lock.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function acquireRepoLock(lockPath) {
|
|
5
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
6
|
+
let fd;
|
|
7
|
+
try {
|
|
8
|
+
fd = fs.openSync(lockPath, "wx"); // exclusive create
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
const msg = e?.code === "EEXIST"
|
|
12
|
+
? `Astrocode lock is already held (${lockPath}). Another opencode process is running in this repo.`
|
|
13
|
+
: `Failed to acquire lock (${lockPath}): ${e?.message ?? String(e)}`;
|
|
14
|
+
throw new Error(msg);
|
|
15
|
+
}
|
|
16
|
+
fs.writeFileSync(fd, `${process.pid}\n`, "utf8");
|
|
17
|
+
return {
|
|
18
|
+
release: () => {
|
|
19
|
+
try {
|
|
20
|
+
fs.closeSync(fd);
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
try {
|
|
24
|
+
fs.unlinkSync(lockPath);
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const SCHEMA_VERSION = 2;
|
|
2
|
+
export declare const SCHEMA_SQL = "\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS repo_state (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n schema_version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n spec_hash_before TEXT,\n spec_hash_after TEXT,\n last_run_id TEXT,\n last_story_key TEXT,\n last_event_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS epics (\n epic_key TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n state TEXT NOT NULL DEFAULT 'active',\n priority INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS story_drafts (\n draft_id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n meta_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS story_keyseq (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n next_story_num INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS stories (\n story_key TEXT PRIMARY KEY,\n epic_key TEXT,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n state TEXT NOT NULL DEFAULT 'queued', -- queued|approved|in_progress|done|blocked|archived\n priority INTEGER NOT NULL DEFAULT 0,\n approved_at TEXT,\n locked_by_run_id TEXT,\n locked_at TEXT,\n in_progress INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n FOREIGN KEY (epic_key) REFERENCES epics(epic_key)\n);\n\nCREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n story_key TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'created', -- created|running|completed|failed|aborted\n pipeline_stages_json TEXT NOT NULL DEFAULT '[]',\n current_stage_key TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n completed_at TEXT,\n updated_at TEXT NOT NULL,\n error_text TEXT,\n FOREIGN KEY (story_key) REFERENCES stories(story_key)\n);\n\nCREATE TABLE IF NOT EXISTS stage_runs (\n stage_run_id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n stage_key TEXT NOT NULL,\n stage_index INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|skipped\n created_at TEXT NOT NULL,\n subagent_type TEXT,\n subagent_session_id TEXT,\n started_at TEXT,\n completed_at TEXT,\n updated_at TEXT NOT NULL,\n baton_path TEXT,\n summary_md TEXT,\n output_json TEXT,\n error_text TEXT,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS artifacts (\n artifact_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n type TEXT NOT NULL, -- plan|baton|evidence|diff|log|summary|commit|tool_output|snapshot\n path TEXT NOT NULL,\n sha256 TEXT,\n meta_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS tool_runs (\n tool_run_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n tool_name TEXT NOT NULL,\n args_json TEXT NOT NULL DEFAULT '{}',\n output_summary TEXT NOT NULL DEFAULT '',\n output_artifact_id TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS events (\n event_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n type TEXT NOT NULL,\n body_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS injects (\n inject_id TEXT PRIMARY KEY,\n type TEXT NOT NULL DEFAULT 'note',\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n tags_json TEXT NOT NULL DEFAULT '[]',\n scope TEXT NOT NULL DEFAULT 'repo', -- repo|run:<id>|story:<key>|global\n source TEXT NOT NULL DEFAULT 'user', -- user|tool|agent|import\n priority INTEGER NOT NULL DEFAULT 50,\n expires_at TEXT,\n sha256 TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS running_batches (\n batch_id TEXT PRIMARY KEY,\n run_id TEXT,\n session_id TEXT,\n status TEXT NOT NULL DEFAULT 'running', -- running|completed|failed|aborted\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS workflow_metrics (\n metric_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n name TEXT NOT NULL,\n value_num REAL,\n value_text TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS template_intents (\n intent_key TEXT PRIMARY KEY,\n body_md TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\n-- vNext tables\n\nCREATE TABLE IF NOT EXISTS story_relations (\n parent_story_key TEXT NOT NULL,\n child_story_key TEXT NOT NULL,\n relation_type TEXT NOT NULL DEFAULT 'split',\n reason TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n PRIMARY KEY (parent_story_key, child_story_key),\n FOREIGN KEY (parent_story_key) REFERENCES stories(story_key),\n FOREIGN KEY (child_story_key) REFERENCES stories(story_key)\n);\n\nCREATE TABLE IF NOT EXISTS continuations (\n continuation_id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n run_id TEXT,\n directive_hash TEXT NOT NULL,\n kind TEXT NOT NULL, -- continue|stage|blocked|repair\n reason TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_continuations_session_created ON continuations(session_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_continuations_run_created ON continuations(run_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS context_snapshots (\n snapshot_id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n stage_key TEXT NOT NULL,\n summary_md TEXT NOT NULL,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_context_snapshots_run_created ON context_snapshots(run_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS agent_sessions (\n session_id TEXT PRIMARY KEY,\n parent_session_id TEXT,\n agent_name TEXT NOT NULL,\n run_id TEXT,\n stage_key TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\n-- Indexes\n\nCREATE INDEX IF NOT EXISTS idx_stories_state ON stories(state);\nCREATE INDEX IF NOT EXISTS idx_runs_story ON runs(story_key);\nCREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);\nCREATE INDEX IF NOT EXISTS idx_stage_runs_run ON stage_runs(run_id, stage_index);\nCREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_key, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_scope_type_priority_updated ON injects(scope, type, priority DESC, updated_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_expires ON injects(expires_at) WHERE expires_at IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_injects_sha256 ON injects(sha256) WHERE sha256 IS NOT NULL;\n\n-- Stronger invariants (SQLite partial indexes)\n-- Only one run may be 'running' at a time (single-repo harness by default).\nCREATE UNIQUE INDEX IF NOT EXISTS uniq_single_running_run\n ON runs(status)\n WHERE status = 'running';\n\n-- Only one story may be in_progress=1 at a time (pairs with single running run).\nCREATE UNIQUE INDEX IF NOT EXISTS uniq_single_in_progress_story\n ON stories(in_progress)\n WHERE in_progress = 1;\n\n";
|