viepilot 2.41.0 → 2.45.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/bin/viepilot.cjs +32 -0
  3. package/bin/vp-tools.cjs +95 -0
  4. package/docs/brainstorm/session-2026-04-24.md +131 -0
  5. package/docs/brainstorm/session-2026-04-25.md +109 -0
  6. package/lib/domain-packs/ai-product.json +33 -0
  7. package/lib/domain-packs/data-science.json +33 -0
  8. package/lib/domain-packs/devops.json +33 -0
  9. package/lib/domain-packs/mobile.json +33 -0
  10. package/lib/domain-packs/web-saas.json +33 -0
  11. package/lib/viepilot-calibrate.cjs +279 -0
  12. package/lib/viepilot-persona.cjs +446 -0
  13. package/package.json +1 -1
  14. package/skills/vp-audit/SKILL.md +10 -0
  15. package/skills/vp-auto/SKILL.md +10 -0
  16. package/skills/vp-brainstorm/SKILL.md +16 -1
  17. package/skills/vp-crystallize/SKILL.md +10 -0
  18. package/skills/vp-debug/SKILL.md +10 -0
  19. package/skills/vp-design/SKILL.md +219 -0
  20. package/skills/vp-docs/SKILL.md +10 -0
  21. package/skills/vp-evolve/SKILL.md +10 -0
  22. package/skills/vp-info/SKILL.md +10 -0
  23. package/skills/vp-pause/SKILL.md +10 -0
  24. package/skills/vp-persona/SKILL.md +207 -0
  25. package/skills/vp-proposal/SKILL.md +10 -0
  26. package/skills/vp-request/SKILL.md +10 -0
  27. package/skills/vp-resume/SKILL.md +10 -0
  28. package/skills/vp-rollback/SKILL.md +34 -1
  29. package/skills/vp-skills/SKILL.md +10 -0
  30. package/skills/vp-status/SKILL.md +10 -0
  31. package/skills/vp-task/SKILL.md +10 -0
  32. package/skills/vp-ui-components/SKILL.md +10 -0
  33. package/skills/vp-update/SKILL.md +10 -0
  34. package/workflows/autonomous.md +59 -0
  35. package/workflows/brainstorm.md +148 -1
  36. package/workflows/crystallize.md +111 -0
  37. package/workflows/design.md +601 -0
  38. package/workflows/evolve.md +9 -0
  39. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viepilot",
3
- "version": "2.41.0",
3
+ "version": "2.45.2",
4
4
  "description": "**Autonomous Vibe Coding Framework / Bộ khung phát triển tự động có kiểm soát**",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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>
@@ -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>
@@ -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>