valent-pipeline 0.2.15 → 0.2.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -135,7 +135,7 @@ Then test other commands:
135
135
  ```bash
136
136
  valent-pipeline config validate # should exit 0
137
137
  valent-pipeline upgrade --dry-run # should show no changes (just installed)
138
- valent-pipeline db init # should create .valent-pipeline/pipeline.db
138
+ valent-pipeline db rebuild # indexes story artifacts (auto-creates DB if missing)
139
139
  valent-pipeline db rebuild # should complete (no stories to index yet)
140
140
  ```
141
141
 
@@ -62,6 +62,6 @@ Send inbox message to lead: `[EMBED-COMPLETE] Indexed {count} items.` (or `[EMBE
62
62
  ## Error Handling
63
63
 
64
64
  - If embed-instructions.md is missing: send `[BLOCKER]` to lead, terminate.
65
- - If SQLite database is missing: run `valent-pipeline db init` first, or skip DB instructions and index curated files only.
65
+ - If SQLite database is missing: it will be auto-created on first write. If the DB file cannot be created, skip DB instructions and index curated files only.
66
66
  - If ChromaDB connection fails (legacy mode): skip ChromaDB instructions, index curated files only, report partial completion.
67
67
  - If a curated file write fails: log the failure, continue to next instruction, report in completion message.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * db-bootstrap.ts — SQLite schema utilities for pipeline TS scripts.
3
+ *
4
+ * This file is the TypeScript-side copy of the schema defined in
5
+ * src/lib/db.js. Keep both files in sync when modifying the schema
6
+ * (see docs/design/refactor-checklist.md).
7
+ *
8
+ * Imported by embed-sqlite.ts and query-kb.ts to self-bootstrap the
9
+ * database — tables are created automatically if they don't exist.
10
+ */
11
+
12
+ import Database from 'better-sqlite3';
13
+ import { existsSync, readFileSync } from 'fs';
14
+ import { join } from 'path';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Schema DDL — must match src/lib/db.js SCHEMA_DDL exactly.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const SCHEMA_DDL = `
21
+ CREATE TABLE IF NOT EXISTS artifacts (
22
+ id TEXT PRIMARY KEY,
23
+ story_id TEXT NOT NULL,
24
+ agent TEXT NOT NULL,
25
+ artifact_type TEXT NOT NULL,
26
+ content TEXT NOT NULL,
27
+ metadata TEXT,
28
+ created_at TEXT DEFAULT (datetime('now')),
29
+ UNIQUE(story_id, artifact_type)
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS correction_directives (
33
+ id TEXT PRIMARY KEY,
34
+ target_agent TEXT NOT NULL,
35
+ directive TEXT NOT NULL,
36
+ reason TEXT,
37
+ status TEXT DEFAULT 'active',
38
+ created_batch INTEGER,
39
+ metadata TEXT
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS calibration (
43
+ story_id TEXT PRIMARY KEY,
44
+ story_points INTEGER,
45
+ ac_count INTEGER,
46
+ surface TEXT,
47
+ estimated_points INTEGER,
48
+ actual_mins REAL,
49
+ rework_cycles INTEGER DEFAULT 0,
50
+ sprint_id TEXT,
51
+ created_at TEXT DEFAULT (datetime('now'))
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS artifacts_working (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ story_id TEXT NOT NULL,
57
+ agent TEXT NOT NULL,
58
+ artifact_type TEXT NOT NULL,
59
+ content TEXT,
60
+ metadata TEXT,
61
+ created_at TEXT DEFAULT (datetime('now')),
62
+ UNIQUE(story_id, artifact_type)
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_artifacts_story ON artifacts(story_id);
66
+ CREATE INDEX IF NOT EXISTS idx_artifacts_type ON artifacts(artifact_type);
67
+ CREATE INDEX IF NOT EXISTS idx_artifacts_agent ON artifacts(agent);
68
+ CREATE INDEX IF NOT EXISTS idx_cd_target ON correction_directives(target_agent);
69
+ CREATE INDEX IF NOT EXISTS idx_cd_status ON correction_directives(status);
70
+ CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
71
+ `;
72
+
73
+ export const FTS_DDL = `
74
+ CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
75
+ content,
76
+ story_id UNINDEXED,
77
+ agent UNINDEXED,
78
+ artifact_type UNINDEXED,
79
+ content=artifacts,
80
+ content_rowid=rowid
81
+ );
82
+ `;
83
+
84
+ export const TRIGGERS_DDL = `
85
+ CREATE TRIGGER IF NOT EXISTS artifacts_ai AFTER INSERT ON artifacts BEGIN
86
+ INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
87
+ VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
88
+ END;
89
+
90
+ CREATE TRIGGER IF NOT EXISTS artifacts_ad AFTER DELETE ON artifacts BEGIN
91
+ INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
92
+ VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
93
+ END;
94
+
95
+ CREATE TRIGGER IF NOT EXISTS artifacts_au AFTER UPDATE ON artifacts BEGIN
96
+ INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
97
+ VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
98
+ INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
99
+ VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
100
+ END;
101
+ `;
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Schema management
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export function ensureSchema(db: InstanceType<typeof Database>): void {
108
+ db.exec(SCHEMA_DDL);
109
+ db.exec(FTS_DDL);
110
+ db.exec(TRIGGERS_DDL);
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // DB path resolution
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export function resolveDbPath(flagDbPath?: string): string {
118
+ if (flagDbPath) return flagDbPath;
119
+ const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
120
+ if (existsSync(configPath)) {
121
+ const content = readFileSync(configPath, 'utf-8');
122
+ const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
123
+ if (match) return join(process.cwd(), match[1].trim());
124
+ }
125
+ return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // High-level open helpers
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Open a read-write database connection with auto-schema creation.
134
+ * Creates the DB file and all tables if they don't exist.
135
+ */
136
+ export function openDb(dbPath: string): InstanceType<typeof Database> {
137
+ const db = new Database(dbPath);
138
+ ensureSchema(db);
139
+ return db;
140
+ }
141
+
142
+ /**
143
+ * Open a read-only database connection. Does NOT run schema DDL.
144
+ * Errors with a helpful message if the DB file does not exist.
145
+ */
146
+ export function openReadonlyDb(dbPath: string): InstanceType<typeof Database> {
147
+ if (!existsSync(dbPath)) {
148
+ console.error(`Database not found: ${dbPath}`);
149
+ console.error('Run "valent-pipeline init" or "valent-pipeline db init" to create it.');
150
+ process.exit(1);
151
+ }
152
+ return new Database(dbPath, { readonly: true });
153
+ }
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { readFileSync, existsSync, appendFileSync, mkdirSync, readdirSync, statSync } from 'fs';
20
20
  import { join, dirname, basename } from 'path';
21
- import Database from 'better-sqlite3';
21
+ import { openDb } from './db-bootstrap.js';
22
22
 
23
23
  // Parse CLI args
24
24
  const args = process.argv.slice(2);
@@ -99,7 +99,7 @@ async function indexSingleHandoff(
99
99
  return;
100
100
  }
101
101
 
102
- const db = new Database(dbPath);
102
+ const db = openDb(dbPath);
103
103
  const id = `${storyId}:${artifactType}`;
104
104
 
105
105
  db.prepare(`
@@ -133,7 +133,7 @@ async function rebuildAll(dbPath: string, storiesDir: string) {
133
133
  'story-report.md': { type: 'story-report', agent: 'JUDGE' },
134
134
  };
135
135
 
136
- const db = new Database(dbPath);
136
+ const db = openDb(dbPath);
137
137
  const insert = db.prepare(`
138
138
  INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
139
139
  VALUES (?, ?, ?, ?, ?, ?)
@@ -181,7 +181,7 @@ async function processEmbedInstructions(instructionsPath: string, dbPath: string
181
181
 
182
182
  console.log(`Parsed ${items.length} embed instructions`);
183
183
 
184
- const db = dryRun ? null : new Database(dbPath);
184
+ const db = dryRun ? null : openDb(dbPath);
185
185
  let indexed = 0;
186
186
  let curatedCount = 0;
187
187
 
@@ -25,8 +25,7 @@
25
25
  * # DB path defaults to .valent-pipeline/pipeline.db, override with --db-path
26
26
  */
27
27
 
28
- import Database from 'better-sqlite3';
29
- import { existsSync } from 'fs';
28
+ import { resolveDbPath, openReadonlyDb } from './db-bootstrap.js';
30
29
 
31
30
  const args = process.argv.slice(2);
32
31
  const flags: Record<string, string> = {};
@@ -48,15 +47,8 @@ for (let i = 0; i < args.length; i++) {
48
47
  }
49
48
  }
50
49
 
51
- const dbPath = flags['db-path'] || '.valent-pipeline/pipeline.db';
52
-
53
- if (!existsSync(dbPath)) {
54
- console.error(`Database not found: ${dbPath}`);
55
- console.error('Run "valent-pipeline db init" to create it.');
56
- process.exit(1);
57
- }
58
-
59
- const db = new Database(dbPath, { readonly: true });
50
+ const dbPath = resolveDbPath(flags['db-path']);
51
+ const db = openReadonlyDb(dbPath);
60
52
  const mode = modes[0];
61
53
 
62
54
  if (!mode) {
@@ -104,7 +104,7 @@ Ask the user to pick one:
104
104
  | `local-docker` | Local ChromaDB via Docker (legacy). Pipeline provides a docker-compose file. |
105
105
  | `connect-to-existing` | Remote/hosted ChromaDB instance for shared team knowledge (legacy). |
106
106
 
107
- **If `sqlite`:** Set `sqlite_db_path` to `./.valent-pipeline/pipeline.db` (default). Inform the user to run `valent-pipeline db init` to create the database, or it will be created automatically on first story run.
107
+ **If `sqlite`:** Set `sqlite_db_path` to `./.valent-pipeline/pipeline.db` (default). The database is auto-created during `valent-pipeline init` and on first use no separate `db init` step is needed.
108
108
 
109
109
  Also show the `knowledge_base_path` setting:
110
110
  - Default: `"./knowledge"`
@@ -71,7 +71,7 @@ Read these as needed to answer questions:
71
71
  → `npx valent-pipeline upgrade` — replaces infrastructure files, never touches your config or knowledge files.
72
72
 
73
73
  **"How do I initialize the SQLite database?"**
74
- `npx valent-pipeline db init` to create it, `npx valent-pipeline db rebuild` to re-index from existing story artifacts.
74
+ The database is auto-created during `init` and on first use. Run `npx valent-pipeline db rebuild` to re-index from existing story artifacts. Run `npx valent-pipeline db init` to manually reset it.
75
75
 
76
76
  ## Answer Format
77
77
 
@@ -42,7 +42,21 @@ From the source document, identify each story and extract:
42
42
  | Dependencies | No | Which stories must ship before this one can start (`depends_on`) |
43
43
  | Optional inputs | No | UX spec, architecture notes, scenario outlines if mentioned |
44
44
 
45
- **Priority assignment:** First story in first epic = 1, second story = 2, etc. Stories within the same epic are ordered by their natural sequence. Cross-epic dependencies use `depends_on`.
45
+ **Priority assignment E2E-first ordering:**
46
+
47
+ The goal is to get working end-to-end slices as early as possible, not to finish one epic before starting the next.
48
+
49
+ 1. **Identify vertical slices.** Scan all stories across all epics and group them into vertical slices — sets of stories that together produce an end-to-end testable path through the system. What counts as "end-to-end" depends on project type:
50
+ - **backend-api**: request entry point → business logic → data persistence → response
51
+ - **fullstack-web**: UI interaction → API call → backend processing → UI display
52
+ - **data-pipeline**: data ingestion → transformation → output/storage
53
+ Each slice should be the smallest set of stories that delivers a complete path. Prefer simpler stories (fewest ACs, fewest dependencies) for the first slice.
54
+
55
+ 2. **Order slices by value.** Rank slices so the most foundational or highest-value E2E path comes first. Later slices may share stories with earlier ones — don't duplicate, just note the dependency.
56
+
57
+ 3. **Assign priorities slice-first.** Assign priorities 1 through K to the first slice (ordered by dependency chain), K+1 through M to the second slice, and so on. Stories that appear in multiple slices keep the priority from their earliest slice.
58
+
59
+ 4. **Fill in the rest.** Assign remaining priorities to stories that don't belong to any vertical slice, in normal epic order (story sequence within epic, epics in document order). Cross-epic dependencies use `depends_on`.
46
60
 
47
61
  **Dependency detection:** Look for:
48
62
  - Explicit "depends on" or "requires" references
@@ -52,18 +66,27 @@ From the source document, identify each story and extract:
52
66
 
53
67
  ## Step 4: Present Summary for Approval
54
68
 
55
- Before writing any files, show the user:
69
+ Before writing any files, show the user with the vertical slice separated:
56
70
 
57
71
  ```
58
- Epic: {EPIC-NAME}
72
+ Vertical Slice 1 (E2E path: {description}):
73
+ {STORY-ID}: {title} [priority: 1] {depends_on if any}
74
+ {STORY-ID}: {title} [priority: 2] {depends_on if any}
75
+
76
+ Vertical Slice 2 (E2E path: {description}):
59
77
  {STORY-ID}: {title} [priority: {N}] {depends_on if any}
60
- {STORY-ID}: {title} [priority: {N}]
61
78
  ...
62
79
 
63
- Epic: {EPIC-NAME}
64
- ...
80
+ Remaining Backlog:
81
+ Epic: {EPIC-NAME}
82
+ {STORY-ID}: {title} [priority: {N}] {depends_on if any}
83
+ {STORY-ID}: {title} [priority: {N}]
84
+ ...
85
+
86
+ Epic: {EPIC-NAME}
87
+ ...
65
88
 
66
- Total: {N} stories across {M} epics
89
+ Total: {N} stories across {M} epics ({K} in {S} vertical slices)
67
90
  Dependencies: {list any depends_on relationships}
68
91
  ```
69
92
 
@@ -173,9 +196,8 @@ If the repo is empty or brand new (no source code yet), skip the subagents and c
173
196
  ### Step 7b: Initialize and Populate Knowledge Database
174
197
 
175
198
  After writing the curated knowledge files:
176
- 1. Run `node .valent-pipeline/bin/cli.js db init` to create the SQLite database if it doesn't exist
177
- 2. Run `node .valent-pipeline/bin/cli.js db rebuild` to index any existing story artifacts
178
- 3. The database is now ready for the Knowledge Agent to query during story execution
199
+ 1. Run `npx valent-pipeline db rebuild` to index any existing story artifacts (the database is auto-created if it doesn't exist)
200
+ 2. The database is now ready for the Knowledge Agent to query during story execution
179
201
 
180
202
  ## Step 8: Report
181
203
 
@@ -1,5 +1,6 @@
1
1
  import { join, dirname } from 'path';
2
2
  import { readFileSync, existsSync, appendFileSync, mkdirSync, readdirSync, statSync } from 'fs';
3
+ import { resolveDbPath, openDb } from '../lib/db.js';
3
4
 
4
5
  const ARTIFACT_MAP = {
5
6
  'reqs-brief.md': { type: 'reqs-brief', agent: 'REQS' },
@@ -19,22 +20,6 @@ const ARTIFACT_MAP = {
19
20
  'visual-validation-checklist.md': { type: 'visual-validation-checklist', agent: 'QA-A' },
20
21
  };
21
22
 
22
- function getDb(dbPath) {
23
- const Database = require(join(process.cwd(), '.valent-pipeline', 'node_modules', 'better-sqlite3'));
24
- return new Database(dbPath);
25
- }
26
-
27
- function resolveDbPath(options) {
28
- if (options.dbPath) return options.dbPath;
29
- const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
30
- if (existsSync(configPath)) {
31
- const content = readFileSync(configPath, 'utf-8');
32
- const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
33
- if (match) return join(process.cwd(), match[1].trim());
34
- }
35
- return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
36
- }
37
-
38
23
  export async function dbIndexHandoff(options) {
39
24
  const dbPath = resolveDbPath(options);
40
25
  const { file, storyId, agent, artifactType } = options;
@@ -50,7 +35,7 @@ export async function dbIndexHandoff(options) {
50
35
  return;
51
36
  }
52
37
 
53
- const db = getDb(dbPath);
38
+ const db = await openDb({ dbPath });
54
39
  const id = `${storyId}:${artifactType}`;
55
40
  db.prepare(`
56
41
  INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
@@ -70,7 +55,7 @@ export async function dbIndexWorking(options) {
70
55
  }
71
56
 
72
57
  const content = readFileSync(file, 'utf-8');
73
- const db = getDb(dbPath);
58
+ const db = await openDb({ dbPath });
74
59
  db.prepare(`
75
60
  INSERT OR REPLACE INTO artifacts_working (story_id, agent, artifact_type, content, metadata)
76
61
  VALUES (?, ?, ?, ?, ?)
@@ -83,7 +68,7 @@ export async function dbFlushWorking(options) {
83
68
  const dbPath = resolveDbPath(options);
84
69
  const { sprintId } = options;
85
70
 
86
- const db = getDb(dbPath);
71
+ const db = await openDb({ dbPath });
87
72
  const rows = db.prepare('SELECT * FROM artifacts_working').all();
88
73
 
89
74
  const insert = db.prepare(`
@@ -108,7 +93,7 @@ export async function dbQueryWorking(options) {
108
93
  const dbPath = resolveDbPath(options);
109
94
  const { sprintId, excludeStory } = options;
110
95
 
111
- const db = getDb(dbPath);
96
+ const db = await openDb({ dbPath });
112
97
  let rows;
113
98
  if (excludeStory) {
114
99
  rows = db.prepare(
@@ -133,7 +118,7 @@ export async function dbRecordCalibration(options) {
133
118
  const dbPath = resolveDbPath(options);
134
119
  const { storyId, storyPoints, acCount, surface, estimatedPoints, actualMins, reworkCycles, sprintId } = options;
135
120
 
136
- const db = getDb(dbPath);
121
+ const db = await openDb({ dbPath });
137
122
  db.prepare(`
138
123
  INSERT OR REPLACE INTO calibration (story_id, story_points, ac_count, surface, estimated_points, actual_mins, rework_cycles, sprint_id)
139
124
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
@@ -144,7 +129,7 @@ export async function dbRecordCalibration(options) {
144
129
 
145
130
  export async function dbQueryVelocity(options) {
146
131
  const dbPath = resolveDbPath(options);
147
- const db = getDb(dbPath);
132
+ const db = await openDb({ dbPath });
148
133
 
149
134
  const rows = db.prepare(`
150
135
  SELECT sprint_id, SUM(story_points) as points_shipped
@@ -182,7 +167,7 @@ export async function dbEmbed(options) {
182
167
  const items = parseEmbedInstructions(content);
183
168
  console.log(`Parsed ${items.length} embed instructions`);
184
169
 
185
- const db = getDb(dbPath);
170
+ const db = await openDb({ dbPath });
186
171
  let indexed = 0;
187
172
  let curatedCount = 0;
188
173
 
@@ -1,3 +1,4 @@
1
+ import { openDb, resolveDbPath } from '../lib/db.js';
1
2
  import { join } from 'path';
2
3
  import { existsSync, readFileSync } from 'fs';
3
4
 
@@ -16,113 +17,7 @@ export async function dbInit() {
16
17
  const dbPath = dbPathMatch ? dbPathMatch[1].trim() : './.valent-pipeline/pipeline.db';
17
18
  const fullDbPath = join(projectRoot, dbPath);
18
19
 
19
- let Database;
20
- let sqliteVec;
21
- try {
22
- Database = (await import('better-sqlite3')).default;
23
- sqliteVec = await import('sqlite-vec');
24
- } catch (err) {
25
- console.error('Error: better-sqlite3 or sqlite-vec not installed.');
26
- console.error('Run: npm install better-sqlite3 sqlite-vec');
27
- process.exit(1);
28
- }
29
-
30
- const db = new Database(fullDbPath);
31
-
32
- // Load sqlite-vec extension
33
- try {
34
- sqliteVec.load(db);
35
- console.log('Loaded sqlite-vec extension');
36
- } catch (err) {
37
- console.warn(`Warning: Could not load sqlite-vec: ${err.message}`);
38
- console.warn('Vector search will not be available. FTS5 will still work.');
39
- }
40
-
41
- // Create tables
42
- db.exec(`
43
- CREATE TABLE IF NOT EXISTS artifacts (
44
- id TEXT PRIMARY KEY,
45
- story_id TEXT NOT NULL,
46
- agent TEXT NOT NULL,
47
- artifact_type TEXT NOT NULL,
48
- content TEXT NOT NULL,
49
- metadata TEXT,
50
- created_at TEXT DEFAULT (datetime('now')),
51
- UNIQUE(story_id, artifact_type)
52
- );
53
-
54
- CREATE TABLE IF NOT EXISTS correction_directives (
55
- id TEXT PRIMARY KEY,
56
- target_agent TEXT NOT NULL,
57
- directive TEXT NOT NULL,
58
- reason TEXT,
59
- status TEXT DEFAULT 'active',
60
- created_batch INTEGER,
61
- metadata TEXT
62
- );
63
-
64
- CREATE INDEX IF NOT EXISTS idx_artifacts_story ON artifacts(story_id);
65
- CREATE INDEX IF NOT EXISTS idx_artifacts_type ON artifacts(artifact_type);
66
- CREATE INDEX IF NOT EXISTS idx_artifacts_agent ON artifacts(agent);
67
- CREATE INDEX IF NOT EXISTS idx_cd_target ON correction_directives(target_agent);
68
- CREATE INDEX IF NOT EXISTS idx_cd_status ON correction_directives(status);
69
-
70
- CREATE TABLE IF NOT EXISTS calibration (
71
- story_id TEXT PRIMARY KEY,
72
- story_points INTEGER,
73
- ac_count INTEGER,
74
- surface TEXT,
75
- estimated_points INTEGER,
76
- actual_mins REAL,
77
- rework_cycles INTEGER DEFAULT 0,
78
- sprint_id TEXT,
79
- created_at TEXT DEFAULT (datetime('now'))
80
- );
81
- CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
82
-
83
- CREATE TABLE IF NOT EXISTS artifacts_working (
84
- id INTEGER PRIMARY KEY AUTOINCREMENT,
85
- story_id TEXT NOT NULL,
86
- agent TEXT NOT NULL,
87
- artifact_type TEXT NOT NULL,
88
- content TEXT,
89
- metadata TEXT,
90
- created_at TEXT DEFAULT (datetime('now')),
91
- UNIQUE(story_id, artifact_type)
92
- );
93
- `);
94
-
95
- // Create FTS5 virtual table
96
- db.exec(`
97
- CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
98
- content,
99
- story_id UNINDEXED,
100
- agent UNINDEXED,
101
- artifact_type UNINDEXED,
102
- content=artifacts,
103
- content_rowid=rowid
104
- );
105
- `);
106
-
107
- // Create FTS triggers for auto-sync
108
- db.exec(`
109
- CREATE TRIGGER IF NOT EXISTS artifacts_ai AFTER INSERT ON artifacts BEGIN
110
- INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
111
- VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
112
- END;
113
-
114
- CREATE TRIGGER IF NOT EXISTS artifacts_ad AFTER DELETE ON artifacts BEGIN
115
- INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
116
- VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
117
- END;
118
-
119
- CREATE TRIGGER IF NOT EXISTS artifacts_au AFTER UPDATE ON artifacts BEGIN
120
- INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
121
- VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
122
- INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
123
- VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
124
- END;
125
- `);
20
+ const db = await openDb({ dbPath: fullDbPath, loadVec: true });
126
21
 
127
22
  console.log(`Database initialized at ${fullDbPath}`);
128
23
  console.log('Tables: artifacts, artifacts_fts, correction_directives, calibration, artifacts_working');
@@ -1,27 +1,10 @@
1
- import { join } from 'path';
2
- import { readFileSync, existsSync } from 'fs';
3
-
4
- function resolveDbPath(options) {
5
- if (options.dbPath) return options.dbPath;
6
- const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
7
- if (existsSync(configPath)) {
8
- const content = readFileSync(configPath, 'utf-8');
9
- const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
10
- if (match) return join(process.cwd(), match[1].trim());
11
- }
12
- return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
13
- }
14
-
15
- function getDb(dbPath) {
16
- const Database = require(join(process.cwd(), '.valent-pipeline', 'node_modules', 'better-sqlite3'));
17
- return new Database(dbPath, { readonly: true });
18
- }
1
+ import { resolveDbPath, openReadonlyDb } from '../lib/db.js';
19
2
 
20
3
  export async function dbQueryArtifact(options) {
21
4
  const dbPath = resolveDbPath(options);
5
+ const db = await openReadonlyDb({ dbPath });
22
6
  const { story, type } = options;
23
7
 
24
- const db = getDb(dbPath);
25
8
  const row = db.prepare(
26
9
  'SELECT content, agent, created_at FROM artifacts WHERE story_id = ? AND artifact_type = ?'
27
10
  ).get(story, type);
@@ -37,9 +20,9 @@ export async function dbQueryArtifact(options) {
37
20
 
38
21
  export async function dbQueryDirectives(options) {
39
22
  const dbPath = resolveDbPath(options);
23
+ const db = await openReadonlyDb({ dbPath });
40
24
  const { agent } = options;
41
25
 
42
- const db = getDb(dbPath);
43
26
  let rows;
44
27
  if (agent) {
45
28
  rows = db.prepare(
@@ -65,9 +48,9 @@ export async function dbQueryDirectives(options) {
65
48
 
66
49
  export async function dbQuerySearch(options) {
67
50
  const dbPath = resolveDbPath(options);
51
+ const db = await openReadonlyDb({ dbPath });
68
52
  const { query } = options;
69
53
 
70
- const db = getDb(dbPath);
71
54
  const rows = db.prepare(
72
55
  "SELECT story_id, agent, artifact_type, snippet(artifacts_fts, 0, '>>>', '<<<', '...', 40) as snippet FROM artifacts_fts WHERE artifacts_fts MATCH ? ORDER BY rank LIMIT 10"
73
56
  ).all(query);
@@ -84,9 +67,9 @@ export async function dbQuerySearch(options) {
84
67
 
85
68
  export async function dbQueryList(options) {
86
69
  const dbPath = resolveDbPath(options);
70
+ const db = await openReadonlyDb({ dbPath });
87
71
  const { story } = options;
88
72
 
89
- const db = getDb(dbPath);
90
73
  const rows = db.prepare(
91
74
  'SELECT artifact_type, agent, length(content) as size, created_at FROM artifacts WHERE story_id = ? ORDER BY created_at'
92
75
  ).all(story);
@@ -104,7 +87,8 @@ export async function dbQueryList(options) {
104
87
 
105
88
  export async function dbQueryStories(options) {
106
89
  const dbPath = resolveDbPath(options);
107
- const db = getDb(dbPath);
90
+ const db = await openReadonlyDb({ dbPath });
91
+
108
92
  const rows = db.prepare(
109
93
  'SELECT DISTINCT story_id, COUNT(*) as artifact_count FROM artifacts GROUP BY story_id ORDER BY story_id'
110
94
  ).all();
@@ -121,9 +105,9 @@ export async function dbQueryStories(options) {
121
105
 
122
106
  export async function dbQueryBugsSince(options) {
123
107
  const dbPath = resolveDbPath(options);
108
+ const db = await openReadonlyDb({ dbPath });
124
109
  const { since } = options;
125
110
 
126
- const db = getDb(dbPath);
127
111
  const rows = db.prepare(
128
112
  "SELECT story_id, content FROM artifacts WHERE artifact_type = 'bugs' AND created_at > ? ORDER BY created_at"
129
113
  ).all(since);
@@ -1,5 +1,6 @@
1
1
  import { join } from 'path';
2
2
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
+ import { openDb, resolveDbPath } from '../lib/db.js';
3
4
 
4
5
  // Artifact files we index (filename -> artifact_type mapping)
5
6
  const ARTIFACT_MAP = {
@@ -29,53 +30,14 @@ export async function dbRebuild() {
29
30
  process.exit(1);
30
31
  }
31
32
 
32
- // Read db path from config
33
+ // Read config for db path and stories directory
33
34
  const content = readFileSync(configPath, 'utf-8');
34
35
  const dbPathMatch = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
35
36
  const dbPath = dbPathMatch ? dbPathMatch[1].trim() : './.valent-pipeline/pipeline.db';
36
37
  const fullDbPath = join(projectRoot, dbPath);
37
38
 
38
- if (!existsSync(fullDbPath)) {
39
- console.error('Error: Database not found. Run "valent-pipeline db init" first.');
40
- process.exit(1);
41
- }
42
-
43
- let Database;
44
- try {
45
- Database = (await import('better-sqlite3')).default;
46
- } catch (err) {
47
- console.error('Error: better-sqlite3 not installed. Run: npm install better-sqlite3');
48
- process.exit(1);
49
- }
50
-
51
- const db = new Database(fullDbPath);
52
-
53
- // Ensure calibration and working tables exist
54
- db.exec(`
55
- CREATE TABLE IF NOT EXISTS calibration (
56
- story_id TEXT PRIMARY KEY,
57
- story_points INTEGER,
58
- ac_count INTEGER,
59
- surface TEXT,
60
- estimated_points INTEGER,
61
- actual_mins REAL,
62
- rework_cycles INTEGER DEFAULT 0,
63
- sprint_id TEXT,
64
- created_at TEXT DEFAULT (datetime('now'))
65
- );
66
- CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
67
-
68
- CREATE TABLE IF NOT EXISTS artifacts_working (
69
- id INTEGER PRIMARY KEY AUTOINCREMENT,
70
- story_id TEXT NOT NULL,
71
- agent TEXT NOT NULL,
72
- artifact_type TEXT NOT NULL,
73
- content TEXT,
74
- metadata TEXT,
75
- created_at TEXT DEFAULT (datetime('now')),
76
- UNIQUE(story_id, artifact_type)
77
- );
78
- `);
39
+ // openDb auto-creates schema if DB is new
40
+ const db = await openDb({ dbPath: fullDbPath });
79
41
 
80
42
  // Find stories directory
81
43
  const storyDirMatch = content.match(/story_directory:\s*"?([^"\n]+)"?/);
@@ -89,10 +89,12 @@ export async function init(options = {}) {
89
89
  writeFileSafe(vpPkgPath, JSON.stringify({
90
90
  "name": "valent-pipeline-runtime",
91
91
  "private": true,
92
+ "type": "module",
92
93
  "description": "Runtime dependencies for valent-pipeline scripts. Do not edit.",
93
94
  "dependencies": {
94
95
  "better-sqlite3": "^11.0.0",
95
- "sqlite-vec": "^0.1.0"
96
+ "sqlite-vec": "^0.1.0",
97
+ "tsx": "^4.0.0"
96
98
  }
97
99
  }, null, 2) + '\n');
98
100
  }
@@ -100,11 +102,23 @@ export async function init(options = {}) {
100
102
  const { execSync } = await import('child_process');
101
103
  try {
102
104
  execSync('npm install --production', { cwd: pipelineDest, stdio: 'pipe' });
103
- console.log(' Installed better-sqlite3 + sqlite-vec');
105
+ console.log(' Installed better-sqlite3 + sqlite-vec + tsx');
104
106
  } catch (err) {
105
107
  console.warn(' Warning: Failed to install SQLite dependencies. Run "cd .valent-pipeline && npm install" manually.');
106
108
  }
107
- console.log(' Run "valent-pipeline db init" to create the database.');
109
+
110
+ // Auto-create the SQLite database (no separate "db init" step needed)
111
+ try {
112
+ const { openDb } = await import('../lib/db.js');
113
+ const dbPathMatch = config.knowledge?.sqlite_db_path;
114
+ const fullDbPath = join(projectRoot, dbPathMatch || './.valent-pipeline/pipeline.db');
115
+ const db = await openDb({ dbPath: fullDbPath });
116
+ db.close();
117
+ console.log(' Created SQLite knowledge database');
118
+ } catch (err) {
119
+ console.warn(` Warning: Could not auto-create database: ${err.message}`);
120
+ console.warn(' Run "valent-pipeline db init" manually.');
121
+ }
108
122
  }
109
123
 
110
124
  // 7b. Install browser automation MCP for UI projects
@@ -91,6 +91,50 @@ export async function upgrade(options = {}) {
91
91
  }
92
92
  }
93
93
 
94
+ // Migrate runtime package.json: add "type": "module" and tsx if missing
95
+ const runtimePkgPath = join(pipelineDest, 'package.json');
96
+ if (fileExists(runtimePkgPath)) {
97
+ try {
98
+ const runtimePkg = JSON.parse(readFile(runtimePkgPath));
99
+ let needsInstall = false;
100
+
101
+ if (!runtimePkg.type) {
102
+ runtimePkg.type = 'module';
103
+ console.log('Migrated .valent-pipeline/package.json: added "type": "module"');
104
+ }
105
+
106
+ if (!runtimePkg.dependencies?.tsx) {
107
+ runtimePkg.dependencies = runtimePkg.dependencies || {};
108
+ runtimePkg.dependencies.tsx = '^4.0.0';
109
+ needsInstall = true;
110
+ console.log('Migrated .valent-pipeline/package.json: added tsx dependency');
111
+ }
112
+
113
+ writeFileSafe(runtimePkgPath, JSON.stringify(runtimePkg, null, 2) + '\n');
114
+
115
+ if (needsInstall) {
116
+ const { execSync } = await import('child_process');
117
+ try {
118
+ execSync('npm install --production', { cwd: pipelineDest, stdio: 'pipe' });
119
+ console.log('Installed new dependencies in .valent-pipeline/');
120
+ } catch (err) {
121
+ console.warn('Warning: Failed to install new dependencies. Run "cd .valent-pipeline && npm install" manually.');
122
+ }
123
+ }
124
+ } catch { /* skip if parse fails */ }
125
+ }
126
+
127
+ // Ensure database schema is up to date (adds any new tables from this version)
128
+ try {
129
+ const { openDb, resolveDbPath } = await import('../lib/db.js');
130
+ const dbPath = resolveDbPath();
131
+ const db = await openDb({ dbPath });
132
+ db.close();
133
+ console.log('Ensured database schema is up to date');
134
+ } catch {
135
+ // DB may not be configured — skip silently
136
+ }
137
+
94
138
  // Update version file
95
139
  writeFileSafe(versionFile, packageVersion);
96
140
  console.log(`Updated .valent-pipeline/.valent-version to ${packageVersion}`);
package/src/lib/db.js ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * db.js — Shared SQLite database utilities for valent-pipeline.
3
+ *
4
+ * Single source of truth for:
5
+ * - Schema DDL (all tables, indexes, FTS5, triggers)
6
+ * - DB path resolution from config
7
+ * - Database open/close with auto-schema creation
8
+ */
9
+
10
+ import { join } from 'path';
11
+ import { existsSync, readFileSync } from 'fs';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Schema DDL — authoritative source. Keep pipeline/scripts/db-bootstrap.ts
15
+ // in sync when modifying (see docs/design/refactor-checklist.md).
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export const SCHEMA_DDL = `
19
+ CREATE TABLE IF NOT EXISTS artifacts (
20
+ id TEXT PRIMARY KEY,
21
+ story_id TEXT NOT NULL,
22
+ agent TEXT NOT NULL,
23
+ artifact_type TEXT NOT NULL,
24
+ content TEXT NOT NULL,
25
+ metadata TEXT,
26
+ created_at TEXT DEFAULT (datetime('now')),
27
+ UNIQUE(story_id, artifact_type)
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS correction_directives (
31
+ id TEXT PRIMARY KEY,
32
+ target_agent TEXT NOT NULL,
33
+ directive TEXT NOT NULL,
34
+ reason TEXT,
35
+ status TEXT DEFAULT 'active',
36
+ created_batch INTEGER,
37
+ metadata TEXT
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS calibration (
41
+ story_id TEXT PRIMARY KEY,
42
+ story_points INTEGER,
43
+ ac_count INTEGER,
44
+ surface TEXT,
45
+ estimated_points INTEGER,
46
+ actual_mins REAL,
47
+ rework_cycles INTEGER DEFAULT 0,
48
+ sprint_id TEXT,
49
+ created_at TEXT DEFAULT (datetime('now'))
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS artifacts_working (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ story_id TEXT NOT NULL,
55
+ agent TEXT NOT NULL,
56
+ artifact_type TEXT NOT NULL,
57
+ content TEXT,
58
+ metadata TEXT,
59
+ created_at TEXT DEFAULT (datetime('now')),
60
+ UNIQUE(story_id, artifact_type)
61
+ );
62
+
63
+ CREATE INDEX IF NOT EXISTS idx_artifacts_story ON artifacts(story_id);
64
+ CREATE INDEX IF NOT EXISTS idx_artifacts_type ON artifacts(artifact_type);
65
+ CREATE INDEX IF NOT EXISTS idx_artifacts_agent ON artifacts(agent);
66
+ CREATE INDEX IF NOT EXISTS idx_cd_target ON correction_directives(target_agent);
67
+ CREATE INDEX IF NOT EXISTS idx_cd_status ON correction_directives(status);
68
+ CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
69
+ `;
70
+
71
+ export const FTS_DDL = `
72
+ CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
73
+ content,
74
+ story_id UNINDEXED,
75
+ agent UNINDEXED,
76
+ artifact_type UNINDEXED,
77
+ content=artifacts,
78
+ content_rowid=rowid
79
+ );
80
+ `;
81
+
82
+ export const TRIGGERS_DDL = `
83
+ CREATE TRIGGER IF NOT EXISTS artifacts_ai AFTER INSERT ON artifacts BEGIN
84
+ INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
85
+ VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
86
+ END;
87
+
88
+ CREATE TRIGGER IF NOT EXISTS artifacts_ad AFTER DELETE ON artifacts BEGIN
89
+ INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
90
+ VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
91
+ END;
92
+
93
+ CREATE TRIGGER IF NOT EXISTS artifacts_au AFTER UPDATE ON artifacts BEGIN
94
+ INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
95
+ VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
96
+ INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
97
+ VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
98
+ END;
99
+ `;
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // DB path resolution
103
+ // ---------------------------------------------------------------------------
104
+
105
+ export function resolveDbPath(options = {}) {
106
+ if (options.dbPath) return options.dbPath;
107
+ const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
108
+ if (existsSync(configPath)) {
109
+ const content = readFileSync(configPath, 'utf-8');
110
+ const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
111
+ if (match) return join(process.cwd(), match[1].trim());
112
+ }
113
+ return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Database loading
118
+ // ---------------------------------------------------------------------------
119
+
120
+ export async function loadDatabase() {
121
+ try {
122
+ return (await import('better-sqlite3')).default;
123
+ } catch (err) {
124
+ console.error('Error: better-sqlite3 not installed.');
125
+ console.error('Run: npm install better-sqlite3');
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ export async function loadSqliteVec() {
131
+ try {
132
+ return await import('sqlite-vec');
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Schema management
140
+ // ---------------------------------------------------------------------------
141
+
142
+ export function ensureSchema(db) {
143
+ db.exec(SCHEMA_DDL);
144
+ db.exec(FTS_DDL);
145
+ db.exec(TRIGGERS_DDL);
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // High-level open helpers
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Open a read-write database connection with auto-schema creation.
154
+ * Creates the DB file and all tables if they don't exist.
155
+ *
156
+ * @param {object} [options]
157
+ * @param {string} [options.dbPath] — explicit path, overrides config
158
+ * @param {boolean} [options.loadVec] — attempt to load sqlite-vec extension
159
+ * @returns {Promise<import('better-sqlite3').Database>}
160
+ */
161
+ export async function openDb(options = {}) {
162
+ const dbPath = typeof options === 'string' ? options : resolveDbPath(options);
163
+ const Database = await loadDatabase();
164
+ const db = new Database(dbPath);
165
+
166
+ if (options.loadVec !== false) {
167
+ const sqliteVec = await loadSqliteVec();
168
+ if (sqliteVec) {
169
+ try {
170
+ sqliteVec.load(db);
171
+ } catch {
172
+ // sqlite-vec not available — FTS5 still works
173
+ }
174
+ }
175
+ }
176
+
177
+ ensureSchema(db);
178
+ return db;
179
+ }
180
+
181
+ /**
182
+ * Open a read-only database connection. Does NOT run schema DDL.
183
+ * Errors with a helpful message if the DB file does not exist.
184
+ *
185
+ * @param {object} [options]
186
+ * @param {string} [options.dbPath] — explicit path, overrides config
187
+ * @returns {Promise<import('better-sqlite3').Database>}
188
+ */
189
+ export async function openReadonlyDb(options = {}) {
190
+ const dbPath = typeof options === 'string' ? options : resolveDbPath(options);
191
+
192
+ if (!existsSync(dbPath)) {
193
+ console.error(`Database not found: ${dbPath}`);
194
+ console.error('Run "valent-pipeline init" or "valent-pipeline db init" to create it.');
195
+ process.exit(1);
196
+ }
197
+
198
+ const Database = await loadDatabase();
199
+ return new Database(dbPath, { readonly: true });
200
+ }