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,19 @@
1
+ /** Normalize to posix-like separators for DB paths. */
2
+ export declare function toPosix(p: string): string;
3
+ export declare function ensureDir(p: string): void;
4
+ export declare function joinRepo(root: string, ...parts: string[]): string;
5
+ export type AstroPaths = {
6
+ repoRoot: string;
7
+ astroRoot: string;
8
+ dbPath: string;
9
+ runsDir: string;
10
+ specPath: string;
11
+ toolOutputDir: string;
12
+ configPathPreferred: string;
13
+ configPathFallback: string;
14
+ };
15
+ export declare function getAstroPaths(repoRoot: string, dbPathOverride?: string): AstroPaths;
16
+ export declare function ensureAstroDirs(paths: AstroPaths): void;
17
+ export declare function runDir(paths: AstroPaths, runId: string): string;
18
+ export declare function stageDir(paths: AstroPaths, runId: string, stageKey: string): string;
19
+ export declare function assertInsideAstro(repoRoot: string, filePath: string): void;
@@ -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,3 @@
1
+ export function nowISO() {
2
+ return new Date().toISOString();
3
+ }
@@ -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,67 @@
1
+ /**
2
+ * Acquire a repo-scoped lock with:
3
+ * - ✅ process-local caching + refcount (efficient repeated tool calls)
4
+ * - ✅ heartbeat lease + stale recovery
5
+ * - ✅ atomic create (`wx`) + portable replace fallback
6
+ * - ✅ dead PID eviction + stale eviction
7
+ * - ✅ no live takeover (even same session) to avoid concurrency stomps
8
+ * - ✅ ABA-safe release via lease_id fencing
9
+ * - ✅ exponential backoff + jitter to reduce FS churn
10
+ */
11
+ export declare function acquireRepoLock(opts: {
12
+ lockPath: string;
13
+ repoRoot: string;
14
+ sessionId?: string;
15
+ owner?: string;
16
+ retryMs?: number;
17
+ pollMs?: number;
18
+ pollMaxMs?: number;
19
+ staleMs?: number;
20
+ heartbeatMs?: number;
21
+ minWriteMs?: number;
22
+ }): Promise<{
23
+ release: () => void;
24
+ }>;
25
+ /**
26
+ * Helper wrapper: always releases lock.
27
+ */
28
+ export declare function withRepoLock<T>(opts: {
29
+ lockPath: string;
30
+ repoRoot: string;
31
+ sessionId?: string;
32
+ owner?: string;
33
+ fn: () => Promise<T>;
34
+ }): Promise<T>;
35
+ /**
36
+ * Lock diagnostics and status information.
37
+ */
38
+ export type LockStatus = {
39
+ exists: boolean;
40
+ path: string;
41
+ pid?: number;
42
+ pidAlive?: boolean;
43
+ instanceId?: string;
44
+ sessionId?: string;
45
+ owner?: string;
46
+ leaseId?: string;
47
+ createdAt?: string;
48
+ updatedAt?: string;
49
+ ageMs?: number;
50
+ isStale?: boolean;
51
+ repoRoot?: string;
52
+ version?: number;
53
+ };
54
+ /**
55
+ * Get lock file status and diagnostics.
56
+ * Returns detailed information about the current lock state.
57
+ */
58
+ export declare function getLockStatus(lockPath: string, staleMs?: number): LockStatus;
59
+ /**
60
+ * Attempt to remove a lock file if it's safe to do so.
61
+ * Only removes locks with dead PIDs or stale timestamps.
62
+ * Returns true if lock was removed, false if lock is still held.
63
+ */
64
+ export declare function tryRemoveStaleLock(lockPath: string, staleMs?: number): {
65
+ removed: boolean;
66
+ reason: string;
67
+ };