valent-pipeline 0.2.4 → 0.2.6

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/bin/cli.js CHANGED
@@ -75,4 +75,150 @@ dbCmd
75
75
  await dbRebuild();
76
76
  });
77
77
 
78
+ dbCmd
79
+ .command('index-handoff')
80
+ .description('Index a single handoff artifact into the database')
81
+ .requiredOption('--file <path>', 'Path to the artifact file')
82
+ .requiredOption('--story-id <id>', 'Story identifier')
83
+ .requiredOption('--agent <name>', 'Agent that produced the artifact')
84
+ .requiredOption('--artifact-type <type>', 'Artifact type (e.g., reqs-brief)')
85
+ .option('--db-path <path>', 'Database path (defaults to config)')
86
+ .action(async (options) => {
87
+ const { dbIndexHandoff } = await import('../src/commands/db-index.js');
88
+ await dbIndexHandoff(options);
89
+ });
90
+
91
+ dbCmd
92
+ .command('index-working')
93
+ .description('Index an artifact into the grooming working table')
94
+ .requiredOption('--file <path>', 'Path to the artifact file')
95
+ .requiredOption('--story-id <id>', 'Story identifier')
96
+ .requiredOption('--agent <name>', 'Agent that produced the artifact')
97
+ .requiredOption('--artifact-type <type>', 'Artifact type')
98
+ .option('--sprint-id <id>', 'Sprint identifier')
99
+ .option('--db-path <path>', 'Database path (defaults to config)')
100
+ .action(async (options) => {
101
+ const { dbIndexWorking } = await import('../src/commands/db-index.js');
102
+ await dbIndexWorking(options);
103
+ });
104
+
105
+ dbCmd
106
+ .command('flush-working')
107
+ .description('Flush working table to main artifacts table')
108
+ .option('--sprint-id <id>', 'Sprint identifier')
109
+ .option('--db-path <path>', 'Database path (defaults to config)')
110
+ .action(async (options) => {
111
+ const { dbFlushWorking } = await import('../src/commands/db-index.js');
112
+ await dbFlushWorking(options);
113
+ });
114
+
115
+ dbCmd
116
+ .command('query-working')
117
+ .description('Query artifacts from the grooming working table')
118
+ .option('--sprint-id <id>', 'Sprint identifier')
119
+ .option('--exclude-story <id>', 'Exclude this story from results')
120
+ .option('--db-path <path>', 'Database path (defaults to config)')
121
+ .action(async (options) => {
122
+ const { dbQueryWorking } = await import('../src/commands/db-index.js');
123
+ await dbQueryWorking(options);
124
+ });
125
+
126
+ dbCmd
127
+ .command('record-calibration')
128
+ .description('Record story calibration data for estimation accuracy')
129
+ .requiredOption('--story-id <id>', 'Story identifier')
130
+ .option('--story-points <n>', 'Fibonacci story points', parseInt)
131
+ .option('--ac-count <n>', 'Number of acceptance criteria', parseInt)
132
+ .option('--surface <type>', 'Surface area (backend, full-stack, etc.)')
133
+ .option('--estimated-points <n>', 'Estimated points', parseInt)
134
+ .option('--actual-mins <n>', 'Actual execution minutes', parseFloat)
135
+ .option('--rework-cycles <n>', 'Number of rework cycles', parseInt)
136
+ .option('--sprint-id <id>', 'Sprint identifier')
137
+ .option('--db-path <path>', 'Database path (defaults to config)')
138
+ .action(async (options) => {
139
+ const { dbRecordCalibration } = await import('../src/commands/db-index.js');
140
+ await dbRecordCalibration(options);
141
+ });
142
+
143
+ dbCmd
144
+ .command('query-velocity')
145
+ .description('Query sprint velocity history for estimation')
146
+ .option('--db-path <path>', 'Database path (defaults to config)')
147
+ .action(async (options) => {
148
+ const { dbQueryVelocity } = await import('../src/commands/db-index.js');
149
+ await dbQueryVelocity(options);
150
+ });
151
+
152
+ dbCmd
153
+ .command('embed')
154
+ .description('Process embed-instructions.md from retrospective')
155
+ .requiredOption('--file <path>', 'Path to embed-instructions.md')
156
+ .option('--curated-path <path>', 'Path to curated knowledge files')
157
+ .option('--db-path <path>', 'Database path (defaults to config)')
158
+ .action(async (options) => {
159
+ const { dbEmbed } = await import('../src/commands/db-index.js');
160
+ await dbEmbed(options);
161
+ });
162
+
163
+ // db query commands
164
+ dbCmd
165
+ .command('query-artifact')
166
+ .description('Get a specific artifact from the database')
167
+ .requiredOption('--story <id>', 'Story identifier')
168
+ .requiredOption('--type <type>', 'Artifact type')
169
+ .option('--db-path <path>', 'Database path (defaults to config)')
170
+ .action(async (options) => {
171
+ const { dbQueryArtifact } = await import('../src/commands/db-query.js');
172
+ await dbQueryArtifact(options);
173
+ });
174
+
175
+ dbCmd
176
+ .command('query-directives')
177
+ .description('Get active correction directives')
178
+ .option('--agent <name>', 'Filter by target agent')
179
+ .option('--db-path <path>', 'Database path (defaults to config)')
180
+ .action(async (options) => {
181
+ const { dbQueryDirectives } = await import('../src/commands/db-query.js');
182
+ await dbQueryDirectives(options);
183
+ });
184
+
185
+ dbCmd
186
+ .command('search')
187
+ .description('Full-text search across all artifacts')
188
+ .requiredOption('--query <text>', 'Search query')
189
+ .option('--db-path <path>', 'Database path (defaults to config)')
190
+ .action(async (options) => {
191
+ const { dbQuerySearch } = await import('../src/commands/db-query.js');
192
+ await dbQuerySearch(options);
193
+ });
194
+
195
+ dbCmd
196
+ .command('query-list')
197
+ .description('List all artifacts for a story')
198
+ .requiredOption('--story <id>', 'Story identifier')
199
+ .option('--db-path <path>', 'Database path (defaults to config)')
200
+ .action(async (options) => {
201
+ const { dbQueryList } = await import('../src/commands/db-query.js');
202
+ await dbQueryList(options);
203
+ });
204
+
205
+ dbCmd
206
+ .command('query-stories')
207
+ .description('List all stories in the database')
208
+ .option('--db-path <path>', 'Database path (defaults to config)')
209
+ .action(async (options) => {
210
+ const { dbQueryStories } = await import('../src/commands/db-query.js');
211
+ await dbQueryStories(options);
212
+ });
213
+
214
+ dbCmd
215
+ .command('query-bugs-since')
216
+ .description('Get bugs filed since a date')
217
+ .requiredOption('--since <date>', 'Date in YYYY-MM-DD format')
218
+ .option('--db-path <path>', 'Database path (defaults to config)')
219
+ .action(async (options) => {
220
+ const { dbQueryBugsSince } = await import('../src/commands/db-query.js');
221
+ await dbQueryBugsSince(options);
222
+ });
223
+
78
224
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,8 +30,7 @@ Verify `{story_output_dir}/embed-instructions.md` exists. If missing, send `[BLO
30
30
  **If `{knowledge_mode}` is `sqlite` (recommended):**
31
31
 
32
32
  ```bash
