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.
Files changed (40) hide show
  1. package/CHANGELOG.md +104 -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-install.cjs +5 -3
  13. package/lib/viepilot-persona.cjs +446 -0
  14. package/package.json +1 -1
  15. package/skills/vp-audit/SKILL.md +10 -0
  16. package/skills/vp-auto/SKILL.md +10 -0
  17. package/skills/vp-brainstorm/SKILL.md +16 -1
  18. package/skills/vp-crystallize/SKILL.md +10 -0
  19. package/skills/vp-debug/SKILL.md +10 -0
  20. package/skills/vp-design/SKILL.md +219 -0
  21. package/skills/vp-docs/SKILL.md +10 -0
  22. package/skills/vp-evolve/SKILL.md +10 -0
  23. package/skills/vp-info/SKILL.md +10 -0
  24. package/skills/vp-pause/SKILL.md +10 -0
  25. package/skills/vp-persona/SKILL.md +207 -0
  26. package/skills/vp-proposal/SKILL.md +10 -0
  27. package/skills/vp-request/SKILL.md +10 -0
  28. package/skills/vp-resume/SKILL.md +10 -0
  29. package/skills/vp-rollback/SKILL.md +34 -1
  30. package/skills/vp-skills/SKILL.md +10 -0
  31. package/skills/vp-status/SKILL.md +10 -0
  32. package/skills/vp-task/SKILL.md +10 -0
  33. package/skills/vp-ui-components/SKILL.md +10 -0
  34. package/skills/vp-update/SKILL.md +10 -0
  35. package/workflows/autonomous.md +59 -0
  36. package/workflows/brainstorm.md +148 -1
  37. package/workflows/crystallize.md +111 -0
  38. package/workflows/design.md +601 -0
  39. package/workflows/evolve.md +9 -0
  40. 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
+ };
@@ -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
- for (const f of libFiles) {
185
- steps.push({ kind: 'copy_file', from: path.join(root, 'lib', f), to: path.join(vpDir, 'lib', f) });
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')) {