valent-pipeline 0.1.0
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 +75 -0
- package/package.json +36 -0
- package/skills/v3-debug-export/SKILL.md +78 -0
- package/skills/v3-help/SKILL.md +82 -0
- package/src/commands/db-init.js +107 -0
- package/src/commands/db-rebuild.js +124 -0
- package/src/commands/init.js +227 -0
- package/src/commands/upgrade.js +129 -0
- package/src/commands/validate.js +92 -0
- package/src/lib/config-schema.js +117 -0
- package/src/lib/file-ops.js +58 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('valent-pipeline')
|
|
16
|
+
.description('v3 multi-agent AI pipeline for software development lifecycle')
|
|
17
|
+
.version(pkg.version);
|
|
18
|
+
|
|
19
|
+
// init command
|
|
20
|
+
program
|
|
21
|
+
.command('init')
|
|
22
|
+
.description('Initialize a new v3 pipeline in the current project')
|
|
23
|
+
.option('--yes', 'Use defaults for all prompts (non-interactive)')
|
|
24
|
+
.option('--force', 'Overwrite existing pipeline configuration')
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
const { init } = await import('../src/commands/init.js');
|
|
27
|
+
await init(options);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// upgrade command
|
|
31
|
+
program
|
|
32
|
+
.command('upgrade')
|
|
33
|
+
.description('Upgrade pipeline infrastructure files to the latest version')
|
|
34
|
+
.option('--dry-run', 'Show what would change without writing')
|
|
35
|
+
.option('--force', 'Force upgrade even on major version bumps')
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
const { upgrade } = await import('../src/commands/upgrade.js');
|
|
38
|
+
await upgrade(options);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// config validate command
|
|
42
|
+
const configCmd = program
|
|
43
|
+
.command('config')
|
|
44
|
+
.description('Pipeline configuration commands');
|
|
45
|
+
|
|
46
|
+
configCmd
|
|
47
|
+
.command('validate')
|
|
48
|
+
.description('Validate pipeline-config.yaml against the schema')
|
|
49
|
+
.action(async () => {
|
|
50
|
+
const { validate } = await import('../src/commands/validate.js');
|
|
51
|
+
await validate();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// db commands
|
|
55
|
+
const dbCmd = program
|
|
56
|
+
.command('db')
|
|
57
|
+
.description('Knowledge database commands');
|
|
58
|
+
|
|
59
|
+
dbCmd
|
|
60
|
+
.command('init')
|
|
61
|
+
.description('Initialize the SQLite knowledge database')
|
|
62
|
+
.action(async () => {
|
|
63
|
+
const { dbInit } = await import('../src/commands/db-init.js');
|
|
64
|
+
await dbInit();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
dbCmd
|
|
68
|
+
.command('rebuild')
|
|
69
|
+
.description('Rebuild the knowledge database from story artifacts')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
const { dbRebuild } = await import('../src/commands/db-rebuild.js');
|
|
72
|
+
await dbRebuild();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "valent-pipeline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "v3 multi-agent AI pipeline for software development lifecycle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"valent-pipeline": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"pipeline/",
|
|
16
|
+
"skills/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node scripts/test-local.js",
|
|
20
|
+
"prepublishOnly": "node scripts/test-local.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^12.0.0",
|
|
24
|
+
"inquirer": "^9.0.0",
|
|
25
|
+
"better-sqlite3": "^11.0.0",
|
|
26
|
+
"sqlite-vec": "^0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"ai",
|
|
30
|
+
"pipeline",
|
|
31
|
+
"multi-agent",
|
|
32
|
+
"sdlc",
|
|
33
|
+
"claude"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: v3-debug-export
|
|
3
|
+
description: 'Export pipeline diagnostic context for debugging in another session. Use when the user says "debug export", "export context", or "v3 debug"'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# v3 Debug Export
|
|
7
|
+
|
|
8
|
+
Generate a concise diagnostic dump of the current pipeline state. The user will paste this into another Claude session for debugging assistance.
|
|
9
|
+
|
|
10
|
+
## Output Format
|
|
11
|
+
|
|
12
|
+
Print the following to the conversation as a single fenced code block (```yaml). Do NOT write to a file.
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
v3_debug_export:
|
|
16
|
+
timestamp: {ISO-8601}
|
|
17
|
+
cwd: {current working directory}
|
|
18
|
+
|
|
19
|
+
install:
|
|
20
|
+
valent_version: {contents of v3/.valent-version, or "MISSING"}
|
|
21
|
+
config_exists: {true/false}
|
|
22
|
+
manifest_exists: {true/false for v3/agents-manifest.yaml}
|
|
23
|
+
skills_installed: {list which v3-* skills exist under .claude/skills/}
|
|
24
|
+
knowledge_mode: {from pipeline-config.yaml, or "NO CONFIG"}
|
|
25
|
+
db_exists: {true/false for v3/pipeline.db}
|
|
26
|
+
project_type: {from pipeline-config.yaml}
|
|
27
|
+
|
|
28
|
+
files:
|
|
29
|
+
prompts: {count of .md files in v3/prompts/}
|
|
30
|
+
step_dirs: {list of directories in v3/steps/}
|
|
31
|
+
step_files: {total count of .md files across v3/steps/}
|
|
32
|
+
task_graphs: {list of .yaml files in v3/task-graphs/}
|
|
33
|
+
templates: {count of .md files in v3/templates/}
|
|
34
|
+
spawn_templates: {count of .md files in v3/spawn-templates/}
|
|
35
|
+
|
|
36
|
+
config_summary:
|
|
37
|
+
tech_stack: {language, backend_framework, frontend_framework from config}
|
|
38
|
+
models: {opus, sonnet, haiku agent lists from config}
|
|
39
|
+
quality: {max_rejection_cycles, retrospective_every_n_stories}
|
|
40
|
+
knowledge: {mode, sqlite_db_path or chromadb_host}
|
|
41
|
+
|
|
42
|
+
backlog:
|
|
43
|
+
exists: {true/false}
|
|
44
|
+
total_items: {count}
|
|
45
|
+
pending: {count}
|
|
46
|
+
shipped: {count}
|
|
47
|
+
|
|
48
|
+
pipeline_state:
|
|
49
|
+
exists: {true/false for pipeline-state.json}
|
|
50
|
+
current_story: {id and status if exists, or "none"}
|
|
51
|
+
stories_since_retro: {count}
|
|
52
|
+
|
|
53
|
+
git:
|
|
54
|
+
branch: {current branch}
|
|
55
|
+
clean: {true/false}
|
|
56
|
+
recent_commits: {last 3 one-line commits}
|
|
57
|
+
|
|
58
|
+
errors: {list any missing expected files, parse errors, or anomalies found during export}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How to Generate
|
|
62
|
+
|
|
63
|
+
1. Read `v3/.valent-version`
|
|
64
|
+
2. Read `v3/pipeline-config.yaml` — parse key fields
|
|
65
|
+
3. Count/list files in v3/prompts/, v3/steps/, v3/task-graphs/, v3/templates/, v3/spawn-templates/
|
|
66
|
+
4. Read `pipeline-backlog.yaml` — count items by status
|
|
67
|
+
5. Read `pipeline-state.json` — extract current story
|
|
68
|
+
6. Check `.claude/skills/` for v3-* directories
|
|
69
|
+
7. Check if `v3/pipeline.db` exists
|
|
70
|
+
8. Run `git branch --show-current` and `git status --short` and `git log --oneline -3`
|
|
71
|
+
9. Collect any missing files or parse errors into the `errors` list
|
|
72
|
+
|
|
73
|
+
## Rules
|
|
74
|
+
|
|
75
|
+
- Output ONLY the YAML block. No preamble, no explanation, no suggestions.
|
|
76
|
+
- Keep it under 80 lines. This is a paste target, not a report.
|
|
77
|
+
- If a file is missing, note it in `errors` — don't fail.
|
|
78
|
+
- If config can't be parsed, set fields to "PARSE_ERROR" and add to errors.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: v3-help
|
|
3
|
+
description: 'Answer questions about the v3 pipeline — how it works, how to use it, agent roles, configuration, troubleshooting. Use when the user says "v3 help", "how does the pipeline work", "explain v3", or asks any question about the pipeline.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# v3 Pipeline Help
|
|
7
|
+
|
|
8
|
+
You are a helpful guide for the v3 multi-agent pipeline. Answer the user's question by reading the pipeline documentation and configuration.
|
|
9
|
+
|
|
10
|
+
## How to Answer
|
|
11
|
+
|
|
12
|
+
1. Read the user's question
|
|
13
|
+
2. Search the relevant docs and config files below for the answer
|
|
14
|
+
3. Give a clear, concise answer with file references so they can dig deeper
|
|
15
|
+
4. If the question is about their specific setup, read their `v3/pipeline-config.yaml`
|
|
16
|
+
|
|
17
|
+
## Documentation Sources
|
|
18
|
+
|
|
19
|
+
Read these as needed to answer questions:
|
|
20
|
+
|
|
21
|
+
| Topic | File |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Pipeline overview, agent roles, execution flow | `v3/docs/pipeline-overview.md` |
|
|
24
|
+
| Agent reference (all agents, models, inputs/outputs) | `v3/docs/agent-reference.md` |
|
|
25
|
+
| Lead lifecycle (kick-off, monitoring, ship/teardown) | `v3/docs/lead-lifecycle.md` |
|
|
26
|
+
| Task graphs and dependency flow | `v3/docs/task-graph.md` |
|
|
27
|
+
| Communication protocol (inbox, message types) | `v3/docs/communication-standard.md` |
|
|
28
|
+
| Knowledge system (curated files, SQLite, ChromaDB) | `v3/docs/knowledge-system.md` |
|
|
29
|
+
| Pipeline state and crash recovery | `v3/docs/pipeline-state-schema.md` |
|
|
30
|
+
| NPX package and installation | `v3/docs/npx-packaging.md` |
|
|
31
|
+
| Template skeleton for handoff docs | `v3/docs/template-skeleton.md` |
|
|
32
|
+
| Agent manifest (definitions, model tiers) | `v3/agents-manifest.yaml` |
|
|
33
|
+
| Project configuration | `v3/pipeline-config.yaml` |
|
|
34
|
+
|
|
35
|
+
## Common Questions — Quick Answers
|
|
36
|
+
|
|
37
|
+
**"How do I run a story?"**
|
|
38
|
+
→ `/v3-run-story STORY-ID` or just `/v3-run-story` to auto-pick the next from the backlog.
|
|
39
|
+
|
|
40
|
+
**"How do I run an epic?"**
|
|
41
|
+
→ `/v3-run-epic EPIC-ID` — runs all stories tagged with that epic sequentially.
|
|
42
|
+
|
|
43
|
+
**"How do I configure the pipeline?"**
|
|
44
|
+
→ `/v3-configure` for interactive wizard, or edit `v3/pipeline-config.yaml` directly.
|
|
45
|
+
|
|
46
|
+
**"How do I add a story to the backlog?"**
|
|
47
|
+
→ Add an entry to `pipeline-backlog.yaml` with id, type: story, status: pending, priority, and epic field.
|
|
48
|
+
|
|
49
|
+
**"What agents run and in what order?"**
|
|
50
|
+
→ REQS → UXA → QA-A → JUDGE-G1 Pass 1 → BEND + FEND (parallel) → CRITIC → QA-B → JUDGE-G1 Pass 2 → JUDGE-G2. Agents are filtered by project type and testing profiles. See `v3/docs/agent-reference.md`.
|
|
51
|
+
|
|
52
|
+
**"How do agents communicate?"**
|
|
53
|
+
→ Peer-to-peer via inbox. Terse messages: `[HANDOFF]`, `[BLOCKER]`, `[KNOWLEDGE-QUERY]`, etc. Lead monitors but doesn't relay. See `v3/docs/communication-standard.md`.
|
|
54
|
+
|
|
55
|
+
**"What does each agent do?"**
|
|
56
|
+
→ Read `v3/docs/agent-reference.md` for the full roster, or check individual prompts in `v3/prompts/`.
|
|
57
|
+
|
|
58
|
+
**"How does the knowledge system work?"**
|
|
59
|
+
→ Knowledge agent answers `[KNOWLEDGE-QUERY]` messages by searching correction directives, curated files, and the SQLite database (if configured). See `v3/docs/knowledge-system.md`.
|
|
60
|
+
|
|
61
|
+
**"How do I change which model an agent uses?"**
|
|
62
|
+
→ Edit the `models` section in `v3/pipeline-config.yaml`. Agents are assigned to opus/sonnet/haiku tiers.
|
|
63
|
+
|
|
64
|
+
**"What happens when an agent gets rejected?"**
|
|
65
|
+
→ Peer-to-peer: JUDGE-G1 rejects specs back to authors, CRITIC rejects code to devs, QA-B routes bugs to devs. Lead only handles G2 rejections and circuit breaker (after max_rejection_cycles). See `v3/docs/lead-lifecycle.md`.
|
|
66
|
+
|
|
67
|
+
**"How do correction directives work?"**
|
|
68
|
+
→ The Retrospective Agent (every N stories) analyzes patterns and writes directives to `knowledge/correction-directives.yaml`. These are loaded at the next story's kick-off and passed to all agents. See `v3/docs/knowledge-system.md`.
|
|
69
|
+
|
|
70
|
+
**"How do I upgrade the pipeline?"**
|
|
71
|
+
→ `npx valent-pipeline upgrade` — replaces infrastructure files, never touches your config or knowledge files.
|
|
72
|
+
|
|
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.
|
|
75
|
+
|
|
76
|
+
## Answer Format
|
|
77
|
+
|
|
78
|
+
Keep answers short and actionable. Reference specific files so the user can read more. Example:
|
|
79
|
+
|
|
80
|
+
> Stories execute in this order: REQS → UXA → QA-A → JUDGE-G1 → BEND/FEND → CRITIC → QA-B → JUDGE-G2. Agents spawn in 4 waves to avoid idle token waste. Your project type (`fullstack-web`) runs all agents. See `v3/docs/agent-reference.md` for details on each agent's role.
|
|
81
|
+
|
|
82
|
+
If you don't know the answer, say so and point them to the most relevant doc file to search manually.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function dbInit() {
|
|
5
|
+
const projectRoot = process.cwd();
|
|
6
|
+
const configPath = join(projectRoot, 'v3', 'pipeline-config.yaml');
|
|
7
|
+
|
|
8
|
+
if (!existsSync(configPath)) {
|
|
9
|
+
console.error('Error: v3/pipeline-config.yaml not found. Run "valent-pipeline init" first.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Read db path from config (simple extraction)
|
|
14
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
15
|
+
const dbPathMatch = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
16
|
+
const dbPath = dbPathMatch ? dbPathMatch[1].trim() : './v3/pipeline.db';
|
|
17
|
+
const fullDbPath = join(projectRoot, dbPath);
|
|
18
|
+
|
|
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
|
+
|
|
71
|
+
// Create FTS5 virtual table
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
|
|
74
|
+
content,
|
|
75
|
+
story_id UNINDEXED,
|
|
76
|
+
agent UNINDEXED,
|
|
77
|
+
artifact_type UNINDEXED,
|
|
78
|
+
content=artifacts,
|
|
79
|
+
content_rowid=rowid
|
|
80
|
+
);
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
// Create FTS triggers for auto-sync
|
|
84
|
+
db.exec(`
|
|
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
|
+
console.log(`Database initialized at ${fullDbPath}`);
|
|
104
|
+
console.log('Tables: artifacts, artifacts_fts, correction_directives');
|
|
105
|
+
|
|
106
|
+
db.close();
|
|
107
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
// Artifact files we index (filename -> artifact_type mapping)
|
|
5
|
+
const ARTIFACT_MAP = {
|
|
6
|
+
'reqs-brief.md': { type: 'reqs-brief', agent: 'REQS' },
|
|
7
|
+
'uxa-spec.md': { type: 'uxa-spec', agent: 'UXA' },
|
|
8
|
+
'qa-test-spec.md': { type: 'qa-test-spec', agent: 'QA-A' },
|
|
9
|
+
'bend-handoff.md': { type: 'bend-handoff', agent: 'BEND' },
|
|
10
|
+
'fend-handoff.md': { type: 'fend-handoff', agent: 'FEND' },
|
|
11
|
+
'critic-review.md': { type: 'critic-review', agent: 'CRITIC' },
|
|
12
|
+
'execution-report.md': { type: 'execution-report', agent: 'QA-B' },
|
|
13
|
+
'bugs.md': { type: 'bugs', agent: 'QA-B' },
|
|
14
|
+
'traceability-matrix.md': { type: 'traceability-matrix', agent: 'QA-B' },
|
|
15
|
+
'judge-g1-review.md': { type: 'judge-g1-review', agent: 'JUDGE-G1' },
|
|
16
|
+
'judge-g2-decision.md': { type: 'judge-g2-decision', agent: 'JUDGE-G2' },
|
|
17
|
+
'story-report.md': { type: 'story-report', agent: 'JUDGE-G2' },
|
|
18
|
+
'pmcp-evidence.md': { type: 'pmcp-evidence', agent: 'PMCP' },
|
|
19
|
+
'visual-validation-checklist.md': { type: 'visual-validation-checklist', agent: 'QA-A' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function dbRebuild() {
|
|
23
|
+
const projectRoot = process.cwd();
|
|
24
|
+
const configPath = join(projectRoot, 'v3', 'pipeline-config.yaml');
|
|
25
|
+
|
|
26
|
+
if (!existsSync(configPath)) {
|
|
27
|
+
console.error('Error: v3/pipeline-config.yaml not found. Run "valent-pipeline init" first.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Read db path from config
|
|
32
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
33
|
+
const dbPathMatch = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
34
|
+
const dbPath = dbPathMatch ? dbPathMatch[1].trim() : './v3/pipeline.db';
|
|
35
|
+
const fullDbPath = join(projectRoot, dbPath);
|
|
36
|
+
|
|
37
|
+
if (!existsSync(fullDbPath)) {
|
|
38
|
+
console.error('Error: Database not found. Run "valent-pipeline db init" first.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let Database;
|
|
43
|
+
try {
|
|
44
|
+
Database = (await import('better-sqlite3')).default;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('Error: better-sqlite3 not installed. Run: npm install better-sqlite3');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const db = new Database(fullDbPath);
|
|
51
|
+
|
|
52
|
+
// Find stories directory
|
|
53
|
+
const storyDirMatch = content.match(/story_directory:\s*"?([^"\n]+)"?/);
|
|
54
|
+
const storyDir = storyDirMatch ? storyDirMatch[1].trim() : './stories';
|
|
55
|
+
const fullStoryDir = join(projectRoot, storyDir);
|
|
56
|
+
|
|
57
|
+
if (!existsSync(fullStoryDir)) {
|
|
58
|
+
console.log('No stories directory found. Nothing to rebuild.');
|
|
59
|
+
db.close();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Scan for story directories
|
|
64
|
+
const storyDirs = readdirSync(fullStoryDir).filter(name => {
|
|
65
|
+
const storyPath = join(fullStoryDir, name);
|
|
66
|
+
return statSync(storyPath).isDirectory();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(`Found ${storyDirs.length} story directories`);
|
|
70
|
+
|
|
71
|
+
// Prepare insert statement
|
|
72
|
+
const insert = db.prepare(`
|
|
73
|
+
INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
|
|
74
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
75
|
+
`);
|
|
76
|
+
|
|
77
|
+
let indexedCount = 0;
|
|
78
|
+
|
|
79
|
+
const insertMany = db.transaction((items) => {
|
|
80
|
+
for (const item of items) {
|
|
81
|
+
insert.run(item.id, item.storyId, item.agent, item.type, item.content, item.metadata);
|
|
82
|
+
indexedCount++;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const allItems = [];
|
|
87
|
+
|
|
88
|
+
for (const storyName of storyDirs) {
|
|
89
|
+
const outputDir = join(fullStoryDir, storyName, 'output');
|
|
90
|
+
if (!existsSync(outputDir)) continue;
|
|
91
|
+
|
|
92
|
+
const storyId = storyName.toUpperCase();
|
|
93
|
+
|
|
94
|
+
for (const [filename, info] of Object.entries(ARTIFACT_MAP)) {
|
|
95
|
+
const filePath = join(outputDir, filename);
|
|
96
|
+
if (!existsSync(filePath)) continue;
|
|
97
|
+
|
|
98
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
99
|
+
// Skip pass-through/skipped files
|
|
100
|
+
if (fileContent.includes('status: skipped')) continue;
|
|
101
|
+
|
|
102
|
+
allItems.push({
|
|
103
|
+
id: `${storyId}:${info.type}`,
|
|
104
|
+
storyId,
|
|
105
|
+
agent: info.agent,
|
|
106
|
+
type: info.type,
|
|
107
|
+
content: fileContent,
|
|
108
|
+
metadata: JSON.stringify({ source: 'rebuild', file: filePath }),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
insertMany(allItems);
|
|
114
|
+
console.log(`Indexed ${indexedCount} artifacts from ${storyDirs.length} stories`);
|
|
115
|
+
|
|
116
|
+
// Rebuild correction directives
|
|
117
|
+
const cdPath = join(projectRoot, 'knowledge', 'correction-directives.yaml');
|
|
118
|
+
if (existsSync(cdPath)) {
|
|
119
|
+
console.log('Note: Correction directives should be re-indexed separately via the Retrospective Agent.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
db.close();
|
|
123
|
+
console.log('Rebuild complete.');
|
|
124
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { defaults } from '../lib/config-schema.js';
|
|
5
|
+
import { copyDir, writeFileSafe, appendToGitignore, fileExists } from '../lib/file-ops.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const PACKAGE_ROOT = resolve(__dirname, '..', '..');
|
|
10
|
+
|
|
11
|
+
export async function init(options = {}) {
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
const configPath = join(projectRoot, 'v3', 'pipeline-config.yaml');
|
|
14
|
+
|
|
15
|
+
// Guard: warn if config already exists
|
|
16
|
+
if (fileExists(configPath) && !options.force) {
|
|
17
|
+
console.error('Error: v3/pipeline-config.yaml already exists.');
|
|
18
|
+
console.error('Use --force to overwrite, or run "valent-pipeline config validate" to check the existing config.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let config;
|
|
23
|
+
if (options.yes) {
|
|
24
|
+
config = { ...defaults };
|
|
25
|
+
console.log('Using default configuration (--yes flag).');
|
|
26
|
+
} else {
|
|
27
|
+
config = await runWizard();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('\nInitializing v3 pipeline...\n');
|
|
31
|
+
|
|
32
|
+
// 1. Copy pipeline infrastructure
|
|
33
|
+
const pipelineSrc = join(PACKAGE_ROOT, 'pipeline');
|
|
34
|
+
const pipelineDest = join(projectRoot, 'v3');
|
|
35
|
+
if (existsSync(pipelineSrc)) {
|
|
36
|
+
copyDir(pipelineSrc, pipelineDest);
|
|
37
|
+
console.log(' Copied pipeline infrastructure to v3/');
|
|
38
|
+
} else {
|
|
39
|
+
console.warn(' Warning: pipeline/ directory not found in package. Skipping infrastructure copy.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Generate pipeline-config.yaml
|
|
43
|
+
const configContent = generateConfigYaml(config);
|
|
44
|
+
writeFileSafe(configPath, configContent);
|
|
45
|
+
console.log(' Generated v3/pipeline-config.yaml');
|
|
46
|
+
|
|
47
|
+
// 3. Initialize backlog
|
|
48
|
+
const backlogPath = join(projectRoot, config.project?.backlog_path || 'pipeline-backlog.yaml');
|
|
49
|
+
if (!fileExists(backlogPath)) {
|
|
50
|
+
writeFileSafe(backlogPath, `# Pipeline Backlog\n# Add stories and bugs here. See v3/docs/pipeline-overview.md for format.\n\nitems: []\n`);
|
|
51
|
+
console.log(' Created pipeline-backlog.yaml');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 4. Install skills
|
|
55
|
+
const skillsSrc = join(PACKAGE_ROOT, 'skills');
|
|
56
|
+
const skillsDest = join(projectRoot, '.claude', 'skills');
|
|
57
|
+
if (existsSync(skillsSrc)) {
|
|
58
|
+
copyDir(skillsSrc, skillsDest);
|
|
59
|
+
console.log(' Installed skills to .claude/skills/');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. Create knowledge directories
|
|
63
|
+
const curatedPath = join(projectRoot, 'knowledge', 'curated');
|
|
64
|
+
if (!existsSync(curatedPath)) {
|
|
65
|
+
writeFileSafe(join(curatedPath, 'README.md'),
|
|
66
|
+
'# Curated Knowledge Files\n\nAdd project-specific knowledge files here. These are indexed by the Knowledge Agent at story startup.\n\nExamples:\n- `api-reference.md` — API endpoint specifications\n- `code-conventions.md` — language and framework conventions\n- `project-context.md` — project identity and architecture\n');
|
|
67
|
+
console.log(' Created knowledge/curated/ with README');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 6. Create correction directives
|
|
71
|
+
const cdPath = join(projectRoot, 'knowledge', 'correction-directives.yaml');
|
|
72
|
+
if (!fileExists(cdPath)) {
|
|
73
|
+
writeFileSafe(cdPath, '# Correction Directives\n# Populated by the Retrospective Agent after batch analysis.\n\ndirectives: []\n');
|
|
74
|
+
console.log(' Created knowledge/correction-directives.yaml');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 7. Initialize SQLite DB if sqlite mode
|
|
78
|
+
if (config.knowledge?.mode === 'sqlite') {
|
|
79
|
+
console.log(' SQLite knowledge mode selected. Run "valent-pipeline db init" to create the database.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 8. Update .gitignore
|
|
83
|
+
appendToGitignore(projectRoot, 'v3/pipeline.db');
|
|
84
|
+
appendToGitignore(projectRoot, 'v3/pipeline.db-*');
|
|
85
|
+
appendToGitignore(projectRoot, 'v3/chromadb-data/');
|
|
86
|
+
console.log(' Updated .gitignore');
|
|
87
|
+
|
|
88
|
+
// 9. Write version file
|
|
89
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
90
|
+
writeFileSafe(join(projectRoot, 'v3', '.valent-version'), pkg.version);
|
|
91
|
+
console.log(` Wrote v3/.valent-version (${pkg.version})`);
|
|
92
|
+
|
|
93
|
+
console.log('\nPipeline initialized. Run /v3-run-story to execute your first story.\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function runWizard() {
|
|
97
|
+
const inquirer = (await import('inquirer')).default;
|
|
98
|
+
const config = JSON.parse(JSON.stringify(defaults));
|
|
99
|
+
|
|
100
|
+
console.log('\n valent-pipeline init\n');
|
|
101
|
+
|
|
102
|
+
// Project type
|
|
103
|
+
const { projectType } = await inquirer.prompt([{
|
|
104
|
+
type: 'list',
|
|
105
|
+
name: 'projectType',
|
|
106
|
+
message: 'Project type:',
|
|
107
|
+
choices: [
|
|
108
|
+
'fullstack-web', 'backend-api', 'frontend-only',
|
|
109
|
+
'data-pipeline', 'mcp-server', 'document-generation', 'library'
|
|
110
|
+
],
|
|
111
|
+
default: 'fullstack-web',
|
|
112
|
+
}]);
|
|
113
|
+
config.project.type = projectType;
|
|
114
|
+
|
|
115
|
+
// Tech stack
|
|
116
|
+
const { language } = await inquirer.prompt([{
|
|
117
|
+
type: 'input', name: 'language', message: 'Language:', default: 'TypeScript',
|
|
118
|
+
}]);
|
|
119
|
+
config.tech_stack.language = language;
|
|
120
|
+
|
|
121
|
+
const { backendFramework } = await inquirer.prompt([{
|
|
122
|
+
type: 'input', name: 'backendFramework', message: 'Backend framework:', default: 'Express',
|
|
123
|
+
}]);
|
|
124
|
+
config.tech_stack.backend_framework = backendFramework;
|
|
125
|
+
|
|
126
|
+
if (!['backend-api', 'data-pipeline', 'mcp-server', 'library'].includes(projectType)) {
|
|
127
|
+
const { frontendFramework } = await inquirer.prompt([{
|
|
128
|
+
type: 'input', name: 'frontendFramework', message: 'Frontend framework:', default: 'React',
|
|
129
|
+
}]);
|
|
130
|
+
config.tech_stack.frontend_framework = frontendFramework;
|
|
131
|
+
} else {
|
|
132
|
+
config.tech_stack.frontend_framework = 'none';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { databaseOrm } = await inquirer.prompt([{
|
|
136
|
+
type: 'input', name: 'databaseOrm', message: 'Database/ORM:', default: 'Prisma',
|
|
137
|
+
}]);
|
|
138
|
+
config.tech_stack.database_orm = databaseOrm;
|
|
139
|
+
|
|
140
|
+
const { testUnit } = await inquirer.prompt([{
|
|
141
|
+
type: 'input', name: 'testUnit', message: 'Unit test framework:', default: 'Vitest',
|
|
142
|
+
}]);
|
|
143
|
+
config.tech_stack.test_framework_unit = testUnit;
|
|
144
|
+
|
|
145
|
+
const { testE2e } = await inquirer.prompt([{
|
|
146
|
+
type: 'input', name: 'testE2e', message: 'E2E test framework:', default: 'Playwright',
|
|
147
|
+
}]);
|
|
148
|
+
config.tech_stack.test_framework_e2e = testE2e;
|
|
149
|
+
|
|
150
|
+
// Knowledge mode
|
|
151
|
+
const { knowledgeMode } = await inquirer.prompt([{
|
|
152
|
+
type: 'list',
|
|
153
|
+
name: 'knowledgeMode',
|
|
154
|
+
message: 'Knowledge store:',
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: 'SQLite (recommended — local, zero infrastructure)', value: 'sqlite' },
|
|
157
|
+
{ name: 'None (curated files only)', value: 'none' },
|
|
158
|
+
{ name: 'ChromaDB (local Docker)', value: 'local-docker' },
|
|
159
|
+
{ name: 'ChromaDB (connect to existing)', value: 'connect-to-existing' },
|
|
160
|
+
],
|
|
161
|
+
default: 'sqlite',
|
|
162
|
+
}]);
|
|
163
|
+
config.knowledge.mode = knowledgeMode;
|
|
164
|
+
|
|
165
|
+
if (knowledgeMode === 'local-docker' || knowledgeMode === 'connect-to-existing') {
|
|
166
|
+
const { chromadbHost } = await inquirer.prompt([{
|
|
167
|
+
type: 'input', name: 'chromadbHost', message: 'ChromaDB host:', default: 'http://localhost:8000',
|
|
168
|
+
}]);
|
|
169
|
+
config.knowledge.chromadb_host = chromadbHost;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return config;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function generateConfigYaml(config) {
|
|
176
|
+
return `# v3 Pipeline Configuration
|
|
177
|
+
# Generated by: npx valent-pipeline init
|
|
178
|
+
# To regenerate with the interactive wizard: npx valent-pipeline init --force
|
|
179
|
+
# To validate this file: npx valent-pipeline config validate
|
|
180
|
+
|
|
181
|
+
project:
|
|
182
|
+
type: "${config.project.type}"
|
|
183
|
+
root: "${config.project.root}"
|
|
184
|
+
story_directory: "${config.project.story_directory}"
|
|
185
|
+
story_output_directory: "${config.project.story_output_directory}"
|
|
186
|
+
backlog_path: "${config.project.backlog_path}"
|
|
187
|
+
|
|
188
|
+
tech_stack:
|
|
189
|
+
language: "${config.tech_stack.language}"
|
|
190
|
+
backend_framework: "${config.tech_stack.backend_framework}"
|
|
191
|
+
frontend_framework: "${config.tech_stack.frontend_framework}"
|
|
192
|
+
database_orm: "${config.tech_stack.database_orm}"
|
|
193
|
+
test_framework_unit: "${config.tech_stack.test_framework_unit}"
|
|
194
|
+
test_framework_e2e: "${config.tech_stack.test_framework_e2e}"
|
|
195
|
+
browser_automation_mcp: "${config.tech_stack.browser_automation_mcp || 'playwright-mcp'}"
|
|
196
|
+
state_management: "${config.tech_stack.state_management || 'React Context'}"
|
|
197
|
+
|
|
198
|
+
models:
|
|
199
|
+
opus: [${(config.models.opus || defaults.models.opus).map(a => `"${a}"`).join(', ')}]
|
|
200
|
+
sonnet: [${(config.models.sonnet || defaults.models.sonnet).map(a => `"${a}"`).join(', ')}]
|
|
201
|
+
haiku: [${(config.models.haiku || defaults.models.haiku).map(a => `"${a}"`).join(', ')}]
|
|
202
|
+
|
|
203
|
+
quality:
|
|
204
|
+
max_rejection_cycles: ${config.quality.max_rejection_cycles}
|
|
205
|
+
retrospective_every_n_stories: ${config.quality.retrospective_every_n_stories}
|
|
206
|
+
stall_threshold_minutes: ${config.quality.stall_threshold_minutes}
|
|
207
|
+
|
|
208
|
+
git:
|
|
209
|
+
target_branch: "${config.git.target_branch}"
|
|
210
|
+
story_branch_prefix: "${config.git.story_branch_prefix}"
|
|
211
|
+
|
|
212
|
+
communication:
|
|
213
|
+
handoff_format: "${config.communication.handoff_format}"
|
|
214
|
+
inbox_message_max_tokens: ${config.communication.inbox_message_max_tokens}
|
|
215
|
+
|
|
216
|
+
knowledge:
|
|
217
|
+
mode: "${config.knowledge.mode}"
|
|
218
|
+
${config.knowledge.mode === 'sqlite' ? ` sqlite_db_path: "${config.knowledge.sqlite_db_path}"` : ''}${config.knowledge.chromadb_host ? ` chromadb_host: "${config.knowledge.chromadb_host}"
|
|
219
|
+
chromadb_collection_prefix: "${config.knowledge.chromadb_collection_prefix || 'v3-'}"` : ''}
|
|
220
|
+
curated_files_path: "${config.knowledge.curated_files_path}"
|
|
221
|
+
correction_directives_path: "${config.knowledge.correction_directives_path}"
|
|
222
|
+
|
|
223
|
+
orchestration:
|
|
224
|
+
recommended_context_window: "${config.orchestration?.recommended_context_window || '200k'}"
|
|
225
|
+
epic_progress_path: "${config.orchestration?.epic_progress_path || './epic-progress.md'}"
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, copyFileSync } from 'fs';
|
|
4
|
+
import { copyDir, writeFileSafe, fileExists, readFile } from '../lib/file-ops.js';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const PACKAGE_ROOT = resolve(__dirname, '..', '..');
|
|
9
|
+
|
|
10
|
+
// Files that are NEVER overwritten during upgrade (project-specific)
|
|
11
|
+
const PROTECTED_FILES = [
|
|
12
|
+
'pipeline-config.yaml',
|
|
13
|
+
'knowledge/curated',
|
|
14
|
+
'knowledge/correction-directives.yaml',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export async function upgrade(options = {}) {
|
|
18
|
+
const projectRoot = process.cwd();
|
|
19
|
+
const versionFile = join(projectRoot, 'v3', '.valent-version');
|
|
20
|
+
|
|
21
|
+
if (!fileExists(versionFile)) {
|
|
22
|
+
console.error('Error: v3/.valent-version not found. Run "valent-pipeline init" first.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const installedVersion = readFile(versionFile).trim();
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
28
|
+
const packageVersion = pkg.version;
|
|
29
|
+
|
|
30
|
+
console.log(`Installed version: ${installedVersion}`);
|
|
31
|
+
console.log(`Package version: ${packageVersion}`);
|
|
32
|
+
|
|
33
|
+
if (installedVersion === packageVersion) {
|
|
34
|
+
console.log('\nAlready up to date.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check for major version bump
|
|
39
|
+
const installedMajor = parseInt(installedVersion.split('.')[0], 10);
|
|
40
|
+
const packageMajor = parseInt(packageVersion.split('.')[0], 10);
|
|
41
|
+
if (packageMajor > installedMajor && !options.force) {
|
|
42
|
+
console.error(`\nMajor version upgrade (${installedMajor} -> ${packageMajor}) requires --force flag.`);
|
|
43
|
+
console.error('Review migration notes at https://github.com/valent/valent-pipeline/releases');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pipelineSrc = join(PACKAGE_ROOT, 'pipeline');
|
|
48
|
+
const pipelineDest = join(projectRoot, 'v3');
|
|
49
|
+
const skillsSrc = join(PACKAGE_ROOT, 'skills');
|
|
50
|
+
const skillsDest = join(projectRoot, '.claude', 'skills');
|
|
51
|
+
|
|
52
|
+
if (options.dryRun) {
|
|
53
|
+
console.log('\n--- Dry run: files that would be updated ---\n');
|
|
54
|
+
if (existsSync(pipelineSrc)) {
|
|
55
|
+
listChanges(pipelineSrc, pipelineDest, 'v3/');
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(skillsSrc)) {
|
|
58
|
+
listChanges(skillsSrc, skillsDest, '.claude/skills/');
|
|
59
|
+
}
|
|
60
|
+
console.log('\n--- End dry run ---');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Copy infrastructure files (skip protected)
|
|
65
|
+
if (existsSync(pipelineSrc)) {
|
|
66
|
+
copyDirFiltered(pipelineSrc, pipelineDest, PROTECTED_FILES);
|
|
67
|
+
console.log('\nUpdated pipeline infrastructure in v3/');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Copy skills
|
|
71
|
+
if (existsSync(skillsSrc)) {
|
|
72
|
+
copyDir(skillsSrc, skillsDest);
|
|
73
|
+
console.log('Updated skills in .claude/skills/');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update version file
|
|
77
|
+
writeFileSafe(versionFile, packageVersion);
|
|
78
|
+
console.log(`Updated v3/.valent-version to ${packageVersion}`);
|
|
79
|
+
|
|
80
|
+
console.log('\nUpgrade complete.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function copyDirFiltered(src, dest, protectedPaths) {
|
|
84
|
+
const entries = readdirSync(src);
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const srcPath = join(src, entry);
|
|
87
|
+
const destPath = join(dest, entry);
|
|
88
|
+
const relativePath = entry;
|
|
89
|
+
|
|
90
|
+
// Skip protected files/dirs
|
|
91
|
+
if (protectedPaths.some(p => relativePath.startsWith(p))) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stat = statSync(srcPath);
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
copyDirFiltered(srcPath, destPath, protectedPaths.map(p => {
|
|
98
|
+
if (p.startsWith(entry + '/')) return p.slice(entry.length + 1);
|
|
99
|
+
return p;
|
|
100
|
+
}).filter(p => p.length > 0));
|
|
101
|
+
} else {
|
|
102
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
103
|
+
copyFileSync(srcPath, destPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function listChanges(src, dest, prefix) {
|
|
109
|
+
const entries = readdirSync(src);
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const srcPath = join(src, entry);
|
|
112
|
+
const destPath = join(dest, entry);
|
|
113
|
+
const stat = statSync(srcPath);
|
|
114
|
+
|
|
115
|
+
if (stat.isDirectory()) {
|
|
116
|
+
listChanges(srcPath, destPath, prefix + entry + '/');
|
|
117
|
+
} else {
|
|
118
|
+
if (!existsSync(destPath)) {
|
|
119
|
+
console.log(` + ${prefix}${entry} (new)`);
|
|
120
|
+
} else {
|
|
121
|
+
const srcContent = readFileSync(srcPath, 'utf-8');
|
|
122
|
+
const destContent = readFileSync(destPath, 'utf-8');
|
|
123
|
+
if (srcContent !== destContent) {
|
|
124
|
+
console.log(` ~ ${prefix}${entry} (modified)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { validateConfig } from '../lib/config-schema.js';
|
|
4
|
+
|
|
5
|
+
export async function validate() {
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
const configPath = join(projectRoot, 'v3', 'pipeline-config.yaml');
|
|
8
|
+
|
|
9
|
+
if (!existsSync(configPath)) {
|
|
10
|
+
console.error('Error: v3/pipeline-config.yaml not found.');
|
|
11
|
+
console.error('Run "valent-pipeline init" to create one.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Simple YAML parser (avoids adding yaml dependency)
|
|
16
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
config = parseSimpleYaml(content);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(`Error parsing v3/pipeline-config.yaml: ${err.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = validateConfig(config);
|
|
26
|
+
|
|
27
|
+
if (result.valid) {
|
|
28
|
+
console.log('v3/pipeline-config.yaml is valid.');
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} else {
|
|
31
|
+
console.error('Validation errors:');
|
|
32
|
+
for (const error of result.errors) {
|
|
33
|
+
console.error(` - ${error}`);
|
|
34
|
+
}
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Minimal YAML parser for pipeline-config.yaml.
|
|
41
|
+
* Handles the specific structure we generate (flat sections with string/number/array values).
|
|
42
|
+
* For complex YAML, users should install yaml package.
|
|
43
|
+
*/
|
|
44
|
+
function parseSimpleYaml(content) {
|
|
45
|
+
const result = {};
|
|
46
|
+
let currentSection = null;
|
|
47
|
+
|
|
48
|
+
for (const line of content.split('\n')) {
|
|
49
|
+
// Skip comments and empty lines
|
|
50
|
+
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
51
|
+
|
|
52
|
+
// Top-level section (no indentation)
|
|
53
|
+
const sectionMatch = line.match(/^(\w+):\s*$/);
|
|
54
|
+
if (sectionMatch) {
|
|
55
|
+
currentSection = sectionMatch[1];
|
|
56
|
+
result[currentSection] = {};
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Key-value pair (indented)
|
|
61
|
+
const kvMatch = line.match(/^\s+(\w+):\s+(.+)$/);
|
|
62
|
+
if (kvMatch && currentSection) {
|
|
63
|
+
const key = kvMatch[1];
|
|
64
|
+
let value = kvMatch[2].trim();
|
|
65
|
+
|
|
66
|
+
// Remove quotes
|
|
67
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
68
|
+
value = value.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse arrays [a, b, c]
|
|
72
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
73
|
+
value = value.slice(1, -1).split(',').map(s => {
|
|
74
|
+
s = s.trim();
|
|
75
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
76
|
+
s = s.slice(1, -1);
|
|
77
|
+
}
|
|
78
|
+
return s;
|
|
79
|
+
}).filter(s => s.length > 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse numbers
|
|
83
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
84
|
+
value = parseInt(value, 10);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
result[currentSection][key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a pipeline-config.yaml object against the expected schema.
|
|
3
|
+
* Returns { valid: boolean, errors: string[] }
|
|
4
|
+
*/
|
|
5
|
+
export function validateConfig(config) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
|
|
8
|
+
// Required top-level sections
|
|
9
|
+
const requiredSections = ['project', 'tech_stack', 'models', 'quality', 'git', 'communication', 'knowledge'];
|
|
10
|
+
for (const section of requiredSections) {
|
|
11
|
+
if (!config[section]) {
|
|
12
|
+
errors.push(`Missing required section: ${section}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (errors.length > 0) {
|
|
17
|
+
return { valid: false, errors };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Project section
|
|
21
|
+
const validProjectTypes = ['fullstack-web', 'backend-api', 'frontend-only', 'data-pipeline', 'mcp-server', 'document-generation', 'library'];
|
|
22
|
+
if (config.project?.type && !validProjectTypes.includes(config.project.type)) {
|
|
23
|
+
errors.push(`Invalid project.type: "${config.project.type}". Must be one of: ${validProjectTypes.join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
if (!config.project?.root) {
|
|
26
|
+
errors.push('Missing required field: project.root');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Models section
|
|
30
|
+
const modelTiers = ['opus', 'sonnet', 'haiku'];
|
|
31
|
+
for (const tier of modelTiers) {
|
|
32
|
+
if (!config.models?.[tier]) {
|
|
33
|
+
errors.push(`Missing model tier: models.${tier}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Quality section
|
|
38
|
+
const qualityFields = ['max_rejection_cycles', 'retrospective_every_n_stories', 'stall_threshold_minutes'];
|
|
39
|
+
for (const field of qualityFields) {
|
|
40
|
+
if (config.quality?.[field] !== undefined && typeof config.quality[field] !== 'number') {
|
|
41
|
+
errors.push(`quality.${field} must be a number, got: ${typeof config.quality[field]}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Communication section
|
|
46
|
+
const validFormats = ['distilled', 'verbose'];
|
|
47
|
+
if (config.communication?.handoff_format && !validFormats.includes(config.communication.handoff_format)) {
|
|
48
|
+
errors.push(`Invalid communication.handoff_format: "${config.communication.handoff_format}". Must be: ${validFormats.join(' or ')}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Knowledge section
|
|
52
|
+
const validModes = ['none', 'sqlite', 'local-docker', 'connect-to-existing'];
|
|
53
|
+
if (config.knowledge?.mode && !validModes.includes(config.knowledge.mode)) {
|
|
54
|
+
errors.push(`Invalid knowledge.mode: "${config.knowledge.mode}". Must be one of: ${validModes.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (config.knowledge?.mode === 'sqlite' && !config.knowledge?.sqlite_db_path) {
|
|
58
|
+
errors.push('knowledge.sqlite_db_path is required when knowledge.mode is "sqlite"');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ((config.knowledge?.mode === 'local-docker' || config.knowledge?.mode === 'connect-to-existing') && !config.knowledge?.chromadb_host) {
|
|
62
|
+
errors.push('knowledge.chromadb_host is required when knowledge.mode is "local-docker" or "connect-to-existing"');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { valid: errors.length === 0, errors };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default configuration values.
|
|
70
|
+
*/
|
|
71
|
+
export const defaults = {
|
|
72
|
+
project: {
|
|
73
|
+
type: 'fullstack-web',
|
|
74
|
+
root: '.',
|
|
75
|
+
story_directory: './stories',
|
|
76
|
+
story_output_directory: './stories/{story_id}/output',
|
|
77
|
+
backlog_path: './pipeline-backlog.yaml',
|
|
78
|
+
},
|
|
79
|
+
tech_stack: {
|
|
80
|
+
language: 'TypeScript',
|
|
81
|
+
backend_framework: 'Express',
|
|
82
|
+
frontend_framework: 'React',
|
|
83
|
+
database_orm: 'Prisma',
|
|
84
|
+
test_framework_unit: 'Vitest',
|
|
85
|
+
test_framework_e2e: 'Playwright',
|
|
86
|
+
browser_automation_mcp: 'playwright-mcp',
|
|
87
|
+
state_management: 'React Context',
|
|
88
|
+
},
|
|
89
|
+
models: {
|
|
90
|
+
opus: ['BEND', 'FEND', 'CRITIC'],
|
|
91
|
+
sonnet: ['REQS', 'UXA', 'QA-A', 'QA-B', 'JUDGE-G1', 'JUDGE-G2', 'PMCP', 'Retrospective'],
|
|
92
|
+
haiku: ['Knowledge', 'Embed', 'Help'],
|
|
93
|
+
},
|
|
94
|
+
quality: {
|
|
95
|
+
max_rejection_cycles: 5,
|
|
96
|
+
retrospective_every_n_stories: 5,
|
|
97
|
+
stall_threshold_minutes: 15,
|
|
98
|
+
},
|
|
99
|
+
git: {
|
|
100
|
+
target_branch: '',
|
|
101
|
+
story_branch_prefix: 'story/',
|
|
102
|
+
},
|
|
103
|
+
communication: {
|
|
104
|
+
handoff_format: 'distilled',
|
|
105
|
+
inbox_message_max_tokens: 500,
|
|
106
|
+
},
|
|
107
|
+
knowledge: {
|
|
108
|
+
mode: 'sqlite',
|
|
109
|
+
sqlite_db_path: './v3/pipeline.db',
|
|
110
|
+
curated_files_path: './knowledge/curated',
|
|
111
|
+
correction_directives_path: './knowledge/correction-directives.yaml',
|
|
112
|
+
},
|
|
113
|
+
orchestration: {
|
|
114
|
+
recommended_context_window: '200k',
|
|
115
|
+
epic_progress_path: './epic-progress.md',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, readdirSync, statSync, appendFileSync } from 'fs';
|
|
2
|
+
import { join, dirname, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively copy a directory's contents to a destination.
|
|
6
|
+
* Creates directories as needed.
|
|
7
|
+
*/
|
|
8
|
+
export function copyDir(src, dest) {
|
|
9
|
+
mkdirSync(dest, { recursive: true });
|
|
10
|
+
const entries = readdirSync(src);
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const srcPath = join(src, entry);
|
|
13
|
+
const destPath = join(dest, entry);
|
|
14
|
+
const stat = statSync(srcPath);
|
|
15
|
+
if (stat.isDirectory()) {
|
|
16
|
+
copyDir(srcPath, destPath);
|
|
17
|
+
} else {
|
|
18
|
+
copyFileSync(srcPath, destPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Write a file, creating parent directories as needed.
|
|
25
|
+
*/
|
|
26
|
+
export function writeFileSafe(filePath, content) {
|
|
27
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
28
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Append a line to .gitignore if not already present.
|
|
33
|
+
*/
|
|
34
|
+
export function appendToGitignore(projectRoot, line) {
|
|
35
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
36
|
+
let content = '';
|
|
37
|
+
if (existsSync(gitignorePath)) {
|
|
38
|
+
content = readFileSync(gitignorePath, 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
if (!content.includes(line)) {
|
|
41
|
+
const separator = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
42
|
+
appendFileSync(gitignorePath, `${separator}${line}\n`, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a file exists.
|
|
48
|
+
*/
|
|
49
|
+
export function fileExists(filePath) {
|
|
50
|
+
return existsSync(filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read a file as UTF-8 string.
|
|
55
|
+
*/
|
|
56
|
+
export function readFile(filePath) {
|
|
57
|
+
return readFileSync(filePath, 'utf-8');
|
|
58
|
+
}
|