33
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts {story_output_dir}/embed-instructions.md \
34
- --db-path {sqlite_db_path} \
33
+ npx valent-pipeline db embed {story_output_dir}/embed-instructions.md \
35
34
  --curated-path {curated_files_path}
36
35
  ```
37
36
 
@@ -38,7 +38,7 @@ Read all files in `{curated_files_path}`. Build in-memory index of file names, s
38
38
  **If `{knowledge_mode}` is `sqlite`:**
39
39
  Verify the database is accessible by running:
40
40
  ```bash
41
- npx tsx .valent-pipeline/scripts/query-kb.ts --stories
41
+ npx valent-pipeline db query-stories
42
42
  ```
43
43
  If it returns results or "No stories in database", the DB is accessible. If the command fails, operate in curated-only mode.
44
44
 
@@ -53,12 +53,12 @@ For each incoming query:
53
53
  1. Search correction directives for relevant entries
54
54
  2. Search curated knowledge files for matching sections
55
55
  3. If database connected (SQLite mode): query using the CLI tool and read stdout for results:
56
- - Fetch a specific artifact: `npx tsx .valent-pipeline/scripts/query-kb.ts --artifact --story KANBAN-001 --type reqs-brief`
57
- - Fetch directives for an agent: `npx tsx .valent-pipeline/scripts/query-kb.ts --directives --agent BEND`
58
- - Full-text search: `npx tsx .valent-pipeline/scripts/query-kb.ts --search "acceptance criteria"`
59
- - List artifacts for a story: `npx tsx .valent-pipeline/scripts/query-kb.ts --list --story KANBAN-001`
60
- - List all stories: `npx tsx .valent-pipeline/scripts/query-kb.ts --stories`
61
- - Cross-story bug search: `npx tsx .valent-pipeline/scripts/query-kb.ts --bugs-since 2026-03-01`
56
+ - Fetch a specific artifact: `npx valent-pipeline db query-artifact --story KANBAN-001 --type reqs-brief`
57
+ - Fetch directives for an agent: `npx valent-pipeline db query-directives --agent BEND`
58
+ - Full-text search: `npx valent-pipeline db search --query "acceptance criteria"`
59
+ - List artifacts for a story: `npx valent-pipeline db query-list --story KANBAN-001`
60
+ - List all stories: `npx valent-pipeline db query-stories`
61
+ - Cross-story bug search: `npx valent-pipeline db query-bugs-since --since 2026-03-01`
62
62
  If ChromaDB mode: use collection query (ChromaDB)
63
63
  4. Compose response: targeted, SHORT (aim ~200 tokens, max 500)
64
64
  5. Include source reference: `Source: curated/{file}#section` or `Source: sqlite:artifacts/{story_id}/{type}` or `Source: correction-directives#{directive-id}`
