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,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
+ };