viepilot 2.23.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.
- package/CHANGELOG.md +288 -0
- package/README.md +6 -6
- package/bin/viepilot.cjs +140 -1
- package/bin/vp-tools.cjs +204 -0
- package/docs/brainstorm/session-2026-04-20.md +261 -0
- package/docs/brainstorm/session-2026-04-24.md +131 -0
- package/docs/brainstorm/session-2026-04-25.md +109 -0
- package/docs/skills-reference.md +22 -0
- package/docs/user/features/adapters.md +2 -2
- package/docs/user/features/scaffold-first.md +62 -0
- package/docs/user/features/skill-registry.md +125 -0
- package/lib/adapters/antigravity.cjs +5 -4
- 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/skill-installer.cjs +274 -0
- package/lib/skill-registry.cjs +212 -0
- package/lib/viepilot-calibrate.cjs +279 -0
- package/lib/viepilot-persona.cjs +446 -0
- package/lib/viepilot-update.cjs +113 -0
- package/package.json +1 -1
- package/skills/vp-audit/SKILL.md +67 -9
- package/skills/vp-auto/SKILL.md +54 -0
- package/skills/vp-brainstorm/SKILL.md +124 -2
- package/skills/vp-crystallize/SKILL.md +82 -0
- package/skills/vp-debug/SKILL.md +37 -0
- package/skills/vp-design/SKILL.md +219 -0
- package/skills/vp-docs/SKILL.md +37 -0
- package/skills/vp-evolve/SKILL.md +69 -6
- package/skills/vp-info/SKILL.md +37 -0
- package/skills/vp-pause/SKILL.md +37 -0
- package/skills/vp-persona/SKILL.md +207 -0
- package/skills/vp-proposal/SKILL.md +37 -0
- package/skills/vp-request/SKILL.md +62 -6
- package/skills/vp-resume/SKILL.md +37 -0
- package/skills/vp-rollback/SKILL.md +61 -1
- package/skills/vp-skills/SKILL.md +311 -0
- package/skills/vp-status/SKILL.md +37 -0
- package/skills/vp-task/SKILL.md +37 -0
- package/skills/vp-ui-components/SKILL.md +37 -0
- package/skills/vp-update/SKILL.md +37 -0
- package/templates/phase/TASK.md +7 -0
- package/templates/project/PROJECT-CONTEXT.md +76 -0
- package/workflows/audit.md +131 -0
- package/workflows/autonomous.md +199 -0
- package/workflows/brainstorm.md +1172 -9
- package/workflows/crystallize.md +639 -3
- 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/lib/viepilot-update.cjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plan and run `npm` upgrade for the viepilot package (FEAT-008 / vp-tools update).
|
|
3
|
+
* ENH-072: checkLatestVersion() — non-blocking npm registry check with 24h cache.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const https = require('https');
|
|
6
10
|
const { spawnSync, execFileSync } = require('child_process');
|
|
7
11
|
const viepilotInfo = require('./viepilot-info.cjs');
|
|
8
12
|
|
|
@@ -147,10 +151,119 @@ function runNpmUpdate(plan) {
|
|
|
147
151
|
return { ok: false, code: r.status == null ? 1 : r.status };
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
/**
|
|
155
|
+
* ENH-072: Check npm registry for a newer ViePilot version, with 24h cache.
|
|
156
|
+
*
|
|
157
|
+
* Silent on all errors — never throws, never crashes a skill invocation.
|
|
158
|
+
*
|
|
159
|
+
* @param {{ force?: boolean, cacheFile?: string, _fetchFn?: function }} opts
|
|
160
|
+
* @returns {Promise<{ upToDate: boolean, installed: string, latest: string }>}
|
|
161
|
+
*/
|
|
162
|
+
async function checkLatestVersion(opts = {}) {
|
|
163
|
+
const SILENT_RESULT = { upToDate: true, installed: '', latest: '' };
|
|
164
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const cacheFile =
|
|
168
|
+
opts.cacheFile || path.join(os.homedir(), '.viepilot', 'update-cache.json');
|
|
169
|
+
|
|
170
|
+
// Read installed version from package.json at viepilot package root
|
|
171
|
+
const pkgRoot = viepilotInfo.resolveViepilotPackageRoot(path.join(__dirname, '..'));
|
|
172
|
+
const installed = pkgRoot ? viepilotInfo.readInstalledVersion(pkgRoot) : null;
|
|
173
|
+
if (!installed) return SILENT_RESULT;
|
|
174
|
+
|
|
175
|
+
// Check cache unless force
|
|
176
|
+
if (!opts.force) {
|
|
177
|
+
try {
|
|
178
|
+
const raw = fs.readFileSync(cacheFile, 'utf8');
|
|
179
|
+
const cache = JSON.parse(raw);
|
|
180
|
+
if (
|
|
181
|
+
cache &&
|
|
182
|
+
typeof cache.checked_at === 'string' &&
|
|
183
|
+
typeof cache.latest === 'string' &&
|
|
184
|
+
Date.now() - new Date(cache.checked_at).getTime() < TTL_MS
|
|
185
|
+
) {
|
|
186
|
+
const upToDate = compareSemver(installed, cache.latest) >= 0;
|
|
187
|
+
return { upToDate, installed, latest: cache.latest };
|
|
188
|
+
}
|
|
189
|
+
} catch (_e) {
|
|
190
|
+
// cache missing or unreadable — proceed to network
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Fetch latest version from npm registry with 3s timeout
|
|
195
|
+
const fetchFn = opts._fetchFn || _fetchNpmLatest;
|
|
196
|
+
let latest;
|
|
197
|
+
try {
|
|
198
|
+
latest = await fetchFn('viepilot');
|
|
199
|
+
} catch (_e) {
|
|
200
|
+
return { upToDate: true, installed, latest: installed };
|
|
201
|
+
}
|
|
202
|
+
if (!latest || typeof latest !== 'string') {
|
|
203
|
+
return { upToDate: true, installed, latest: installed };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Write cache
|
|
207
|
+
try {
|
|
208
|
+
const cacheDir = path.dirname(cacheFile);
|
|
209
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
210
|
+
const has_update = compareSemver(installed, latest) < 0;
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
cacheFile,
|
|
213
|
+
JSON.stringify({ checked_at: new Date().toISOString(), installed, latest, has_update }),
|
|
214
|
+
'utf8'
|
|
215
|
+
);
|
|
216
|
+
} catch (_e) {
|
|
217
|
+
// cache write failure is non-fatal
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const upToDate = compareSemver(installed, latest) >= 0;
|
|
221
|
+
return { upToDate, installed, latest };
|
|
222
|
+
} catch (_e) {
|
|
223
|
+
return SILENT_RESULT;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Fetch latest version of a package from npm registry using https (3s timeout).
|
|
229
|
+
* @param {string} pkgName
|
|
230
|
+
* @returns {Promise<string>}
|
|
231
|
+
*/
|
|
232
|
+
function _fetchNpmLatest(pkgName) {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const req = https.get(
|
|
235
|
+
`https://registry.npmjs.org/${pkgName}/latest`,
|
|
236
|
+
{ headers: { 'Accept': 'application/json' } },
|
|
237
|
+
(res) => {
|
|
238
|
+
let body = '';
|
|
239
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
240
|
+
res.on('end', () => {
|
|
241
|
+
try {
|
|
242
|
+
const data = JSON.parse(body);
|
|
243
|
+
if (data && typeof data.version === 'string') {
|
|
244
|
+
resolve(data.version);
|
|
245
|
+
} else {
|
|
246
|
+
reject(new Error('no version in response'));
|
|
247
|
+
}
|
|
248
|
+
} catch (e) {
|
|
249
|
+
reject(e);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
req.on('error', reject);
|
|
255
|
+
req.setTimeout(3000, () => {
|
|
256
|
+
req.destroy(new Error('timeout'));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
150
261
|
module.exports = {
|
|
151
262
|
tryGetNpmGlobalViepilotPath,
|
|
152
263
|
classifyInstall,
|
|
153
264
|
compareSemver,
|
|
154
265
|
buildUpdatePlan,
|
|
155
266
|
runNpmUpdate,
|
|
267
|
+
checkLatestVersion,
|
|
268
|
+
_fetchNpmLatest,
|
|
156
269
|
};
|
package/package.json
CHANGED
package/skills/vp-audit/SKILL.md
CHANGED
|
@@ -15,6 +15,43 @@ Output this banner as the **first** thing on every invocation — before questio
|
|
|
15
15
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
16
16
|
```
|
|
17
17
|
</greeting>
|
|
18
|
+
<version_check>
|
|
19
|
+
## Version Update Check (ENH-072)
|
|
20
|
+
|
|
21
|
+
After displaying the greeting banner, run:
|
|
22
|
+
```bash
|
|
23
|
+
node "$HOME/.claude/viepilot/bin/vp-tools.cjs" check-update --silent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**If exit code = 1** (update available — new version printed to stdout):
|
|
27
|
+
Display notice banner before any other output:
|
|
28
|
+
```
|
|
29
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ ✨ ViePilot {latest_version} available (installed: {current}) │
|
|
31
|
+
│ npm i -g viepilot && vp-tools install --target {adapter_id} │
|
|
32
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
33
|
+
```
|
|
34
|
+
Replace `{latest_version}` with stdout from the command, `{current}` with the installed
|
|
35
|
+
version, `{adapter_id}` with the active adapter (claude-code / cursor / antigravity / codex / copilot).
|
|
36
|
+
|
|
37
|
+
**If exit code = 0 or command unavailable**: silent, continue.
|
|
38
|
+
|
|
39
|
+
**Suppression rules:**
|
|
40
|
+
- `--no-update-check` flag on skill invocation → skip this step entirely
|
|
41
|
+
- `config.json` → `update.check: false` → skip this step entirely
|
|
42
|
+
- Show at most once per session (`update_check_done` session guard)
|
|
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>
|
|
54
|
+
|
|
18
55
|
|
|
19
56
|
<cursor_skill_adapter>
|
|
20
57
|
## A. Skill Invocation
|
|
@@ -103,15 +140,36 @@ When auditing a project bootstrapped via `vp-crystallize --brownfield`:
|
|
|
103
140
|
|
|
104
141
|
<context>
|
|
105
142
|
Optional flags:
|
|
106
|
-
- `--framework`
|
|
107
|
-
- `--project`
|
|
108
|
-
- `--fix`
|
|
109
|
-
- `--report`
|
|
110
|
-
- `--silent`
|
|
111
|
-
- `--tier1`
|
|
112
|
-
- `--tier2`
|
|
113
|
-
- `--tier3`
|
|
114
|
-
- `--tier4`
|
|
143
|
+
- `--framework` : Force Tier 3 framework checks (even if not auto-detected)
|
|
144
|
+
- `--project` : Force project-only mode — skip Tier 3 framework checks
|
|
145
|
+
- `--fix` : Auto-fix all detected issues
|
|
146
|
+
- `--report` : Generate report file at `.viepilot/audit-report.md`
|
|
147
|
+
- `--silent` : Only output if issues found
|
|
148
|
+
- `--tier1` : Run Tier 1 (state consistency) only
|
|
149
|
+
- `--tier2` : Run Tier 2 (docs drift) only
|
|
150
|
+
- `--tier3` : Run Tier 3 (stack best-practice) only
|
|
151
|
+
- `--tier4` : Run Tier 4 (framework integrity) only
|
|
152
|
+
- `--no-autolog` : Skip auto-logging of gaps to `.viepilot/requests/`; report-only mode
|
|
153
|
+
|
|
154
|
+
### Auto-Log Behavior (ENH-070)
|
|
155
|
+
|
|
156
|
+
By default, `vp-audit` automatically logs each detected gap as a request file after the audit report is shown — no manual `/vp-request` step needed:
|
|
157
|
+
|
|
158
|
+
| Tier | Issue category | Request type | Priority |
|
|
159
|
+
|------|----------------|-------------|----------|
|
|
160
|
+
| 1 | State inconsistency / HANDOFF drift / git tag missing | BUG | medium |
|
|
161
|
+
| 1 | Execute-first ordering risk | BUG | medium |
|
|
162
|
+
| 2 | Doc drift (README/CHANGELOG version) | BUG | low |
|
|
163
|
+
| 2 | Missing docs / placeholder URLs | ENH | low |
|
|
164
|
+
| 3 | Stack violation / correctness anti-pattern | BUG | high |
|
|
165
|
+
| 3 | Stack improvement / best-practice gap | ENH | medium |
|
|
166
|
+
| 4 | Framework integrity gap | ENH | high |
|
|
167
|
+
|
|
168
|
+
**Duplicate detection**: if a matching open request already exists (title ≥ 70% match or file overlap), the finding is appended to it as a "Re-detected" note rather than creating a duplicate file.
|
|
169
|
+
|
|
170
|
+
**Post-audit banner**: after auto-logging, shows all logged request IDs and recommends `/vp-evolve {IDs}` as the next action. AUQ prompt on Claude Code terminal.
|
|
171
|
+
|
|
172
|
+
**Disable**: use `vp-audit --no-autolog` for report-only mode (no `.viepilot/requests/` files created).
|
|
115
173
|
</context>
|
|
116
174
|
|
|
117
175
|
<process>
|