@@ -493,8 +493,7 @@ This runs PMCP in parallel with QA-B's test execution, removing it from the crit
493
493
  When `{knowledge_mode}` is `sqlite` and you receive a `[HANDOFF]` from an agent that produces an output file, index the artifact into the SQLite database so downstream agents can query it via Knowledge:
494
494
 
495
495
  ```bash
496
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --index-handoff {story_output_dir}/{artifact_file} \
497
- --db-path {sqlite_db_path} \
496
+ npx valent-pipeline db index-handoff --file {story_output_dir}/{artifact_file} \
498
497
  --story-id {story_id} \
499
498
  --agent {agent_name} \
500
499
  --artifact-type {type}
@@ -589,8 +588,7 @@ Tear down all per-story teammates. Send `shutdown_request` to each individually.
589
588
  If `{knowledge_mode}` is `sqlite`, record story actuals to the calibration table for future estimation accuracy:
590
589
 
591
590
  ```bash
592
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --record-calibration \
593
- --db-path {sqlite_db_path} \
591
+ npx valent-pipeline db record-calibration \
594
592
  --story-id {story_id} \
595
593
  --ac-count {ac_count_from_reqs_brief} \
596
594
  --surface {project_type} \
@@ -20,8 +20,7 @@ For each story in grooming candidates (up to `{groom_target}` from sprint-init):
20
20
  4. On UXA handoff → update status to `test-case-development` → QA-A writes `qa-test-spec.md`
21
21
  5. On QA-A handoff → index all artifacts to SQLite **working table**:
22
22
  ```bash
23
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --index-working \
24
- --db-path {sqlite_db_path} \
23
+ npx valent-pipeline db index-working \
25
24
  --story-id {story_id} \
26
25
  --sprint-id {current_sprint_id}
27
26
  ```
@@ -47,8 +46,7 @@ After every `{sprint_max_groom_batch}` stories (default: 10), kill and respawn P
47
46
  After all stories groomed:
48
47
 
49
48
  ```bash
50
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --flush-working \
51
- --db-path {sqlite_db_path} \
49
+ npx valent-pipeline db flush-working \
52
50
  --sprint-id {current_sprint_id}
53
51
  ```
54
52
 
@@ -15,8 +15,7 @@ Sprint ID format:
15
15
  Query the calibration table for historical velocity data:
16
16
 
17
17
  ```bash
18
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --query-velocity \
19
- --db-path {sqlite_db_path}
18
+ npx valent-pipeline db query-velocity
20
19
  ```
21
20
 
22
21
  **Velocity rules:**
@@ -34,8 +34,7 @@ Update `sprint-{n}-status.yaml`:
34
34
  For each shipped story, ensure the calibration table has complete data:
35
35
 
36
36
  ```bash
37
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --update-calibration \
38
- --db-path {sqlite_db_path} \
37
+ npx valent-pipeline db record-calibration \
39
38
  --story-id {story_id} \
40
39
  --story-points {fibonacci_estimate} \
41
40
  --estimated-points {fibonacci_estimate} \
@@ -9,8 +9,7 @@ These steps run AFTER the standalone review (Steps 1-8) passes. If the standalon
9
9
  Query the SQLite working table for specs from other stories groomed in this sprint batch:
10
10
 
11
11
  ```bash
12
- npx tsx .valent-pipeline/scripts/embed-sqlite.ts --query-working \
13
- --db-path {sqlite_db_path} \
12
+ npx valent-pipeline db query-working \
14
13
  --sprint-id {current_sprint_id} \
15
14
  --exclude-story {story_id}
