viepilot 1.14.0 → 2.1.0

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 (54) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +3 -3
  3. package/bin/viepilot.cjs +7 -5
  4. package/bin/vp-tools.cjs +193 -0
  5. package/dev-install.sh +34 -13
  6. package/docs/user/features/hooks.md +93 -0
  7. package/lib/adapters/claude-code.cjs +42 -0
  8. package/lib/adapters/cursor.cjs +31 -0
  9. package/lib/adapters/index.cjs +26 -0
  10. package/lib/hooks/brainstorm-staleness.cjs +231 -0
  11. package/lib/viepilot-config.cjs +103 -0
  12. package/lib/viepilot-install.cjs +128 -153
  13. package/package.json +1 -1
  14. package/skills/vp-audit/SKILL.md +21 -21
  15. package/skills/vp-auto/SKILL.md +21 -7
  16. package/skills/vp-brainstorm/SKILL.md +42 -36
  17. package/skills/vp-crystallize/SKILL.md +22 -16
  18. package/skills/vp-debug/SKILL.md +2 -2
  19. package/skills/vp-docs/SKILL.md +7 -7
  20. package/skills/vp-evolve/SKILL.md +25 -12
  21. package/skills/vp-info/SKILL.md +23 -23
  22. package/skills/vp-pause/SKILL.md +5 -5
  23. package/skills/vp-request/SKILL.md +12 -12
  24. package/skills/vp-resume/SKILL.md +4 -4
  25. package/skills/vp-rollback/SKILL.md +3 -3
  26. package/skills/vp-status/SKILL.md +4 -4
  27. package/skills/vp-task/SKILL.md +2 -2
  28. package/skills/vp-ui-components/SKILL.md +12 -12
  29. package/skills/vp-update/SKILL.md +17 -17
  30. package/templates/architect/apis.html +11 -10
  31. package/templates/architect/architect-actions.js +217 -0
  32. package/templates/architect/architecture.html +8 -7
  33. package/templates/architect/data-flow.html +5 -4
  34. package/templates/architect/decisions.html +4 -3
  35. package/templates/architect/deployment.html +10 -9
  36. package/templates/architect/erd.html +7 -6
  37. package/templates/architect/feature-map.html +5 -4
  38. package/templates/architect/sequence-diagram.html +6 -5
  39. package/templates/architect/style.css +146 -0
  40. package/templates/architect/tech-notes.html +3 -2
  41. package/templates/architect/tech-stack.html +8 -7
  42. package/templates/architect/user-use-cases.html +8 -7
  43. package/templates/project/AI-GUIDE.md +49 -49
  44. package/workflows/audit.md +3 -3
  45. package/workflows/autonomous.md +38 -5
  46. package/workflows/brainstorm.md +398 -222
  47. package/workflows/crystallize.md +46 -33
  48. package/workflows/debug.md +9 -9
  49. package/workflows/documentation.md +5 -5
  50. package/workflows/evolve.md +44 -12
  51. package/workflows/pause-work.md +2 -2
  52. package/workflows/request.md +8 -8
  53. package/workflows/resume-work.md +1 -1
  54. package/workflows/rollback.md +1 -1
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+ const adapters = {
3
+ 'claude-code': require('./claude-code.cjs'),
4
+ 'cursor': require('./cursor.cjs'),
5
+ 'cursor-agent': require('./cursor.cjs'), // alias
6
+ 'cursor-ide': require('./cursor.cjs'), // alias
7
+ };
8
+
9
+ /**
10
+ * Get adapter by id. Throws if unknown.
11
+ * @param {string} id
12
+ */
13
+ function getAdapter(id) {
14
+ const a = adapters[id];
15
+ if (!a) throw new Error(`Unknown adapter: "${id}". Known: ${Object.keys(adapters).join(', ')}`);
16
+ return a;
17
+ }
18
+
19
+ /**
20
+ * List unique adapters (deduplicated — aliases share the same object).
21
+ */
22
+ function listAdapters() {
23
+ return [...new Set(Object.values(adapters))];
24
+ }
25
+
26
+ module.exports = { getAdapter, listAdapters, adapters };
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * ViePilot brainstorm staleness hook (FEAT-012)
5
+ * Claude Code Stop event handler.
6
+ *
7
+ * Reads stdin JSON: { session_id, transcript_path, cwd, ... }
8
+ * Detects architect HTML items that have become stale relative to the active
9
+ * brainstorm session notes, and marks them data-arch-stale="true" (flag-only).
10
+ *
11
+ * Non-blocking: exit 0 always. Errors are logged to stderr, never thrown.
12
+ *
13
+ * Install via: node bin/vp-tools.cjs hooks install
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ // ──────────────────────────────────────────────────────────────────────────────
21
+ // Architect page trigger keywords (reuses ENH-034 keyword lists)
22
+ // ──────────────────────────────────────────────────────────────────────────────
23
+ const ARCHITECT_TRIGGERS = {
24
+ 'architecture.html': ['c4', 'context diagram', 'system diagram', 'component', 'architecture'],
25
+ 'data-flow.html': ['data flow', 'request flow', 'event flow', 'pipeline', 'data-flow'],
26
+ 'erd.html': ['entity', 'relation', 'table', 'schema', 'database', 'erd'],
27
+ 'user-use-cases.html': ['use case', 'actor', 'user story', 'persona', 'use-case'],
28
+ 'sequence-diagram.html':['sequence', 'interaction', 'message flow', 'sequence diagram'],
29
+ 'deployment.html': ['deploy', 'infrastructure', 'container', 'cloud', 'k8s', 'kubernetes'],
30
+ 'apis.html': ['api', 'endpoint', 'rest', 'graphql', 'grpc', 'swagger', 'openapi'],
31
+ 'ui-design.html': ['ui design', 'ux design', 'mockup', 'wireframe', 'layout design'],
32
+ };
33
+
34
+ // ──────────────────────────────────────────────────────────────────────────────
35
+ // Session discovery
36
+ // ──────────────────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Find the most recently modified active brainstorm session file in cwd.
40
+ * Searches .viepilot/ui-direction/{id}/notes.md and docs/brainstorm/session-*.md.
41
+ * @param {string} cwd
42
+ * @returns {{ notesPath: string, sessionContent: string } | null}
43
+ */
44
+ function findActiveSession(cwd) {
45
+ const candidates = [];
46
+
47
+ // .viepilot/ui-direction/*/notes.md
48
+ const uiDir = path.join(cwd, '.viepilot', 'ui-direction');
49
+ if (fs.existsSync(uiDir)) {
50
+ try {
51
+ for (const entry of fs.readdirSync(uiDir, { withFileTypes: true })) {
52
+ if (entry.isDirectory()) {
53
+ const notesPath = path.join(uiDir, entry.name, 'notes.md');
54
+ if (fs.existsSync(notesPath)) {
55
+ candidates.push(notesPath);
56
+ }
57
+ }
58
+ }
59
+ } catch (_e) { /* ignore */ }
60
+ }
61
+
62
+ // docs/brainstorm/session-*.md
63
+ const brainstormDir = path.join(cwd, 'docs', 'brainstorm');
64
+ if (fs.existsSync(brainstormDir)) {
65
+ try {
66
+ for (const entry of fs.readdirSync(brainstormDir, { withFileTypes: true })) {
67
+ if (entry.isFile() && entry.name.startsWith('session-') && entry.name.endsWith('.md')) {
68
+ candidates.push(path.join(brainstormDir, entry.name));
69
+ }
70
+ }
71
+ } catch (_e) { /* ignore */ }
72
+ }
73
+
74
+ if (candidates.length === 0) return null;
75
+
76
+ // Pick most recently modified
77
+ let latest = null;
78
+ let latestMtime = 0;
79
+ for (const p of candidates) {
80
+ try {
81
+ const stat = fs.statSync(p);
82
+ if (stat.mtimeMs > latestMtime) {
83
+ latestMtime = stat.mtimeMs;
84
+ latest = p;
85
+ }
86
+ } catch (_e) { /* ignore */ }
87
+ }
88
+
89
+ if (!latest) return null;
90
+
91
+ try {
92
+ const sessionContent = fs.readFileSync(latest, 'utf8');
93
+ return { notesPath: latest, sessionContent };
94
+ } catch (_e) {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ // ──────────────────────────────────────────────────────────────────────────────
100
+ // Staleness detection
101
+ // ──────────────────────────────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * Detect which architect HTML pages have become stale based on session content.
105
+ * @param {string} sessionContent
106
+ * @param {string} architectDir - directory containing architect HTML files
107
+ * @returns {{ page: string, reason: string }[]}
108
+ */
109
+ function detectStaleItems(sessionContent, architectDir) {
110
+ const lower = sessionContent.toLowerCase();
111
+ const stale = [];
112
+
113
+ for (const [page, keywords] of Object.entries(ARCHITECT_TRIGGERS)) {
114
+ const filePath = path.join(architectDir, page);
115
+ // Only flag pages that actually exist
116
+ if (!fs.existsSync(filePath)) continue;
117
+
118
+ const matchedKeywords = keywords.filter((kw) => lower.includes(kw));
119
+ if (matchedKeywords.length > 0) {
120
+ stale.push({
121
+ page,
122
+ reason: `brainstorm session mentions: ${matchedKeywords.slice(0, 3).join(', ')}`,
123
+ });
124
+ }
125
+ }
126
+
127
+ return stale;
128
+ }
129
+
130
+ // ──────────────────────────────────────────────────────────────────────────────
131
+ // HTML patching
132
+ // ──────────────────────────────────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Mark all [data-arch-id] elements in an HTML file as stale (flag-only, no content rewrite).
136
+ * Idempotent: elements already marked are skipped.
137
+ * @param {string} filePath
138
+ * @param {string} reason
139
+ * @returns {boolean} true if file was modified
140
+ */
141
+ function markStaleInFile(filePath, reason) {
142
+ let html;
143
+ try {
144
+ html = fs.readFileSync(filePath, 'utf8');
145
+ } catch (_e) {
146
+ return false;
147
+ }
148
+
149
+ // Match opening tags that have data-arch-id but NOT already data-arch-stale
150
+ // Pattern: any tag with data-arch-id="..." that lacks data-arch-stale
151
+ const tagRegex = /(<(?:tr|div|td|section)[^>]*data-arch-id="[^"]*"[^>]*?)(?!\s*data-arch-stale)(>)/gi;
152
+
153
+ let changed = false;
154
+ const safeReason = reason.replace(/"/g, '&quot;').replace(/</g, '&lt;');
155
+
156
+ const patched = html.replace(tagRegex, (match, tagOpen, closeBracket) => {
157
+ // Double-check not already stale
158
+ if (tagOpen.includes('data-arch-stale')) return match;
159
+ changed = true;
160
+ return `${tagOpen} data-arch-stale="true" data-arch-stale-note="${safeReason}"${closeBracket}`;
161
+ });
162
+
163
+ if (!changed) return false;
164
+
165
+ try {
166
+ fs.writeFileSync(filePath, patched, 'utf8');
167
+ return true;
168
+ } catch (_e) {
169
+ return false;
170
+ }
171
+ }
172
+
173
+ // ──────────────────────────────────────────────────────────────────────────────
174
+ // Main
175
+ // ──────────────────────────────────────────────────────────────────────────────
176
+
177
+ async function run(hookData) {
178
+ const cwd = hookData.cwd || process.cwd();
179
+
180
+ const session = findActiveSession(cwd);
181
+ if (!session) return; // no active brainstorm session — nothing to do
182
+
183
+ // Resolve architect directory: prefer repo-local, fall back to installed location
184
+ const repoArchDir = path.join(cwd, 'templates', 'architect');
185
+ const installArchDir = path.join(os.homedir(), '.claude', 'viepilot', 'templates', 'architect');
186
+ const architectDir = fs.existsSync(repoArchDir) ? repoArchDir
187
+ : fs.existsSync(installArchDir) ? installArchDir
188
+ : null;
189
+
190
+ if (!architectDir) return;
191
+
192
+ const stalePages = detectStaleItems(session.sessionContent, architectDir);
193
+ if (stalePages.length === 0) return;
194
+
195
+ let patchCount = 0;
196
+ for (const { page, reason } of stalePages) {
197
+ const filePath = path.join(architectDir, page);
198
+ const changed = markStaleInFile(filePath, reason);
199
+ if (changed) patchCount++;
200
+ }
201
+
202
+ if (patchCount > 0) {
203
+ process.stderr.write(
204
+ `[viepilot-hook] ⚠ Marked ${patchCount} architect page(s) stale` +
205
+ ` (session: ${path.basename(path.dirname(session.notesPath))})\n`
206
+ );
207
+ }
208
+ }
209
+
210
+ // ──────────────────────────────────────────────────────────────────────────────
211
+ // Entry point — only activates when run directly (not when require()'d in tests)
212
+ // ──────────────────────────────────────────────────────────────────────────────
213
+
214
+ if (require.main === module) {
215
+ process.stdin.setEncoding('utf8');
216
+ let raw = '';
217
+ process.stdin.on('data', (chunk) => { raw += chunk; });
218
+ process.stdin.on('end', () => {
219
+ let hookData = {};
220
+ try { hookData = JSON.parse(raw); } catch (_e) { /* no stdin or not JSON = dev/test run */ }
221
+ run(hookData)
222
+ .catch((e) => {
223
+ process.stderr.write(`[viepilot-hook] error: ${e.message}\n`);
224
+ })
225
+ .finally(() => process.exit(0));
226
+ });
227
+ process.stdin.on('error', () => process.exit(0));
228
+ }
229
+
230
+ // Export internals for testing
231
+ module.exports = { findActiveSession, detectStaleItems, markStaleInFile };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * ViePilot language configuration — schema, read/write, defaults (ENH-032 / Phase 49).
3
+ *
4
+ * Config file: ~/.viepilot/config.json
5
+ * Schema:
6
+ * language.communication — language for AI↔user banners/prompts (default: "en")
7
+ * language.document — language for generated project files (default: "en")
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ /** @type {{ language: { communication: string, document: string } }} */
17
+ const DEFAULTS = {
18
+ language: {
19
+ communication: 'en',
20
+ document: 'en',
21
+ },
22
+ };
23
+
24
+ /**
25
+ * @param {string | undefined} overrideHomedir
26
+ * @returns {string}
27
+ */
28
+ function getConfigPath(overrideHomedir) {
29
+ const home = overrideHomedir != null ? path.resolve(overrideHomedir) : os.homedir();
30
+ return path.join(home, '.viepilot', 'config.json');
31
+ }
32
+
33
+ /**
34
+ * Deep-merge src into dst (one level under each top-level key).
35
+ * @param {object} dst
36
+ * @param {object} src
37
+ * @returns {object}
38
+ */
39
+ function deepMerge(dst, src) {
40
+ const result = Object.assign({}, dst);
41
+ for (const key of Object.keys(src)) {
42
+ if (
43
+ src[key] !== null &&
44
+ typeof src[key] === 'object' &&
45
+ !Array.isArray(src[key]) &&
46
+ dst[key] !== null &&
47
+ typeof dst[key] === 'object'
48
+ ) {
49
+ result[key] = Object.assign({}, dst[key], src[key]);
50
+ } else {
51
+ result[key] = src[key];
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * Read config, deep-merged with DEFAULTS. Returns DEFAULTS when file is absent.
59
+ * @param {string | undefined} overrideHomedir
60
+ * @returns {{ language: { communication: string, document: string } }}
61
+ */
62
+ function readConfig(overrideHomedir) {
63
+ const configPath = getConfigPath(overrideHomedir);
64
+ try {
65
+ const raw = fs.readFileSync(configPath, 'utf8');
66
+ const parsed = JSON.parse(raw);
67
+ return deepMerge(DEFAULTS, parsed);
68
+ } catch (_e) {
69
+ return deepMerge({}, DEFAULTS);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Deep-patch existing config with `patch` and write back to disk.
75
+ * Creates ~/.viepilot/ directory if missing.
76
+ * @param {Partial<{ language: Partial<{ communication: string, document: string }> }>} patch
77
+ * @param {string | undefined} overrideHomedir
78
+ */
79
+ function writeConfig(patch, overrideHomedir) {
80
+ const configPath = getConfigPath(overrideHomedir);
81
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
82
+ const current = readConfig(overrideHomedir);
83
+ const updated = deepMerge(current, patch);
84
+ fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf8');
85
+ }
86
+
87
+ /**
88
+ * Reset config to DEFAULTS.
89
+ * @param {string | undefined} overrideHomedir
90
+ */
91
+ function resetConfig(overrideHomedir) {
92
+ const configPath = getConfigPath(overrideHomedir);
93
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
94
+ fs.writeFileSync(configPath, JSON.stringify(DEFAULTS, null, 2) + '\n', 'utf8');
95
+ }
96
+
97
+ module.exports = {
98
+ DEFAULTS,
99
+ getConfigPath,
100
+ readConfig,
101
+ writeConfig,
102
+ resetConfig,
103
+ };