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.
- package/CHANGELOG.md +98 -0
- package/README.md +3 -3
- package/bin/viepilot.cjs +7 -5
- package/bin/vp-tools.cjs +193 -0
- package/dev-install.sh +34 -13
- package/docs/user/features/hooks.md +93 -0
- package/lib/adapters/claude-code.cjs +42 -0
- package/lib/adapters/cursor.cjs +31 -0
- package/lib/adapters/index.cjs +26 -0
- package/lib/hooks/brainstorm-staleness.cjs +231 -0
- package/lib/viepilot-config.cjs +103 -0
- package/lib/viepilot-install.cjs +128 -153
- package/package.json +1 -1
- package/skills/vp-audit/SKILL.md +21 -21
- package/skills/vp-auto/SKILL.md +21 -7
- package/skills/vp-brainstorm/SKILL.md +42 -36
- package/skills/vp-crystallize/SKILL.md +22 -16
- package/skills/vp-debug/SKILL.md +2 -2
- package/skills/vp-docs/SKILL.md +7 -7
- package/skills/vp-evolve/SKILL.md +25 -12
- package/skills/vp-info/SKILL.md +23 -23
- package/skills/vp-pause/SKILL.md +5 -5
- package/skills/vp-request/SKILL.md +12 -12
- package/skills/vp-resume/SKILL.md +4 -4
- package/skills/vp-rollback/SKILL.md +3 -3
- package/skills/vp-status/SKILL.md +4 -4
- package/skills/vp-task/SKILL.md +2 -2
- package/skills/vp-ui-components/SKILL.md +12 -12
- package/skills/vp-update/SKILL.md +17 -17
- package/templates/architect/apis.html +11 -10
- package/templates/architect/architect-actions.js +217 -0
- package/templates/architect/architecture.html +8 -7
- package/templates/architect/data-flow.html +5 -4
- package/templates/architect/decisions.html +4 -3
- package/templates/architect/deployment.html +10 -9
- package/templates/architect/erd.html +7 -6
- package/templates/architect/feature-map.html +5 -4
- package/templates/architect/sequence-diagram.html +6 -5
- package/templates/architect/style.css +146 -0
- package/templates/architect/tech-notes.html +3 -2
- package/templates/architect/tech-stack.html +8 -7
- package/templates/architect/user-use-cases.html +8 -7
- package/templates/project/AI-GUIDE.md +49 -49
- package/workflows/audit.md +3 -3
- package/workflows/autonomous.md +38 -5
- package/workflows/brainstorm.md +398 -222
- package/workflows/crystallize.md +46 -33
- package/workflows/debug.md +9 -9
- package/workflows/documentation.md +5 -5
- package/workflows/evolve.md +44 -12
- package/workflows/pause-work.md +2 -2
- package/workflows/request.md +8 -8
- package/workflows/resume-work.md +1 -1
- 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, '"').replace(/</g, '<');
|
|
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
|
+
};
|