16
15
  ```
@@ -78,6 +78,21 @@ Present defaults and let the user adjust:
78
78
  | `retrospective_every_n_stories` | `5` | How often the Retrospective Agent runs (range: 3-10) |
79
79
  | `stall_threshold_minutes` | `15` | Minutes before a stalled agent gets a check-in (range: 5-30) |
80
80
 
81
+ ### Step 4b: Sprint Planning
82
+
83
+ Present defaults and let the user adjust. These settings control sprint-based planning in epic and project runs:
84
+
85
+ | Setting | Default | Description |
86
+ |---------|---------|-------------|
87
+ | `duration_minutes` | `480` | Sprint time budget in minutes (480 = 8 hours, "overnight") |
88
+ | `initial_velocity_points` | `60` | Bootstrap velocity until first sprint completes (range: 20-120) |
89
+ | `estimation_model` | `calibrated` | `calibrated` uses retro history for estimates; `baseline` estimates from specs only |
90
+ | `auto_plan` | `true` | Enable sprint planning in epic/project runs |
91
+ | `fibonacci_scale` | `[1, 2, 3, 5, 8, 13, 21]` | Point scale for story sizing (standard Fibonacci) |
92
+ | `max_groom_batch_size` | `10` | Kill and respawn Phase 1 agents after this many stories to manage context |
93
+
94
+ Note: Sprint config only affects `valent-run-epic` and `valent-run-project`. Standalone `valent-run-story` ignores it.
95
+
81
96
  ### Step 5: Knowledge Store Mode
82
97
 
83
98
  Ask the user to pick one:
@@ -174,7 +174,7 @@ If the repo is empty or brand new (no source code yet), skip the subagents and c
174
174
 
175
175
  After writing the curated knowledge files:
176
176
  1. Run `valent-pipeline db init` to create the SQLite database if it doesn't exist
177
- 2. Run `npx tsx .valent-pipeline/scripts/embed-sqlite.ts --rebuild --db-path .valent-pipeline/pipeline.db --stories-dir ./stories` to index any existing story artifacts
177
+ 2. Run `npx valent-pipeline db rebuild` to index any existing story artifacts
178
178
  3. The database is now ready for the Knowledge Agent to query during story execution
179
179
 
180
180
  ## Step 8: Report
@@ -0,0 +1,254 @@
1
+ import { join, dirname } from 'path';
2
+ import { readFileSync, existsSync, appendFileSync, mkdirSync, readdirSync, statSync } from 'fs';
3
+
4
+ const ARTIFACT_MAP = {
5
+ 'reqs-brief.md': { type: 'reqs-brief', agent: 'REQS' },
6
+ 'uxa-spec.md': { type: 'uxa-spec', agent: 'UXA' },
7
+ 'qa-test-spec.md': { type: 'qa-test-spec', agent: 'QA-A' },
8
+ 'bend-handoff.md': { type: 'bend-handoff', agent: 'BEND' },
9
+ 'fend-handoff.md': { type: 'fend-handoff', agent: 'FEND' },
10
+ 'critic-review.md': { type: 'critic-review', agent: 'CRITIC' },
11
+ 'execution-report.md': { type: 'execution-report', agent: 'QA-B' },
12
+ 'bugs.md': { type: 'bugs', agent: 'QA-B' },
13
+ 'traceability-matrix.md': { type: 'traceability-matrix', agent: 'QA-B' },
14
+ 'readiness-review.md': { type: 'readiness-review', agent: 'READINESS' },
15
+ 'judge-review.md': { type: 'judge-review', agent: 'JUDGE' },
16
+ 'judge-decision.md': { type: 'judge-decision', agent: 'JUDGE' },
17
+ 'story-report.md': { type: 'story-report', agent: 'JUDGE' },
18
+ 'pmcp-evidence.md': { type: 'pmcp-evidence', agent: 'PMCP' },
19
+ 'visual-validation-checklist.md': { type: 'visual-validation-checklist', agent: 'QA-A' },
20
+ };
21
+
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
+ export async function dbIndexHandoff(options) {
39
+ const dbPath = resolveDbPath(options);
40
+ const { file, storyId, agent, artifactType } = options;
41
+
42
+ if (!existsSync(file)) {
43
+ console.error(`File not found: ${file}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ const content = readFileSync(file, 'utf-8');
48
+ if (content.includes('status: skipped')) {
49
+ console.log(`Skipping pass-through artifact: ${file}`);
50
+ return;
51
+ }
52
+
53
+ const db = getDb(dbPath);
54
+ const id = `${storyId}:${artifactType}`;
55
+ db.prepare(`
56
+ INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
57
+ VALUES (?, ?, ?, ?, ?, ?)
58
+ `).run(id, storyId, agent, artifactType, content, JSON.stringify({ source: 'handoff', file }));
59
+ db.close();
60
+ console.log(`Indexed ${id} (${content.length} chars)`);
61
+ }
62
+
63
+ export async function dbIndexWorking(options) {
64
+ const dbPath = resolveDbPath(options);
65
+ const { file, storyId, agent, artifactType, sprintId } = options;
66
+
67
+ if (!existsSync(file)) {
68
+ console.error(`File not found: ${file}`);
69
+ process.exit(1);
70
+ }
71
+
72
+ const content = readFileSync(file, 'utf-8');
73
+ const db = getDb(dbPath);
74
+ db.prepare(`
75
+ INSERT OR REPLACE INTO artifacts_working (story_id, agent, artifact_type, content, metadata)
76
+ VALUES (?, ?, ?, ?, ?)
77
+ `).run(storyId, agent, artifactType, content, JSON.stringify({ sprint_id: sprintId, file }));
78
+ db.close();
79
+ console.log(`Indexed to working table: ${storyId}:${artifactType}`);
80
+ }
81
+
82
+ export async function dbFlushWorking(options) {
83
+ const dbPath = resolveDbPath(options);
84
+ const { sprintId } = options;
85
+
86
+ const db = getDb(dbPath);
87
+ const rows = db.prepare('SELECT * FROM artifacts_working').all();
88
+
89
+ const insert = db.prepare(`
90
+ INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
91
+ VALUES (?, ?, ?, ?, ?, ?)
92
+ `);
93
+
94
+ const flush = db.transaction(() => {
95
+ for (const row of rows) {
96
+ const id = `${row.story_id}:${row.artifact_type}`;
97
+ insert.run(id, row.story_id, row.agent, row.artifact_type, row.content, row.metadata);
98
+ }
99
+ db.prepare('DELETE FROM artifacts_working').run();
100
+ });
101
+
102
+ flush();
103
+ db.close();
104
+ console.log(`Flushed ${rows.length} artifacts from working table to main table`);
105
+ }
106
+
107
+ export async function dbQueryWorking(options) {
108
+ const dbPath = resolveDbPath(options);
109
+ const { sprintId, excludeStory } = options;
110
+
111
+ const db = getDb(dbPath);
112
+ let rows;
113
+ if (excludeStory) {
114
+ rows = db.prepare(
115
+ 'SELECT story_id, agent, artifact_type, content FROM artifacts_working WHERE story_id != ?'
116
+ ).all(excludeStory);
117
+ } else {
118
+ rows = db.prepare('SELECT story_id, agent, artifact_type, content FROM artifacts_working').all();
119
+ }
120
+ db.close();
121
+
122
+ if (rows.length === 0) {
123
+ console.log('No artifacts in working table');
124
+ } else {
125
+ for (const r of rows) {
126
+ console.log(`--- ${r.story_id}:${r.artifact_type} (${r.agent}) ---`);
127
+ console.log(r.content);
128
+ }
129
+ }
130
+ }
131
+
132
+ export async function dbRecordCalibration(options) {
133
+ const dbPath = resolveDbPath(options);
134
+ const { storyId, storyPoints, acCount, surface, estimatedPoints, actualMins, reworkCycles, sprintId } = options;
135
+
136
+ const db = getDb(dbPath);
137
+ db.prepare(`
138
+ INSERT OR REPLACE INTO calibration (story_id, story_points, ac_count, surface, estimated_points, actual_mins, rework_cycles, sprint_id)
139
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
140
+ `).run(storyId, storyPoints ?? null, acCount ?? null, surface ?? null, estimatedPoints ?? null, actualMins ?? null, reworkCycles ?? 0, sprintId ?? null);
141
+ db.close();
142
+ console.log(`Recorded calibration data for ${storyId}`);
143
+ }
144
+
145
+ export async function dbQueryVelocity(options) {
146
+ const dbPath = resolveDbPath(options);
147
+ const db = getDb(dbPath);
148
+
149
+ const rows = db.prepare(`
150
+ SELECT sprint_id, SUM(story_points) as points_shipped
151
+ FROM calibration
152
+ WHERE sprint_id IS NOT NULL AND story_points IS NOT NULL
153
+ GROUP BY sprint_id
154
+ ORDER BY created_at DESC
155
+ LIMIT 5
156
+ `).all();
157
+
158
+ db.close();
159
+
160
+ if (rows.length === 0) {
161
+ console.log('No sprint velocity data available');
162
+ } else {
163
+ console.log('Sprint velocity history (most recent first):');
164
+ for (const r of rows) {
165
+ console.log(` ${r.sprint_id}: ${r.points_shipped} points`);
166
+ }
167
+ const avg = rows.reduce((sum, r) => sum + r.points_shipped, 0) / rows.length;
168
+ console.log(` SMA-${rows.length}: ${Math.round(avg)} points`);
169
+ }
170
+ }
171
+
172
+ export async function dbEmbed(options) {
173
+ const dbPath = resolveDbPath(options);
174
+ const { file, curatedPath } = options;
175
+
176
+ if (!existsSync(file)) {
177
+ console.error(`File not found: ${file}`);
178
+ process.exit(1);
179
+ }
180
+
181
+ const content = readFileSync(file, 'utf-8');
182
+ const items = parseEmbedInstructions(content);
183
+ console.log(`Parsed ${items.length} embed instructions`);
184
+
185
+ const db = getDb(dbPath);
186
+ let indexed = 0;
187
+ let curatedCount = 0;
188
+
189
+ for (const item of items) {
190
+ if (item.target.startsWith('curated/')) {
191
+ const targetFile = join(curatedPath || './knowledge/curated', item.target.replace('curated/', ''));
192
+ mkdirSync(dirname(targetFile), { recursive: true });
193
+
194
+ if (item.metadata?.append_section && existsSync(targetFile)) {
195
+ const existing = readFileSync(targetFile, 'utf-8');
196
+ if (existing.includes(`## ${item.metadata.append_section}`)) {
197
+ console.log(` Skipped duplicate section: ${item.metadata.append_section}`);
198
+ continue;
199
+ }
200
+ }
201
+
202
+ const section = item.metadata?.append_section
203
+ ? `\n## ${item.metadata.append_section}\n\n${item.content}\n`
204
+ : `\n${item.content}\n`;
205
+ appendFileSync(targetFile, section, 'utf-8');
206
+ curatedCount++;
207
+ } else {
208
+ const id = `embed:${Date.now()}:${indexed}`;
209
+ db.prepare(`
210
+ INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
211
+ VALUES (?, ?, ?, ?, ?, ?)
212
+ `).run(id, item.metadata?.stories?.[0] || 'batch', 'Retrospective', item.metadata?.category || 'pattern', item.content, JSON.stringify(item.metadata || {}));
213
+ indexed++;
214
+ }
215
+ }
216
+
217
+ db.close();
218
+ console.log(`Complete: ${indexed} items indexed, ${curatedCount} curated files updated`);
219
+ }
220
+
221
+ function parseEmbedInstructions(content) {
222
+ const items = [];
223
+ const sections = content.split(/^### \d+\./m).filter(s => s.trim());
224
+
225
+ for (const section of sections) {
226
+ const yamlMatch = section.match(/```yaml\n([\s\S]*?)```/);
227
+ if (!yamlMatch) continue;
228
+
229
+ const yamlContent = yamlMatch[1];
230
+ const contentMatch = yamlContent.match(/content:\s*>\s*\n([\s\S]*?)(?=\n\s+target:|$)/);
231
+ const targetMatch = yamlContent.match(/target:\s*"?([^"\n]+)"?/);
232
+
233
+ if (contentMatch && targetMatch) {
234
+ const itemContent = contentMatch[1].split('\n').map(l => l.trim()).join('\n').trim();
235
+ const target = targetMatch[1].trim();
236
+ const metadata = {};
237
+ const metadataMatch = yamlContent.match(/metadata:\n([\s\S]*?)(?=\n\S|$)/);
238
+ if (metadataMatch) {
239
+ for (const line of metadataMatch[1].split('\n')) {
240
+ const kv = line.match(/^\s+(\w+):\s+(.+)$/);
241
+ if (kv) {
242
+ let val = kv[2].trim();
243
+ if (val.startsWith('[') && val.endsWith(']')) {
244
+ val = val.slice(1, -1).split(',').map(s => s.trim());
245
+ }
246
+ metadata[kv[1]] = val;
247
+ }
248
+ }
249
+ }
250
+ items.push({ content: itemContent, target, metadata });
251
+ }
252
+ }
253
+ return items;
254
+ }
@@ -0,0 +1,140 @@
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
+ }
19
+
20
+ export async function dbQueryArtifact(options) {
21
+ const dbPath = resolveDbPath(options);
22
+ const { story, type } = options;
23
+
24
+ const db = getDb(dbPath);
25
+ const row = db.prepare(
26
+ 'SELECT content, agent, created_at FROM artifacts WHERE story_id = ? AND artifact_type = ?'
27
+ ).get(story, type);
28
+ db.close();
29
+
30
+ if (row) {
31
+ console.log(`--- ${type} for ${story} (by ${row.agent}, ${row.created_at}) ---`);
32
+ console.log(row.content);
33
+ } else {
34
+ console.log(`No artifact found: story=${story}, type=${type}`);
35
+ }
36
+ }
37
+
38
+ export async function dbQueryDirectives(options) {
39
+ const dbPath = resolveDbPath(options);
40
+ const { agent } = options;
41
+
42
+ const db = getDb(dbPath);
43
+ let rows;
44
+ if (agent) {
45
+ rows = db.prepare(
46
+ "SELECT id, directive, reason FROM correction_directives WHERE target_agent = ? AND status = 'active'"
47
+ ).all(agent);
48
+ } else {
49
+ rows = db.prepare(
50
+ "SELECT id, target_agent, directive, reason FROM correction_directives WHERE status = 'active'"
51
+ ).all();
52
+ }
53
+ db.close();
54
+
55
+ if (rows.length === 0) {
56
+ console.log(agent ? `No active directives for ${agent}` : 'No active correction directives');
57
+ } else {
58
+ for (const r of rows) {
59
+ const target = r.target_agent ? ` [${r.target_agent}]` : '';
60
+ console.log(`${r.id}${target}: ${r.directive}`);
61
+ if (r.reason) console.log(` Reason: ${r.reason}`);
62
+ }
63
+ }
64
+ }
65
+
66
+ export async function dbQuerySearch(options) {
67
+ const dbPath = resolveDbPath(options);
68
+ const { query } = options;
69
+
70
+ const db = getDb(dbPath);
71
+ const rows = db.prepare(
72
+ "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
+ ).all(query);
74
+ db.close();
75
+
76
+ if (rows.length === 0) {
77
+ console.log(`No results for: ${query}`);
78
+ } else {
79
+ for (const r of rows) {
80
+ console.log(`[${r.story_id}] ${r.artifact_type} (${r.agent}): ${r.snippet}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ export async function dbQueryList(options) {
86
+ const dbPath = resolveDbPath(options);
87
+ const { story } = options;
88
+
89
+ const db = getDb(dbPath);
90
+ const rows = db.prepare(
91
+ 'SELECT artifact_type, agent, length(content) as size, created_at FROM artifacts WHERE story_id = ? ORDER BY created_at'
92
+ ).all(story);
93
+ db.close();
94
+
95
+ if (rows.length === 0) {
96
+ console.log(`No artifacts for story ${story}`);
97
+ } else {
98
+ console.log(`Artifacts for ${story}:`);
99
+ for (const r of rows) {
100
+ console.log(` ${r.artifact_type} (${r.agent}, ${r.size} chars, ${r.created_at})`);
101
+ }
102
+ }
103
+ }
104
+
105
+ export async function dbQueryStories(options) {
106
+ const dbPath = resolveDbPath(options);
107
+ const db = getDb(dbPath);
108
+ const rows = db.prepare(
109
+ 'SELECT DISTINCT story_id, COUNT(*) as artifact_count FROM artifacts GROUP BY story_id ORDER BY story_id'
110
+ ).all();
111
+ db.close();
112
+
113
+ if (rows.length === 0) {
114
+ console.log('No stories in database');
115
+ } else {
116
+ for (const r of rows) {
117
+ console.log(`${r.story_id}: ${r.artifact_count} artifacts`);
118
+ }
119
+ }
120
+ }
121
+
122
+ export async function dbQueryBugsSince(options) {
123
+ const dbPath = resolveDbPath(options);
124
+ const { since } = options;
125
+
126
+ const db = getDb(dbPath);
127
+ const rows = db.prepare(
128
+ "SELECT story_id, content FROM artifacts WHERE artifact_type = 'bugs' AND created_at > ? ORDER BY created_at"
129
+ ).all(since);
130
+ db.close();
131
+
132
+ if (rows.length === 0) {
133
+ console.log(`No bugs filed since ${since}`);
134
+ } else {
135
+ for (const r of rows) {
136
+ console.log(`--- Bugs from ${r.story_id} ---`);
137
+ console.log(r.content);
138
+ }
139
+ }
140
+ }
@@ -205,6 +205,34 @@ async function runWizard() {
205
205
  config.knowledge.chromadb_host = chromadbHost;
206
206
  }
207
207
 
208
+ // Sprint planning
209
+ const { sprintDuration } = await inquirer.prompt([{
210
+ type: 'input', name: 'sprintDuration',
211
+ message: 'Sprint duration (minutes, for epic/project runs):',
212
+ default: '480',
213
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number',
214
+ }]);
215
+ config.sprint.duration_minutes = parseInt(sprintDuration);
216
+
217
+ const { initialVelocity } = await inquirer.prompt([{
218
+ type: 'input', name: 'initialVelocity',
219
+ message: 'Initial velocity (story points per sprint):',
220
+ default: '60',
221
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number',
222
+ }]);
223
+ config.sprint.initial_velocity_points = parseInt(initialVelocity);
224
+
225
+ const { estimationModel } = await inquirer.prompt([{
226
+ type: 'list', name: 'estimationModel',
227
+ message: 'Estimation model:',
228
+ choices: [
229
+ { name: 'Calibrated (uses historical data from retros)', value: 'calibrated' },
230
+ { name: 'Baseline (estimates from specs only, no history)', value: 'baseline' },
231
+ ],
232
+ default: 'calibrated',
233
+ }]);
234
+ config.sprint.estimation_model = estimationModel;
235
+
208
236
  return config;
209
237
  }
210
238
 
@@ -256,6 +284,14 @@ ${config.knowledge.mode === 'sqlite' ? ` sqlite_db_path: "${config.knowledge.sq
256
284
  curated_files_path: "${config.knowledge.curated_files_path}"
257
285
  correction_directives_path: "${config.knowledge.correction_directives_path}"
258
286
 
287
+ sprint:
288
+ duration_minutes: ${config.sprint?.duration_minutes ?? 480}
289
+ initial_velocity_points: ${config.sprint?.initial_velocity_points ?? 60}
290
+ estimation_model: "${config.sprint?.estimation_model ?? 'calibrated'}"
291
+ auto_plan: ${config.sprint?.auto_plan ?? true}
292
+ fibonacci_scale: [${(config.sprint?.fibonacci_scale ?? [1, 2, 3, 5, 8, 13, 21]).join(', ')}]
293
+ max_groom_batch_size: ${config.sprint?.max_groom_batch_size ?? 10}
294
+
259
295
  orchestration:
260
296
  recommended_context_window: "${config.orchestration?.recommended_context_window || '200k'}"
261
297
  epic_progress_path: "${config.orchestration?.epic_progress_path || './epic-progress.md'}"
@@ -73,6 +73,24 @@ export async function upgrade(options = {}) {
73
73
  console.log('Updated skills in .claude/skills/');
74
74
  }
75
75
 
76
+ // Migrate config: add sprint section if missing
77
+ const configPath = join(projectRoot, '.valent-pipeline', 'pipeline-config.yaml');
78
+ if (fileExists(configPath)) {
79
+ let configContent = readFile(configPath);
80
+ if (!configContent.includes('sprint:')) {
81
+ // Insert sprint section before orchestration (or at end if orchestration not found)
82
+ const sprintBlock = `\nsprint:\n duration_minutes: 480\n initial_velocity_points: 60\n estimation_model: "calibrated"\n auto_plan: true\n fibonacci_scale: [1, 2, 3, 5, 8, 13, 21]\n max_groom_batch_size: 10\n`;
83
+ const insertPoint = configContent.indexOf('\norchestration:');
84
+ if (insertPoint !== -1) {
85
+ configContent = configContent.slice(0, insertPoint) + sprintBlock + configContent.slice(insertPoint);
86
+ } else {
87
+ configContent += sprintBlock;
88
+ }
89
+ writeFileSafe(configPath, configContent);
90
+ console.log('Migrated pipeline-config.yaml: added sprint section');
91
+ }
92
+ }
93
+
76
94
  // Update version file
77
95
  writeFileSafe(versionFile, packageVersion);
78
96
  console.log(`Updated .valent-pipeline/.valent-version to ${packageVersion}`);
@@ -75,10 +75,18 @@ function parseSimpleYaml(content) {
75
75
  if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
76
76
  s = s.slice(1, -1);
77
77
  }
78
+ // Parse numeric array elements
79
+ if (/^\d+(\.\d+)?$/.test(s)) {
80
+ return parseFloat(s);
81
+ }
78
82
  return s;
79
- }).filter(s => s.length > 0);
83
+ }).filter(s => s !== '');
80
84
  }
81
85
 
86
+ // Parse booleans
87
+ if (typeof value === 'string' && value === 'true') value = true;
88
+ if (typeof value === 'string' && value === 'false') value = false;
89
+
82
90
  // Parse numbers
83
91
  if (typeof value === 'string' && /^\d+$/.test(value)) {
84
92
  value = parseInt(value, 10);