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,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "web-saas",
|
|
3
|
+
"label": "Web SaaS",
|
|
4
|
+
"topic_priority": ["auth", "user-data", "api", "billing", "admin", "onboarding", "content-mgmt"],
|
|
5
|
+
"extra_topics": [
|
|
6
|
+
{
|
|
7
|
+
"id": "billing",
|
|
8
|
+
"label": "Billing & Subscriptions",
|
|
9
|
+
"questions": ["Payment provider (Stripe/Paddle/LemonSqueezy)?", "Pricing model (flat/usage/per-seat)?", "Free trial or freemium tier?", "Proration on plan change?"]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "multi-tenant",
|
|
13
|
+
"label": "Multi-tenancy",
|
|
14
|
+
"questions": ["Tenant isolation: row-level, schema-per-tenant, or separate DB?", "How are tenants identified (subdomain/slug/org)?", "Cross-tenant data visibility rules?"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "onboarding",
|
|
18
|
+
"label": "User Onboarding",
|
|
19
|
+
"questions": ["Onboarding flow steps (email verify → profile → invite team)?", "Guided setup wizard?", "Empty-state design for new accounts?"]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "trial-freemium",
|
|
23
|
+
"label": "Trial / Freemium",
|
|
24
|
+
"questions": ["Trial duration?", "Feature gating strategy (usage caps vs feature flags)?", "Upgrade CTA placement?"]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"phase_template": {
|
|
28
|
+
"name": "lean-startup",
|
|
29
|
+
"phases": ["Auth & Identity", "Core Features", "Monetization", "Scale & Ops"]
|
|
30
|
+
},
|
|
31
|
+
"architect_pages": ["billing.html", "tenant.html"],
|
|
32
|
+
"stacks_hint": ["nextjs", "nestjs", "postgresql", "stripe", "redis", "tailwind"]
|
|
33
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const VIEPILOT_DIR = path.join(os.homedir(), '.viepilot');
|
|
9
|
+
const TRACES_DIR = path.join(VIEPILOT_DIR, 'traces');
|
|
10
|
+
const OVERLAYS_DIR = path.join(VIEPILOT_DIR, 'overlays');
|
|
11
|
+
const REFLECTIONS_FILE = path.join(VIEPILOT_DIR, 'persona-reflections.json');
|
|
12
|
+
const PENDING_REVIEW_FILE = path.join(VIEPILOT_DIR, 'pending-review.md');
|
|
13
|
+
const PERSONAS_DIR = path.join(VIEPILOT_DIR, 'personas');
|
|
14
|
+
const ACTIVE_PERSONA_FILE = path.join(VIEPILOT_DIR, 'persona.json');
|
|
15
|
+
|
|
16
|
+
function readJsonSafe(filePath) {
|
|
17
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
function writeJsonSafe(filePath, data) {
|
|
20
|
+
try {
|
|
21
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
23
|
+
} catch { /* silent */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Write session trace async (fire-and-forget).
|
|
28
|
+
* traceData: { skill, persona, topics_offered, topics_discussed, topics_skipped, stacks_mentioned, duration_min }
|
|
29
|
+
*/
|
|
30
|
+
function writeSessionTrace(traceData) {
|
|
31
|
+
setImmediate(() => {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(TRACES_DIR, { recursive: true });
|
|
34
|
+
const rand = crypto.randomBytes(2).toString('hex');
|
|
35
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
36
|
+
const skill = (traceData.skill || 'unknown').replace(/[^a-z0-9-]/gi, '-');
|
|
37
|
+
const sessionId = `${skill}-${date}-${rand}`;
|
|
38
|
+
const filePath = path.join(TRACES_DIR, `${sessionId}.json`);
|
|
39
|
+
fs.writeFileSync(filePath, JSON.stringify({ ...traceData, session_id: sessionId, recorded_at: new Date().toISOString() }, null, 2), 'utf8');
|
|
40
|
+
} catch { /* silent */ }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read last N traces from ~/.viepilot/traces/*.json
|
|
46
|
+
*/
|
|
47
|
+
function readTraces(n = 20, opts = {}) {
|
|
48
|
+
try {
|
|
49
|
+
const files = fs.readdirSync(TRACES_DIR)
|
|
50
|
+
.filter(f => f.endsWith('.json'))
|
|
51
|
+
.map(f => path.join(TRACES_DIR, f));
|
|
52
|
+
const traces = files
|
|
53
|
+
.map(f => readJsonSafe(f))
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.filter(t => !opts.persona || t.persona === opts.persona)
|
|
56
|
+
.sort((a, b) => (b.recorded_at || '').localeCompare(a.recorded_at || ''));
|
|
57
|
+
return traces.slice(0, n);
|
|
58
|
+
} catch { return []; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detect patterns across traces and return proposals.
|
|
63
|
+
*/
|
|
64
|
+
function detectPatterns(traces) {
|
|
65
|
+
if (!traces || traces.length < 3) return [];
|
|
66
|
+
const reflections = readJsonSafe(REFLECTIONS_FILE) || { applied: [], guardrail_journal: [] };
|
|
67
|
+
const guardrailIds = new Set((reflections.guardrail_journal || []).map(g => g.id));
|
|
68
|
+
const proposals = [];
|
|
69
|
+
|
|
70
|
+
// Group by persona
|
|
71
|
+
const byPersona = {};
|
|
72
|
+
for (const t of traces) {
|
|
73
|
+
const p = t.persona || 'unknown';
|
|
74
|
+
if (!byPersona[p]) byPersona[p] = [];
|
|
75
|
+
byPersona[p].push(t);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const [persona, pTraces] of Object.entries(byPersona)) {
|
|
79
|
+
if (pTraces.length < 3) continue;
|
|
80
|
+
|
|
81
|
+
// Topic skip rate
|
|
82
|
+
const topicSkipCounts = {};
|
|
83
|
+
const topicOfferCounts = {};
|
|
84
|
+
for (const t of pTraces) {
|
|
85
|
+
for (const topic of (t.topics_offered || [])) {
|
|
86
|
+
topicOfferCounts[topic] = (topicOfferCounts[topic] || 0) + 1;
|
|
87
|
+
}
|
|
88
|
+
for (const topic of (t.topics_skipped || [])) {
|
|
89
|
+
topicSkipCounts[topic] = (topicSkipCounts[topic] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [topic, offerCount] of Object.entries(topicOfferCounts)) {
|
|
93
|
+
const skipCount = topicSkipCounts[topic] || 0;
|
|
94
|
+
const skipRate = skipCount / offerCount;
|
|
95
|
+
if (skipRate >= 0.70 && offerCount >= 3) {
|
|
96
|
+
const id = `topic_skip:${persona}:${topic}`;
|
|
97
|
+
if (!guardrailIds.has(id)) {
|
|
98
|
+
proposals.push({
|
|
99
|
+
id, persona, risk: 'green',
|
|
100
|
+
description: `Topic '${topic}' skipped in ${Math.round(skipRate * 100)}% of sessions`,
|
|
101
|
+
patch: { op: 'add_topic_skip', topic_id: topic },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Stack frequency
|
|
108
|
+
const stackCounts = {};
|
|
109
|
+
for (const t of pTraces) {
|
|
110
|
+
for (const stack of (t.stacks_mentioned || [])) {
|
|
111
|
+
stackCounts[stack] = (stackCounts[stack] || 0) + 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const [stack, count] of Object.entries(stackCounts)) {
|
|
115
|
+
const freq = count / pTraces.length;
|
|
116
|
+
if (freq >= 0.80) {
|
|
117
|
+
const id = `stack_add:${persona}:${stack}`;
|
|
118
|
+
if (!guardrailIds.has(id)) {
|
|
119
|
+
proposals.push({
|
|
120
|
+
id, persona, risk: 'green',
|
|
121
|
+
description: `Stack '${stack}' mentioned in ${Math.round(freq * 100)}% of sessions`,
|
|
122
|
+
patch: { op: 'add_stack', stack },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Long session duration → suggest balanced output_style
|
|
129
|
+
const avgDuration = pTraces.reduce((s, t) => s + (t.duration_min || 0), 0) / pTraces.length;
|
|
130
|
+
if (avgDuration > 60) {
|
|
131
|
+
const id = `output_style:${persona}:balanced`;
|
|
132
|
+
if (!guardrailIds.has(id)) {
|
|
133
|
+
proposals.push({
|
|
134
|
+
id, persona, risk: 'yellow',
|
|
135
|
+
description: `Average session duration ${Math.round(avgDuration)}min — suggest output_style: balanced`,
|
|
136
|
+
patch: { op: 'set_output_style', value: 'balanced' },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return proposals;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Apply a proposal patch to the persona file.
|
|
147
|
+
*/
|
|
148
|
+
function applyProposalToPersona(proposal) {
|
|
149
|
+
try {
|
|
150
|
+
const personaFile = path.join(PERSONAS_DIR, `${proposal.persona}.json`);
|
|
151
|
+
const persona = readJsonSafe(personaFile);
|
|
152
|
+
if (!persona) return false;
|
|
153
|
+
const { op, topic_id, stack, value } = proposal.patch;
|
|
154
|
+
if (op === 'add_topic_skip' && topic_id) {
|
|
155
|
+
if (!persona.brainstorm) persona.brainstorm = { topic_priority: [], topic_skip: [] };
|
|
156
|
+
if (!persona.brainstorm.topic_skip.includes(topic_id)) {
|
|
157
|
+
persona.brainstorm.topic_skip.push(topic_id);
|
|
158
|
+
}
|
|
159
|
+
} else if (op === 'add_stack' && stack) {
|
|
160
|
+
if (!persona.stacks) persona.stacks = [];
|
|
161
|
+
if (!persona.stacks.includes(stack)) persona.stacks.push(stack);
|
|
162
|
+
} else if (op === 'set_output_style' && value) {
|
|
163
|
+
persona.output_style = value;
|
|
164
|
+
} else if (op === 'set_phase_default' && value) {
|
|
165
|
+
persona.phase_template = value;
|
|
166
|
+
} else {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
writeJsonSafe(personaFile, persona);
|
|
170
|
+
return true;
|
|
171
|
+
} catch { return false; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Write a JSON Patch overlay for a persona's workflow.
|
|
176
|
+
*/
|
|
177
|
+
function applyOverlay(personaName, patch) {
|
|
178
|
+
try {
|
|
179
|
+
const overlayDir = path.join(OVERLAYS_DIR, personaName);
|
|
180
|
+
fs.mkdirSync(overlayDir, { recursive: true });
|
|
181
|
+
const overlayFile = path.join(overlayDir, 'brainstorm.patch.json');
|
|
182
|
+
const existing = readJsonSafe(overlayFile) || [];
|
|
183
|
+
existing.push({ ...patch, applied_at: new Date().toISOString() });
|
|
184
|
+
writeJsonSafe(overlayFile, existing);
|
|
185
|
+
} catch { /* silent */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Read overlays for a persona.
|
|
190
|
+
*/
|
|
191
|
+
function readOverlays(personaName) {
|
|
192
|
+
try {
|
|
193
|
+
const overlayFile = path.join(OVERLAYS_DIR, personaName, 'brainstorm.patch.json');
|
|
194
|
+
return readJsonSafe(overlayFile) || [];
|
|
195
|
+
} catch { return []; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Append an entry to pending-review.md.
|
|
200
|
+
*/
|
|
201
|
+
function appendPendingReview(message, personaName) {
|
|
202
|
+
try {
|
|
203
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
204
|
+
const line = `- [${date}] 🟡 ${message} (persona: ${personaName}) — run /vp-persona to review\n`;
|
|
205
|
+
fs.mkdirSync(VIEPILOT_DIR, { recursive: true });
|
|
206
|
+
fs.appendFileSync(PENDING_REVIEW_FILE, line, 'utf8');
|
|
207
|
+
} catch { /* silent */ }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Record applied/rejected proposals in reflections file.
|
|
212
|
+
*/
|
|
213
|
+
function recordReflections(applied, rejected) {
|
|
214
|
+
try {
|
|
215
|
+
const reflections = readJsonSafe(REFLECTIONS_FILE) || { applied: [], guardrail_journal: [] };
|
|
216
|
+
const date = new Date().toISOString();
|
|
217
|
+
for (const p of applied) {
|
|
218
|
+
reflections.applied.push({ ...p, applied_at: date });
|
|
219
|
+
}
|
|
220
|
+
for (const p of rejected) {
|
|
221
|
+
reflections.guardrail_journal.push({ id: p.id, rejected_at: date, reason: 'user_rejected' });
|
|
222
|
+
}
|
|
223
|
+
writeJsonSafe(REFLECTIONS_FILE, reflections);
|
|
224
|
+
} catch { /* silent */ }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Main calibration runner. Reads traces → detects patterns → applies by risk tier.
|
|
229
|
+
* Returns { applied, pending, blocked }
|
|
230
|
+
*/
|
|
231
|
+
async function runCalibration(opts = {}) {
|
|
232
|
+
const n = opts.traceCount || 20;
|
|
233
|
+
const personaFilter = opts.persona || null;
|
|
234
|
+
const traces = readTraces(n, personaFilter ? { persona: personaFilter } : {});
|
|
235
|
+
if (traces.length < 3) return { applied: [], pending: [], blocked: [] };
|
|
236
|
+
|
|
237
|
+
const proposals = detectPatterns(traces);
|
|
238
|
+
const green = proposals.filter(p => p.risk === 'green');
|
|
239
|
+
const yellow = proposals.filter(p => p.risk === 'yellow');
|
|
240
|
+
const red = proposals.filter(p => p.risk === 'red');
|
|
241
|
+
|
|
242
|
+
const applied = [];
|
|
243
|
+
const pending = [];
|
|
244
|
+
|
|
245
|
+
// Apply green automatically
|
|
246
|
+
for (const p of green) {
|
|
247
|
+
const ok = applyProposalToPersona(p);
|
|
248
|
+
if (ok) {
|
|
249
|
+
applyOverlay(p.persona, { op: p.patch.op, reason: p.description, ...p.patch });
|
|
250
|
+
applied.push(p);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Apply yellow automatically + log to pending-review
|
|
255
|
+
for (const p of yellow) {
|
|
256
|
+
const ok = applyProposalToPersona(p);
|
|
257
|
+
if (ok) {
|
|
258
|
+
applyOverlay(p.persona, { op: p.patch.op, reason: p.description, ...p.patch });
|
|
259
|
+
appendPendingReview(p.description, p.persona);
|
|
260
|
+
applied.push(p);
|
|
261
|
+
pending.push(p);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Record applied in reflections (prevents re-proposing)
|
|
266
|
+
if (applied.length > 0) recordReflections(applied, []);
|
|
267
|
+
|
|
268
|
+
return { applied, pending, blocked: red };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
writeSessionTrace,
|
|
273
|
+
runCalibration,
|
|
274
|
+
readTraces,
|
|
275
|
+
detectPatterns,
|
|
276
|
+
applyOverlay,
|
|
277
|
+
readOverlays,
|
|
278
|
+
appendPendingReview,
|
|
279
|
+
};
|
package/lib/viepilot-install.cjs
CHANGED
|
@@ -132,7 +132,6 @@ function buildInstallPlan(packageRoot, envSource = process.env, opts = {}) {
|
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
const binFiles = ['vp-tools.cjs', 'viepilot.cjs'];
|
|
135
|
-
const libFiles = ['cli-shared.cjs', 'viepilot-info.cjs', 'viepilot-update.cjs', 'viepilot-install.cjs', 'viepilot-config.cjs'];
|
|
136
135
|
const uiRoot = path.join(root, 'ui-components');
|
|
137
136
|
|
|
138
137
|
// One install loop per selected adapter (replaces cursor block + claude-code if-block).
|
|
@@ -181,8 +180,11 @@ function buildInstallPlan(packageRoot, envSource = process.env, opts = {}) {
|
|
|
181
180
|
for (const f of binFiles) {
|
|
182
181
|
steps.push({ kind: 'copy_file', from: path.join(root, 'bin', f), to: path.join(vpDir, 'bin', f) });
|
|
183
182
|
}
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// BUG-024: dynamic lib/ scan — copies ALL files + subdirs (adapters/, hooks/, domain-packs/, etc.)
|
|
184
|
+
for (const ent of listDirEntries(root, 'lib')) {
|
|
185
|
+
const src = path.join(root, 'lib', ent.name);
|
|
186
|
+
const dest = path.join(vpDir, 'lib', ent.name);
|
|
187
|
+
steps.push(ent.isDirectory() ? { kind: 'copy_dir', from: src, to: dest } : { kind: 'copy_file', from: src, to: dest });
|
|
186
188
|
}
|
|
187
189
|
if (fs.existsSync(uiRoot)) {
|
|
188
190
|
for (const ent of listDirEntries(root, 'ui-components')) {
|