valent-pipeline 0.2.16 → 0.2.18

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.16",
3
+ "version": "0.2.18",
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.
@@ -53,6 +53,7 @@ You receive many message types. Process each by type:
53
53
  Your outbound messages follow the same terse format:
54
54
  - `[SPAWN] Spawning {agent} for {story_id}. Role: {role}. Shared context: {story_output_dir}.`
55
55
  - `[CHECK-IN] {agent}: task {task} has been in_progress for {minutes}min. Status?`
56
+ - `[REVIEW-READY] Story {story_id}` — sent to READINESS immediately when a story reaches `readiness-review` during sprint grooming. Do not batch — send as soon as QA-A hands off each story.
56
57
  - `[TEARDOWN] Tearing down all teammates for {story_id}.`
57
58
  - `[ESCALATION] See escalation block below.`
58
59
 
@@ -12,7 +12,10 @@ Read `.valent-pipeline/steps/common/agent-protocol.md` for Communication Standar
12
12
 
13
13
  You are spawned at story kick-off but do NOT begin work immediately.
14
14
 
15
- - **Wait for:** `[HANDOFF]` from QA-A
15
+ **Standalone mode:** Wait for `[HANDOFF]` from QA-A, then review that story.
16
+
17
+ **Sprint mode (`{is_sprint_mode}` is true):** Lead sends you `[REVIEW-READY] Story {story_id}` each time a story reaches `readiness-review`. Begin reviewing that story immediately. When you finish (approve or reject), return to idle and wait for the next `[REVIEW-READY]` from Lead. You may receive the next signal while still reviewing — queue it and process sequentially.
18
+
16
19
  - **On approval:** Send `[READINESS-APPROVAL]` to BEND (and FEND if active). Send `[DONE]` to Lead. Mark task completed.
17
20
  - **On rejection:** Send `[READINESS-REJECTION]` to the **responsible agent** (REQS, UXA, or QA-A — see Rejection Routing below) AND to Lead. Do NOT send `[DONE]`. Do NOT mark task completed. Task stays `in_progress` — keeps BEND/FEND blocked. After agent revises and downstream re-completes, re-review.
18
21
  - **Escalate to:** Lead — for `[BLOCKER]`, `[ESCALATION]`, or any issue you cannot resolve peer-to-peer.
@@ -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) {
@@ -43,7 +43,8 @@ Process stories using assembly-line parallelism. Each agent moves to the next av
43
43
  --story-id {story_id} \
44
44
  --sprint-id {current_sprint_id}
45
45
  ```
46
- 9. READINESS reviews specs + cross-story checks
46
+ 9. Lead sends `[REVIEW-READY] Story {story_id}` to READINESS **immediately** do not wait for other stories to reach this status. READINESS begins reviewing as soon as it receives the signal (or queues it if already reviewing another story).
47
+ 10. READINESS reviews specs + cross-story checks
47
48
 
48
49
  **Status-based self-selection with type filtering:**
49
50
 
@@ -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
 
@@ -196,9 +196,8 @@ If the repo is empty or brand new (no source code yet), skip the subagents and c
196
196
  ### Step 7b: Initialize and Populate Knowledge Database
197
197
 
198
198
  After writing the curated knowledge files:
199
- 1. Run `node .valent-pipeline/bin/cli.js db init` to create the SQLite database if it doesn't exist
200
- 2. Run `node .valent-pipeline/bin/cli.js db rebuild` to index any existing story artifacts
201
- 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
202
201
 
203
202
  ## Step 8: Report
204
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
+ }