viepilot 2.41.0 → 2.45.3
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/CHANGELOG.md +104 -0
- package/bin/viepilot.cjs +32 -0
- package/bin/vp-tools.cjs +95 -0
- package/docs/brainstorm/session-2026-04-24.md +131 -0
- package/docs/brainstorm/session-2026-04-25.md +109 -0
- package/lib/domain-packs/ai-product.json +33 -0
- package/lib/domain-packs/data-science.json +33 -0
- package/lib/domain-packs/devops.json +33 -0
- package/lib/domain-packs/mobile.json +33 -0
- package/lib/domain-packs/web-saas.json +33 -0
- package/lib/viepilot-calibrate.cjs +279 -0
- package/lib/viepilot-install.cjs +5 -3
- package/lib/viepilot-persona.cjs +446 -0
- package/package.json +1 -1
- package/skills/vp-audit/SKILL.md +10 -0
- package/skills/vp-auto/SKILL.md +10 -0
- package/skills/vp-brainstorm/SKILL.md +16 -1
- package/skills/vp-crystallize/SKILL.md +10 -0
- package/skills/vp-debug/SKILL.md +10 -0
- package/skills/vp-design/SKILL.md +219 -0
- package/skills/vp-docs/SKILL.md +10 -0
- package/skills/vp-evolve/SKILL.md +10 -0
- package/skills/vp-info/SKILL.md +10 -0
- package/skills/vp-pause/SKILL.md +10 -0
- package/skills/vp-persona/SKILL.md +207 -0
- package/skills/vp-proposal/SKILL.md +10 -0
- package/skills/vp-request/SKILL.md +10 -0
- package/skills/vp-resume/SKILL.md +10 -0
- package/skills/vp-rollback/SKILL.md +34 -1
- package/skills/vp-skills/SKILL.md +10 -0
- package/skills/vp-status/SKILL.md +10 -0
- package/skills/vp-task/SKILL.md +10 -0
- package/skills/vp-ui-components/SKILL.md +10 -0
- package/skills/vp-update/SKILL.md +10 -0
- package/workflows/autonomous.md +59 -0
- package/workflows/brainstorm.md +148 -1
- package/workflows/crystallize.md +111 -0
- package/workflows/design.md +601 -0
- package/workflows/evolve.md +9 -0
- package/workflows/rollback.md +79 -10
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const VIEPILOT_DIR = path.join(os.homedir(), '.viepilot');
|
|
9
|
+
const PERSONAS_DIR = path.join(VIEPILOT_DIR, 'personas');
|
|
10
|
+
const ACTIVE_PERSONA_FILE = path.join(VIEPILOT_DIR, 'persona.json');
|
|
11
|
+
const CONTEXT_MAP_FILE = path.join(VIEPILOT_DIR, 'context-map.json');
|
|
12
|
+
const PENDING_REVIEW_FILE = path.join(VIEPILOT_DIR, 'pending-review.md');
|
|
13
|
+
|
|
14
|
+
// Known stack names to match from dependency files
|
|
15
|
+
const STACK_PATTERNS = {
|
|
16
|
+
js: ['nextjs', 'next', 'nestjs', 'express', 'fastify', 'koa', 'hapi',
|
|
17
|
+
'react', 'vue', 'angular', 'svelte', 'remix', 'nuxt',
|
|
18
|
+
'prisma', 'typeorm', 'sequelize', 'mongoose',
|
|
19
|
+
'postgresql', 'pg', 'mysql', 'mysql2', 'redis', 'mongodb',
|
|
20
|
+
'stripe', 'tailwindcss', 'tailwind', 'graphql', 'trpc',
|
|
21
|
+
'socket.io', 'bull', 'kafka', 'rabbitmq'],
|
|
22
|
+
python: ['fastapi', 'django', 'flask', 'starlette', 'tornado',
|
|
23
|
+
'langchain', 'openai', 'anthropic', 'llama-index',
|
|
24
|
+
'pytorch', 'torch', 'tensorflow', 'keras',
|
|
25
|
+
'pandas', 'numpy', 'scikit-learn', 'sklearn',
|
|
26
|
+
'sqlalchemy', 'alembic', 'celery', 'pydantic'],
|
|
27
|
+
cmake: ['freertos', 'esp-idf', 'zephyr', 'stm32', 'arduino',
|
|
28
|
+
'mbedtls', 'lwip', 'fatfs', 'cmsis'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Domain signal detection rules
|
|
32
|
+
const DOMAIN_SIGNALS = [
|
|
33
|
+
{
|
|
34
|
+
domain: 'web-saas',
|
|
35
|
+
checks: [
|
|
36
|
+
(dir) => fileExists(dir, 'package.json') ? 0.4 : 0,
|
|
37
|
+
(dir) => dirExists(dir, 'prisma') ? 0.4 : 0,
|
|
38
|
+
(dir) => fileExists(dir, 'next.config.js') || fileExists(dir, 'next.config.ts') ? 0.2 : 0,
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
domain: 'embedded',
|
|
43
|
+
checks: [
|
|
44
|
+
(dir) => fileExists(dir, 'CMakeLists.txt') ? 0.4 : 0,
|
|
45
|
+
(dir) => fileExists(dir, 'sdkconfig') || fileExists(dir, 'sdkconfig.defaults') ? 0.4 : 0,
|
|
46
|
+
(dir) => fileExists(dir, 'platformio.ini') ? 0.3 : 0,
|
|
47
|
+
(dir) => fileExists(dir, '.pio') || dirExists(dir, '.pio') ? 0.2 : 0,
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
domain: 'data-science',
|
|
52
|
+
checks: [
|
|
53
|
+
(dir) => fileExists(dir, 'requirements.txt') ? 0.3 : 0,
|
|
54
|
+
(dir) => dirExists(dir, 'notebooks') || globExists(dir, '*.ipynb') ? 0.4 : 0,
|
|
55
|
+
(dir) => fileExists(dir, 'pyproject.toml') && hasPythonMLDeps(dir) ? 0.3 : 0,
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
domain: 'mobile',
|
|
60
|
+
checks: [
|
|
61
|
+
(dir) => fileExists(dir, 'pubspec.yaml') ? 0.5 : 0,
|
|
62
|
+
(dir) => globExists(dir, '*.xcodeproj') || globExists(dir, '*.xcworkspace') ? 0.5 : 0,
|
|
63
|
+
(dir) => fileExists(dir, 'android/app/build.gradle') ? 0.4 : 0,
|
|
64
|
+
(dir) => fileExists(dir, 'metro.config.js') ? 0.3 : 0,
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
domain: 'devops',
|
|
69
|
+
checks: [
|
|
70
|
+
(dir) => fileExists(dir, 'Dockerfile') || fileExists(dir, 'docker-compose.yml') ? 0.4 : 0,
|
|
71
|
+
(dir) => dirExists(dir, 'terraform') || globExists(dir, '*.tf') ? 0.4 : 0,
|
|
72
|
+
(dir) => dirExists(dir, '.github/workflows') || dirExists(dir, '.gitlab-ci.yml') ? 0.3 : 0,
|
|
73
|
+
(dir) => fileExists(dir, 'ansible.cfg') || dirExists(dir, 'playbooks') ? 0.2 : 0,
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
domain: 'ai-product',
|
|
78
|
+
checks: [
|
|
79
|
+
(dir) => fileExists(dir, 'pyproject.toml') && hasAIDeps(dir) ? 0.4 : 0,
|
|
80
|
+
(dir) => fileExists(dir, 'requirements.txt') && hasAIDeps(dir) ? 0.3 : 0,
|
|
81
|
+
(dir) => fileExists(dir, 'package.json') && hasJSAIDeps(dir) ? 0.35 : 0,
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Helpers
|
|
87
|
+
|
|
88
|
+
function fileExists(dir, name) {
|
|
89
|
+
try { return fs.statSync(path.join(dir, name)).isFile(); } catch { return false; }
|
|
90
|
+
}
|
|
91
|
+
function dirExists(dir, name) {
|
|
92
|
+
try { return fs.statSync(path.join(dir, name)).isDirectory(); } catch { return false; }
|
|
93
|
+
}
|
|
94
|
+
function globExists(dir, pattern) {
|
|
95
|
+
try {
|
|
96
|
+
const prefix = pattern.replace('*', '');
|
|
97
|
+
return fs.readdirSync(dir).some(f => f.endsWith(prefix.slice(1)) || f.includes(prefix.replace('.', '')));
|
|
98
|
+
} catch { return false; }
|
|
99
|
+
}
|
|
100
|
+
function readJsonSafe(filePath) {
|
|
101
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
102
|
+
}
|
|
103
|
+
function writeJsonSafe(filePath, data) {
|
|
104
|
+
try {
|
|
105
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
106
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
107
|
+
} catch { /* silent */ }
|
|
108
|
+
}
|
|
109
|
+
function readFileSafe(filePath) {
|
|
110
|
+
try { return fs.readFileSync(filePath, 'utf8'); } catch { return ''; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasPythonMLDeps(dir) {
|
|
114
|
+
const content = readFileSafe(path.join(dir, 'requirements.txt')) +
|
|
115
|
+
readFileSafe(path.join(dir, 'pyproject.toml'));
|
|
116
|
+
return /torch|tensorflow|sklearn|scikit|pandas|numpy|xgboost|lightgbm|catboost/.test(content);
|
|
117
|
+
}
|
|
118
|
+
function hasAIDeps(dir) {
|
|
119
|
+
const content = readFileSafe(path.join(dir, 'requirements.txt')) +
|
|
120
|
+
readFileSafe(path.join(dir, 'pyproject.toml'));
|
|
121
|
+
return /langchain|openai|anthropic|llama.index|transformers|huggingface|litellm/.test(content);
|
|
122
|
+
}
|
|
123
|
+
function hasJSAIDeps(dir) {
|
|
124
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
125
|
+
if (!pkg) return false;
|
|
126
|
+
const deps = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) });
|
|
127
|
+
return deps.some(d => /openai|anthropic|langchain|ai-sdk|vercel\/ai|llm/.test(d));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function inferStacks(dir) {
|
|
131
|
+
const stacks = new Set();
|
|
132
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
133
|
+
if (pkg) {
|
|
134
|
+
const deps = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) });
|
|
135
|
+
for (const dep of deps) {
|
|
136
|
+
const normalized = dep.replace(/^@[^/]+\//, '').toLowerCase();
|
|
137
|
+
if (STACK_PATTERNS.js.some(s => normalized.includes(s) || s.includes(normalized))) {
|
|
138
|
+
const match = STACK_PATTERNS.js.find(s => normalized.includes(s));
|
|
139
|
+
if (match) stacks.add(match);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Detect next.js specifically
|
|
143
|
+
if (deps.includes('next')) stacks.add('nextjs');
|
|
144
|
+
if (deps.some(d => d.includes('nest'))) stacks.add('nestjs');
|
|
145
|
+
if (deps.includes('prisma') || deps.includes('@prisma/client')) stacks.add('prisma');
|
|
146
|
+
if (deps.some(d => d === 'pg' || d === 'postgres')) stacks.add('postgresql');
|
|
147
|
+
if (deps.some(d => d.includes('stripe'))) stacks.add('stripe');
|
|
148
|
+
if (deps.includes('tailwindcss')) stacks.add('tailwind');
|
|
149
|
+
if (deps.includes('react')) stacks.add('react');
|
|
150
|
+
}
|
|
151
|
+
const reqs = readFileSafe(path.join(dir, 'requirements.txt'));
|
|
152
|
+
if (reqs) {
|
|
153
|
+
for (const pattern of STACK_PATTERNS.python) {
|
|
154
|
+
if (reqs.toLowerCase().includes(pattern)) stacks.add(pattern);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const cmake = readFileSafe(path.join(dir, 'CMakeLists.txt'));
|
|
158
|
+
if (cmake) {
|
|
159
|
+
for (const pattern of STACK_PATTERNS.cmake) {
|
|
160
|
+
if (cmake.toLowerCase().includes(pattern)) stacks.add(pattern);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Array.from(stacks).slice(0, 8);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function inferTeamSize(dir) {
|
|
167
|
+
try {
|
|
168
|
+
const out = execSync('git shortlog -sn --no-merges', {
|
|
169
|
+
cwd: dir, stdio: 'pipe', timeout: 3000, encoding: 'utf8',
|
|
170
|
+
});
|
|
171
|
+
const count = out.trim().split('\n').filter(Boolean).length;
|
|
172
|
+
if (count <= 1) return 'solo';
|
|
173
|
+
if (count <= 5) return 'small';
|
|
174
|
+
return 'team';
|
|
175
|
+
} catch { return 'unknown'; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function inferRole(dir) {
|
|
179
|
+
try {
|
|
180
|
+
const out = execSync('git log --name-only --pretty=format: -n 200', {
|
|
181
|
+
cwd: dir, stdio: 'pipe', timeout: 3000, encoding: 'utf8',
|
|
182
|
+
});
|
|
183
|
+
const files = out.split('\n').filter(Boolean);
|
|
184
|
+
let frontend = 0, backend = 0, embedded = 0;
|
|
185
|
+
for (const f of files) {
|
|
186
|
+
if (/\.(tsx|jsx|css|scss|html|svelte|vue)$/.test(f)) frontend++;
|
|
187
|
+
else if (/\.(ts|js|py|go|java|rb|php|rs)$/.test(f)) backend++;
|
|
188
|
+
else if (/\.(c|cpp|h|ino|s|asm)$/.test(f)) embedded++;
|
|
189
|
+
}
|
|
190
|
+
if (embedded > backend && embedded > frontend) return 'embedded';
|
|
191
|
+
if (frontend > 0 && backend > 0) return 'full-stack';
|
|
192
|
+
if (frontend > backend) return 'frontend';
|
|
193
|
+
return 'backend';
|
|
194
|
+
} catch { return 'full-stack'; }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function domainPhaseTemplate(domain) {
|
|
198
|
+
const templates = {
|
|
199
|
+
'web-saas': 'lean-startup',
|
|
200
|
+
'embedded': 'firmware',
|
|
201
|
+
'data-science': 'ml-pipeline',
|
|
202
|
+
'mobile': 'mobile-launch',
|
|
203
|
+
'devops': 'infra-ops',
|
|
204
|
+
'ai-product': 'ai-product',
|
|
205
|
+
'generic': 'generic',
|
|
206
|
+
};
|
|
207
|
+
return templates[domain] || 'generic';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function domainTopicPriority(domain) {
|
|
211
|
+
const priorities = {
|
|
212
|
+
'web-saas': ['auth', 'user-data', 'api', 'billing', 'admin', 'onboarding'],
|
|
213
|
+
'embedded': ['hw-topology', 'drivers', 'rtos', 'protocols', 'power-budget'],
|
|
214
|
+
'data-science': ['dataset', 'model-training', 'evaluation', 'serving', 'monitoring'],
|
|
215
|
+
'mobile': ['auth', 'core-features', 'offline-sync', 'push-notifications', 'app-store'],
|
|
216
|
+
'devops': ['infra', 'ci-cd', 'observability', 'slo', 'incident-mgmt'],
|
|
217
|
+
'ai-product': ['llm-integration', 'rag-pipeline', 'prompt-mgmt', 'eval', 'ux'],
|
|
218
|
+
'generic': ['core-features', 'auth', 'api', 'admin'],
|
|
219
|
+
};
|
|
220
|
+
return priorities[domain] || priorities['generic'];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Public API
|
|
224
|
+
|
|
225
|
+
async function inferPersona(projectDir) {
|
|
226
|
+
const dir = path.resolve(projectDir);
|
|
227
|
+
try {
|
|
228
|
+
// Score each domain
|
|
229
|
+
const scores = {};
|
|
230
|
+
for (const { domain, checks } of DOMAIN_SIGNALS) {
|
|
231
|
+
scores[domain] = checks.reduce((sum, check) => {
|
|
232
|
+
try { return sum + check(dir); } catch { return sum; }
|
|
233
|
+
}, 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find top domains
|
|
237
|
+
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
238
|
+
const [topDomain, topScore] = sorted[0];
|
|
239
|
+
const [secondDomain, secondScore] = sorted[1] || ['generic', 0];
|
|
240
|
+
|
|
241
|
+
// Check for multi-domain merge condition
|
|
242
|
+
if (topScore >= 0.35 && secondScore >= 0.35) {
|
|
243
|
+
const personaA = await _buildPersona(dir, topDomain, topScore);
|
|
244
|
+
const personaB = await _buildPersona(dir, secondDomain, secondScore);
|
|
245
|
+
return mergePersonas(personaA, personaB);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const confidence = Math.min(topScore, 1.0);
|
|
249
|
+
return await _buildPersona(dir, confidence >= 0.1 ? topDomain : 'generic', confidence);
|
|
250
|
+
} catch {
|
|
251
|
+
return { name: 'auto-generic', source: 'auto', domain: 'generic', confidence: 0, inferred_at: new Date().toISOString() };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function _buildPersona(dir, domain, confidence) {
|
|
256
|
+
const stacks = inferStacks(dir);
|
|
257
|
+
const teamSize = inferTeamSize(dir);
|
|
258
|
+
const role = inferRole(dir);
|
|
259
|
+
return {
|
|
260
|
+
name: `auto-${domain}`,
|
|
261
|
+
source: 'auto',
|
|
262
|
+
domain,
|
|
263
|
+
role,
|
|
264
|
+
stacks,
|
|
265
|
+
team_size: teamSize,
|
|
266
|
+
output_style: 'lean',
|
|
267
|
+
phase_template: domainPhaseTemplate(domain),
|
|
268
|
+
brainstorm: {
|
|
269
|
+
topic_priority: domainTopicPriority(domain),
|
|
270
|
+
topic_skip: [],
|
|
271
|
+
},
|
|
272
|
+
confidence: Math.round(Math.min(confidence, 1.0) * 100) / 100,
|
|
273
|
+
inferred_at: new Date().toISOString(),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function mergePersonas(a, b) {
|
|
278
|
+
const domainA = Array.isArray(a.domain) ? a.domain : [a.domain];
|
|
279
|
+
const domainB = Array.isArray(b.domain) ? b.domain : [b.domain];
|
|
280
|
+
const domains = [...new Set([...domainA, ...domainB])];
|
|
281
|
+
const domainSlug = domains.join('-');
|
|
282
|
+
return {
|
|
283
|
+
name: `merge-${domainSlug}`,
|
|
284
|
+
source: 'merge',
|
|
285
|
+
domain: domains,
|
|
286
|
+
role: a.role || b.role || 'full-stack',
|
|
287
|
+
stacks: [...new Set([...(a.stacks || []), ...(b.stacks || [])])],
|
|
288
|
+
team_size: a.team_size || b.team_size || 'unknown',
|
|
289
|
+
output_style: a.output_style || 'lean',
|
|
290
|
+
phase_template: `hybrid-${domainSlug}`,
|
|
291
|
+
brainstorm: {
|
|
292
|
+
topic_priority: [...new Set([...(a.brainstorm?.topic_priority || []), ...(b.brainstorm?.topic_priority || [])])],
|
|
293
|
+
topic_skip: [...new Set([...(a.brainstorm?.topic_skip || []), ...(b.brainstorm?.topic_skip || [])])],
|
|
294
|
+
},
|
|
295
|
+
confidence: Math.min(a.confidence || 0, b.confidence || 0),
|
|
296
|
+
inferred_at: new Date().toISOString(),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resolvePersona(projectDir, opts = {}) {
|
|
301
|
+
const dir = path.resolve(projectDir || process.cwd());
|
|
302
|
+
try {
|
|
303
|
+
// Layer 1: project-level override
|
|
304
|
+
const override = readJsonSafe(path.join(dir, '.viepilot', 'persona-override.json'));
|
|
305
|
+
if (override) return override;
|
|
306
|
+
} catch { /* silent */ }
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Layer 2: context-map lookup (longest matching prefix)
|
|
310
|
+
const contextMap = readJsonSafe(CONTEXT_MAP_FILE) || {};
|
|
311
|
+
let bestMatch = null, bestLen = 0;
|
|
312
|
+
for (const [mappedDir, personaName] of Object.entries(contextMap)) {
|
|
313
|
+
if (dir.startsWith(mappedDir) && mappedDir.length > bestLen) {
|
|
314
|
+
bestMatch = personaName;
|
|
315
|
+
bestLen = mappedDir.length;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (bestMatch) {
|
|
319
|
+
const persona = readJsonSafe(path.join(PERSONAS_DIR, `${bestMatch}.json`));
|
|
320
|
+
if (persona) return persona;
|
|
321
|
+
}
|
|
322
|
+
} catch { /* silent */ }
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// Layer 3: global active persona
|
|
326
|
+
const active = readJsonSafe(ACTIVE_PERSONA_FILE);
|
|
327
|
+
if (active && active.name) {
|
|
328
|
+
const persona = readJsonSafe(path.join(PERSONAS_DIR, `${active.name}.json`));
|
|
329
|
+
if (persona) return persona;
|
|
330
|
+
}
|
|
331
|
+
} catch { /* silent */ }
|
|
332
|
+
|
|
333
|
+
// Fallback: run infer synchronously (blocking variant for sync callers)
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function generatePersonaContext(persona) {
|
|
338
|
+
if (!persona) return '';
|
|
339
|
+
const domain = Array.isArray(persona.domain) ? persona.domain.join(' + ') : (persona.domain || 'unknown');
|
|
340
|
+
const stacks = (persona.stacks || []).join(' / ') || 'not specified';
|
|
341
|
+
return [
|
|
342
|
+
'## User Persona',
|
|
343
|
+
`- Role: ${persona.role || 'developer'}`,
|
|
344
|
+
`- Domain: ${domain}`,
|
|
345
|
+
`- Preferred stacks: ${stacks}`,
|
|
346
|
+
`- Output style: ${persona.output_style || 'lean'}`,
|
|
347
|
+
`- Phase template: ${persona.phase_template || 'generic'}`,
|
|
348
|
+
`- Team size: ${persona.team_size || 'unknown'}`,
|
|
349
|
+
].join('\n');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readActivePersona() {
|
|
353
|
+
try {
|
|
354
|
+
const active = readJsonSafe(ACTIVE_PERSONA_FILE);
|
|
355
|
+
if (!active || !active.name) return null;
|
|
356
|
+
return readJsonSafe(path.join(PERSONAS_DIR, `${active.name}.json`)) || null;
|
|
357
|
+
} catch { return null; }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function writePersona(name, persona) {
|
|
361
|
+
try {
|
|
362
|
+
fs.mkdirSync(PERSONAS_DIR, { recursive: true });
|
|
363
|
+
writeJsonSafe(path.join(PERSONAS_DIR, `${name}.json`), { ...persona, name });
|
|
364
|
+
} catch { /* silent */ }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function setActivePersona(name) {
|
|
368
|
+
try {
|
|
369
|
+
fs.mkdirSync(VIEPILOT_DIR, { recursive: true });
|
|
370
|
+
writeJsonSafe(ACTIVE_PERSONA_FILE, { name, updated_at: new Date().toISOString() });
|
|
371
|
+
} catch { /* silent */ }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function listPersonas() {
|
|
375
|
+
try {
|
|
376
|
+
const active = readJsonSafe(ACTIVE_PERSONA_FILE);
|
|
377
|
+
const activeName = active?.name || null;
|
|
378
|
+
const files = fs.readdirSync(PERSONAS_DIR).filter(f => f.endsWith('.json'));
|
|
379
|
+
return files.map(f => {
|
|
380
|
+
const data = readJsonSafe(path.join(PERSONAS_DIR, f)) || {};
|
|
381
|
+
const name = f.replace('.json', '');
|
|
382
|
+
return {
|
|
383
|
+
name,
|
|
384
|
+
domain: Array.isArray(data.domain) ? data.domain.join('+') : (data.domain || 'unknown'),
|
|
385
|
+
confidence: data.confidence ?? '—',
|
|
386
|
+
active: name === activeName,
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
} catch { return []; }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function updateContextMap(projectDir, personaName) {
|
|
393
|
+
try {
|
|
394
|
+
const dir = path.resolve(projectDir);
|
|
395
|
+
const map = readJsonSafe(CONTEXT_MAP_FILE) || {};
|
|
396
|
+
map[dir] = personaName;
|
|
397
|
+
fs.mkdirSync(VIEPILOT_DIR, { recursive: true });
|
|
398
|
+
writeJsonSafe(CONTEXT_MAP_FILE, map);
|
|
399
|
+
} catch { /* silent */ }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function autoSwitch(projectDir) {
|
|
403
|
+
const dir = path.resolve(projectDir || process.cwd());
|
|
404
|
+
try {
|
|
405
|
+
let persona = resolvePersona(dir);
|
|
406
|
+
if (!persona) {
|
|
407
|
+
persona = await inferPersona(dir);
|
|
408
|
+
writePersona(persona.name, persona);
|
|
409
|
+
setActivePersona(persona.name);
|
|
410
|
+
updateContextMap(dir, persona.name);
|
|
411
|
+
} else {
|
|
412
|
+
const active = readActivePersona();
|
|
413
|
+
if (!active || active.name !== persona.name) {
|
|
414
|
+
setActivePersona(persona.name);
|
|
415
|
+
updateContextMap(dir, persona.name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if ((persona.confidence || 0) < 0.6) {
|
|
419
|
+
appendPendingReview(`Auto-detected persona '${persona.name}' for ${path.basename(dir)} with low confidence (${persona.confidence}). Run /vp-persona to review.`, persona.name);
|
|
420
|
+
}
|
|
421
|
+
return persona;
|
|
422
|
+
} catch { return null; }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function appendPendingReview(message, personaName) {
|
|
426
|
+
try {
|
|
427
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
428
|
+
const line = `- [${date}] 🟡 ${message} (persona: ${personaName}) — run /vp-persona to review\n`;
|
|
429
|
+
fs.mkdirSync(VIEPILOT_DIR, { recursive: true });
|
|
430
|
+
fs.appendFileSync(PENDING_REVIEW_FILE, line, 'utf8');
|
|
431
|
+
} catch { /* silent */ }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
inferPersona,
|
|
436
|
+
resolvePersona,
|
|
437
|
+
mergePersonas,
|
|
438
|
+
generatePersonaContext,
|
|
439
|
+
readActivePersona,
|
|
440
|
+
writePersona,
|
|
441
|
+
setActivePersona,
|
|
442
|
+
listPersonas,
|
|
443
|
+
updateContextMap,
|
|
444
|
+
autoSwitch,
|
|
445
|
+
appendPendingReview,
|
|
446
|
+
};
|
package/package.json
CHANGED
package/skills/vp-audit/SKILL.md
CHANGED
|
@@ -41,6 +41,16 @@ version, `{adapter_id}` with the active adapter (claude-code / cursor / antigrav
|
|
|
41
41
|
- `config.json` → `update.check: false` → skip this step entirely
|
|
42
42
|
- Show at most once per session (`update_check_done` session guard)
|
|
43
43
|
</version_check>
|
|
44
|
+
<persona_context>
|
|
45
|
+
## Persona Context Injection (ENH-073)
|
|
46
|
+
At skill start, run:
|
|
47
|
+
```bash
|
|
48
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona auto-switch
|
|
49
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona context
|
|
50
|
+
```
|
|
51
|
+
Inject the output as `## User Persona` context before any task execution.
|
|
52
|
+
Silent if command unavailable or errors.
|
|
53
|
+
</persona_context>
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
<cursor_skill_adapter>
|
package/skills/vp-auto/SKILL.md
CHANGED
|
@@ -41,6 +41,16 @@ version, `{adapter_id}` with the active adapter (claude-code / cursor / antigrav
|
|
|
41
41
|
- `config.json` → `update.check: false` → skip this step entirely
|
|
42
42
|
- Show at most once per session (`update_check_done` session guard)
|
|
43
43
|
</version_check>
|
|
44
|
+
<persona_context>
|
|
45
|
+
## Persona Context Injection (ENH-073)
|
|
46
|
+
At skill start, run:
|
|
47
|
+
```bash
|
|
48
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona auto-switch
|
|
49
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona context
|
|
50
|
+
```
|
|
51
|
+
Inject the output as `## User Persona` context before any task execution.
|
|
52
|
+
Silent if command unavailable or errors.
|
|
53
|
+
</persona_context>
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
<cursor_skill_adapter>
|
|
@@ -41,6 +41,16 @@ version, `{adapter_id}` with the active adapter (claude-code / cursor / antigrav
|
|
|
41
41
|
- `config.json` → `update.check: false` → skip this step entirely
|
|
42
42
|
- Show at most once per session (`update_check_done` session guard)
|
|
43
43
|
</version_check>
|
|
44
|
+
<persona_context>
|
|
45
|
+
## Persona Context Injection (ENH-073)
|
|
46
|
+
At skill start, run:
|
|
47
|
+
```bash
|
|
48
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona auto-switch
|
|
49
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona context
|
|
50
|
+
```
|
|
51
|
+
Inject the output as `## User Persona` context before any task execution.
|
|
52
|
+
Silent if command unavailable or errors.
|
|
53
|
+
</persona_context>
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
<cursor_skill_adapter>
|
|
@@ -127,7 +137,7 @@ Optional flags:
|
|
|
127
137
|
- `--list` : List all sessions
|
|
128
138
|
- `--landing` : Prioritize the Landing Page layout discovery flow
|
|
129
139
|
- `--research` : Enable proactive research suggestions during the session
|
|
130
|
-
- `--ui` : Enable UI Direction mode (live HTML/CSS direction artifacts)
|
|
140
|
+
- `--ui` : Enable UI Direction mode (live HTML/CSS direction artifacts + auto-generates `design.md` when design keywords present — ENH-076)
|
|
131
141
|
- `--domain embedded` : Force-activate Embedded Domain Mode (hardware topology, RTOS, pin map, memory layout, protocol matrix, power budget pages + topic probes)
|
|
132
142
|
</context>
|
|
133
143
|
|
|
@@ -205,6 +215,7 @@ Key steps:
|
|
|
205
215
|
- [ ] **FEAT-010 + ENH-019 + ENH-020**: `/research-ui` (when `--ui`) runs all 3 phases, including **content stress pass** + **archetype recipes** + **`## UX walkthrough log`** (with **Stress findings**) when prototype is updated
|
|
206
216
|
- [ ] `## Phases` present with Phase 1 having real content when scope is discussed
|
|
207
217
|
- [ ] **FEAT-009**: intake completed, binding already present, **or** waiver with reason before Completed; session records **`## Project meta intake (FEAT-009)`**
|
|
218
|
+
- [ ] **ENH-076**: `design.md` generated in session directory when UI mode active and ≥2 design keywords present; `notes.md ## design_tokens` populated
|
|
208
219
|
- [ ] Next steps suggested
|
|
209
220
|
</success_criteria>
|
|
210
221
|
|
|
@@ -232,6 +243,10 @@ plain-text numbered list prompts — no configuration required.
|
|
|
232
243
|
**Prompts using AskUserQuestion in this skill:**
|
|
233
244
|
- Session intent (continue / review / new — Step 2)
|
|
234
245
|
- Landing page layout selection (Step 4 — Layout A/B/C/D)
|
|
246
|
+
- Mid-session structured choices (≥2 discrete named options — BUG-022): topic direction,
|
|
247
|
+
section priority, angle selection, and any numbered decision with enumerable options
|
|
248
|
+
- Session-transition/next-steps prompt (session-flow choices: save/crystallize, update UI
|
|
249
|
+
artifacts, continue discussing — BUG-023)
|
|
235
250
|
|
|
236
251
|
|
|
237
252
|
### Skill Registry Integration (FEAT-020)
|
|
@@ -41,6 +41,16 @@ version, `{adapter_id}` with the active adapter (claude-code / cursor / antigrav
|
|
|
41
41
|
- `config.json` → `update.check: false` → skip this step entirely
|
|
42
42
|
- Show at most once per session (`update_check_done` session guard)
|
|
43
43
|
</version_check>
|
|
44
|
+
<persona_context>
|
|
45
|
+
## Persona Context Injection (ENH-073)
|
|
46
|
+
At skill start, run:
|
|
47
|
+
```bash
|
|
48
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona auto-switch
|
|
49
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona context
|
|
50
|
+
```
|
|
51
|
+
Inject the output as `## User Persona` context before any task execution.
|
|
52
|
+
Silent if command unavailable or errors.
|
|
53
|
+
</persona_context>
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
<cursor_skill_adapter>
|
package/skills/vp-debug/SKILL.md
CHANGED
|
@@ -41,6 +41,16 @@ version, `{adapter_id}` with the active adapter (claude-code / cursor / antigrav
|
|
|
41
41
|
- `config.json` → `update.check: false` → skip this step entirely
|
|
42
42
|
- Show at most once per session (`update_check_done` session guard)
|
|
43
43
|
</version_check>
|
|
44
|
+
<persona_context>
|
|
45
|
+
## Persona Context Injection (ENH-073)
|
|
46
|
+
At skill start, run:
|
|
47
|
+
```bash
|
|
48
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona auto-switch
|
|
49
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" persona context
|
|
50
|
+
```
|
|
51
|
+
Inject the output as `## User Persona` context before any task execution.
|
|
52
|
+
Silent if command unavailable or errors.
|
|
53
|
+
</persona_context>
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
<cursor_skill_adapter>
|