viepilot 1.14.0 → 2.4.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 (58) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +27 -17
  3. package/bin/viepilot.cjs +19 -9
  4. package/bin/vp-tools.cjs +193 -0
  5. package/docs/user/features/adapters.md +74 -0
  6. package/docs/user/features/hooks.md +93 -0
  7. package/lib/adapters/antigravity.cjs +33 -0
  8. package/lib/adapters/claude-code.cjs +42 -0
  9. package/lib/adapters/codex.cjs +34 -0
  10. package/lib/adapters/cursor.cjs +32 -0
  11. package/lib/adapters/index.cjs +28 -0
  12. package/lib/hooks/brainstorm-staleness.cjs +231 -0
  13. package/lib/viepilot-config.cjs +103 -0
  14. package/lib/viepilot-install.cjs +125 -152
  15. package/package.json +1 -3
  16. package/skills/vp-audit/SKILL.md +23 -23
  17. package/skills/vp-auto/SKILL.md +23 -9
  18. package/skills/vp-brainstorm/SKILL.md +44 -38
  19. package/skills/vp-crystallize/SKILL.md +25 -19
  20. package/skills/vp-debug/SKILL.md +4 -4
  21. package/skills/vp-docs/SKILL.md +8 -8
  22. package/skills/vp-evolve/SKILL.md +26 -13
  23. package/skills/vp-info/SKILL.md +24 -24
  24. package/skills/vp-pause/SKILL.md +7 -7
  25. package/skills/vp-request/SKILL.md +14 -14
  26. package/skills/vp-resume/SKILL.md +6 -6
  27. package/skills/vp-rollback/SKILL.md +4 -4
  28. package/skills/vp-status/SKILL.md +4 -4
  29. package/skills/vp-task/SKILL.md +2 -2
  30. package/skills/vp-ui-components/SKILL.md +14 -14
  31. package/skills/vp-update/SKILL.md +18 -18
  32. package/templates/architect/apis.html +11 -10
  33. package/templates/architect/architect-actions.js +217 -0
  34. package/templates/architect/architecture.html +8 -7
  35. package/templates/architect/data-flow.html +5 -4
  36. package/templates/architect/decisions.html +4 -3
  37. package/templates/architect/deployment.html +10 -9
  38. package/templates/architect/erd.html +7 -6
  39. package/templates/architect/feature-map.html +5 -4
  40. package/templates/architect/sequence-diagram.html +6 -5
  41. package/templates/architect/style.css +146 -0
  42. package/templates/architect/tech-notes.html +3 -2
  43. package/templates/architect/tech-stack.html +8 -7
  44. package/templates/architect/user-use-cases.html +8 -7
  45. package/templates/project/AI-GUIDE.md +49 -49
  46. package/workflows/audit.md +3 -3
  47. package/workflows/autonomous.md +70 -5
  48. package/workflows/brainstorm.md +398 -222
  49. package/workflows/crystallize.md +51 -33
  50. package/workflows/debug.md +9 -9
  51. package/workflows/documentation.md +5 -5
  52. package/workflows/evolve.md +46 -12
  53. package/workflows/pause-work.md +2 -2
  54. package/workflows/request.md +8 -8
  55. package/workflows/resume-work.md +1 -1
  56. package/workflows/rollback.md +1 -1
  57. package/dev-install.sh +0 -150
  58. package/install.sh +0 -125
@@ -0,0 +1,74 @@
1
+ # Supported Adapters
2
+
3
+ ViePilot supports multiple AI coding platforms via its adapter system (FEAT-013). Each adapter defines where skills and workflows are installed on your machine.
4
+
5
+ ## Supported Platforms
6
+
7
+ | Adapter ID | Platform | Skills dir | ViePilot dir | Hooks | Skill syntax |
8
+ |------------|----------|------------|--------------|-------|--------------|
9
+ | `claude-code` | Claude Code *(default)* | `~/.claude/skills/` | `~/.claude/viepilot/` | ✅ Stop, PreToolUse, … | `/vp-status` |
10
+ | `cursor-agent` / `cursor-ide` | Cursor | `~/.cursor/skills/` | `~/.cursor/viepilot/` | — | `/vp-status` |
11
+ | `antigravity` | Google Antigravity | `~/.antigravity/skills/` | `~/.antigravity/viepilot/` | — | `/vp-status` |
12
+ | `codex` | OpenAI Codex CLI | `~/.codex/skills/` | `~/.codex/viepilot/` | — | `$vp-status` |
13
+
14
+ > **Note — Codex invocation syntax:** OpenAI Codex CLI uses `$skill-name` to invoke skills (e.g. `$vp-status`, `$vp-brainstorm`). The `/command` prefix is reserved for Codex built-in controls (`/plan`, `/clear`, `/diff`, etc.). SKILL.md file format is fully compatible — no changes needed to skill content.
15
+
16
+ ## Install for a specific platform
17
+
18
+ ```bash
19
+ # Claude Code (default)
20
+ viepilot install
21
+
22
+ # Cursor Agent
23
+ viepilot install --target cursor-agent
24
+
25
+ # Google Antigravity
26
+ viepilot install --target antigravity
27
+
28
+ # OpenAI Codex CLI
29
+ viepilot install --target codex
30
+
31
+ # Multiple targets at once
32
+ viepilot install --target claude-code,antigravity
33
+ ```
34
+
35
+ ## How path resolution works (ENH-035)
36
+
37
+ Skill source files use the neutral placeholder `{envToolDir}` in `execution_context` blocks:
38
+
39
+ ```
40
+ @$HOME/{envToolDir}/workflows/autonomous.md
41
+ ```
42
+
43
+ At install time, `{envToolDir}` is replaced with each adapter's `executionContextBase`:
44
+ - `claude-code` → `.claude/viepilot`
45
+ - `cursor` → `.cursor/viepilot`
46
+ - `antigravity` → `.antigravity/viepilot`
47
+ - `codex` → `.codex/viepilot`
48
+
49
+ ## Adding a new adapter
50
+
51
+ Create `lib/adapters/{name}.cjs`:
52
+
53
+ ```js
54
+ module.exports = {
55
+ id: 'myplatform',
56
+ name: 'My Platform',
57
+ skillsDir: (home) => path.join(home, '.myplatform', 'skills'),
58
+ viepilotDir: (home) => path.join(home, '.myplatform', 'viepilot'),
59
+ executionContextBase: '.myplatform/viepilot', // {envToolDir} resolves to this
60
+ postInstallHint: 'Open project and run /vp-status', // or $vp-status if platform uses $ prefix
61
+ hooks: { configFile: null, schema: 'myplatform', supportedEvents: [] },
62
+ installSubdirs: ['workflows', 'templates/project', 'templates/phase',
63
+ 'templates/architect', 'bin', 'lib', 'ui-components'],
64
+ isAvailable: (home) => fs.existsSync(path.join(home, '.myplatform'))
65
+ };
66
+ ```
67
+
68
+ Register in `lib/adapters/index.cjs`:
69
+
70
+ ```js
71
+ 'myplatform': require('./myplatform.cjs'),
72
+ ```
73
+
74
+ No `pathRewrite` field needed — `{envToolDir}` substitution is handled automatically.
@@ -0,0 +1,93 @@
1
+ # ViePilot Hooks
2
+
3
+ ViePilot integrates with Claude Code's hook system to automate actions after each AI response. Hooks are shell commands that fire at specific Claude Code events — no manual typing required.
4
+
5
+ ## Brainstorm Staleness Hook
6
+
7
+ **Event**: `Stop` (fires after each Claude response)
8
+ **Script**: `~/.viepilot/hooks/brainstorm-staleness.cjs`
9
+ **Config**: `~/.claude/settings.json`
10
+
11
+ ### What it does
12
+
13
+ After each exchange in a `vp-brainstorm` session, the hook automatically:
14
+
15
+ 1. Finds the most recently modified brainstorm session file (`notes.md` or `session-*.md`)
16
+ 2. Scans session content for keywords that match architect HTML pages
17
+ 3. Marks relevant architect items with `data-arch-stale="true"` — rendered as an amber "⚠ gap" badge in the browser
18
+ 4. Exits cleanly — if it fails or finds nothing, your session continues normally
19
+
20
+ This is **flag-only** (Option A) — the hook does not rewrite architect HTML content, only marks items for review. Use `/sync-arch` to perform a full content sync.
21
+
22
+ ### Install
23
+
24
+ Run once per machine:
25
+
26
+ ```bash
27
+ node bin/vp-tools.cjs hooks install
28
+ # or if installed globally:
29
+ vp-tools hooks install
30
+ ```
31
+
32
+ This writes the hook entry into `~/.claude/settings.json`. Running it again is safe (idempotent).
33
+
34
+ To verify the install:
35
+
36
+ ```bash
37
+ cat ~/.claude/settings.json | grep brainstorm-staleness
38
+ ```
39
+
40
+ ### Preview (scaffold)
41
+
42
+ To see what will be written without installing:
43
+
44
+ ```bash
45
+ node bin/vp-tools.cjs hooks scaffold
46
+ ```
47
+
48
+ ### Cursor users
49
+
50
+ Cursor does not support `settings.json` hook events. Use `/sync-arch` manually within the brainstorm session to trigger architect delta sync (ENH-034).
51
+
52
+ ---
53
+
54
+ ## Adapter support
55
+
56
+ Hook behavior is adapter-dependent:
57
+
58
+ | Adapter | Config file | Programmatic events |
59
+ |---------|-------------|---------------------|
60
+ | `claude-code` | `~/.claude/settings.json` | Stop, PreToolUse, PostToolUse, UserPromptSubmit, … |
61
+ | `cursor` | `.cursorrules` / MDC | None (no programmatic hook events) |
62
+
63
+ Check your adapter's supported events:
64
+
65
+ ```bash
66
+ node bin/vp-tools.cjs hooks scaffold --adapter claude-code
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Adding custom hooks
72
+
73
+ 1. Create your hook script in `~/.viepilot/hooks/` (or anywhere accessible)
74
+ 2. Get the config format: `vp-tools hooks scaffold`
75
+ 3. Add your command to the relevant event array in `~/.claude/settings.json`
76
+
77
+ Claude Code passes a JSON payload on stdin with `session_id`, `cwd`, `transcript_path`, and event-specific data. Your hook script should read from stdin and always exit 0.
78
+
79
+ ---
80
+
81
+ ## Troubleshooting
82
+
83
+ **Hook not firing**
84
+ - Confirm `~/.claude/settings.json` has a `Stop` entry: `vp-tools hooks install` (idempotent)
85
+ - Restart your Claude Code session after modifying `settings.json`
86
+
87
+ **No stale badges appearing**
88
+ - The hook only marks stale if session content matches architect keyword triggers
89
+ - Check that the brainstorm session notes file exists in `.viepilot/ui-direction/` or `docs/brainstorm/`
90
+ - The architect HTML files must exist at `templates/architect/` (repo) or `~/.claude/viepilot/templates/architect/` (installed)
91
+
92
+ **Hook error messages**
93
+ Hook errors appear on stderr (not in Claude's response). Check terminal output or Claude Code logs.
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ module.exports = {
7
+ id: 'antigravity',
8
+ name: 'Antigravity',
9
+ skillsDir: (home) => path.join(home, '.antigravity', 'skills'),
10
+ viepilotDir: (home) => path.join(home, '.antigravity', 'viepilot'),
11
+ // {envToolDir} in SKILL.md files resolves to this value at install time (ENH-035)
12
+ executionContextBase: '.antigravity/viepilot',
13
+ // Post-install hint shown in "Next actions" after viepilot install
14
+ postInstallHint: 'Open project and run /vp-status',
15
+ hooks: {
16
+ configFile: null, // Antigravity has no programmatic hooks system
17
+ schema: 'antigravity',
18
+ supportedEvents: []
19
+ },
20
+ installSubdirs: [
21
+ 'workflows',
22
+ path.join('templates', 'project'),
23
+ path.join('templates', 'phase'),
24
+ path.join('templates', 'architect'),
25
+ 'bin',
26
+ 'lib',
27
+ 'ui-components'
28
+ ],
29
+ isAvailable: (home) => {
30
+ const h = home || os.homedir();
31
+ return fs.existsSync(path.join(h, '.antigravity'));
32
+ }
33
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ module.exports = {
7
+ id: 'claude-code',
8
+ name: 'Claude Code',
9
+ // Paths (home-relative; resolved at install time)
10
+ skillsDir: (home) => path.join(home, '.claude', 'skills'),
11
+ viepilotDir: (home) => path.join(home, '.claude', 'viepilot'),
12
+ // execution_context base for skill .md files
13
+ executionContextBase: '.claude/viepilot',
14
+ // Post-install hint shown in "Next actions" after viepilot install
15
+ postInstallHint: 'Restart session so ~/.claude/skills/vp-* is picked up; then /vp-status',
16
+ // Hooks configuration (Claude Code settings.json)
17
+ hooks: {
18
+ configFile: (home) => path.join(home, '.claude', 'settings.json'),
19
+ schema: 'claude-code',
20
+ supportedEvents: [
21
+ 'SessionStart', 'SessionEnd', 'Stop', 'StopFailure',
22
+ 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
23
+ 'FileChanged', 'SubagentStart', 'SubagentStop',
24
+ 'TaskCreated', 'TaskCompleted', 'PreCompact', 'PostCompact'
25
+ ]
26
+ },
27
+ // Files/dirs to install into viepilotDir
28
+ installSubdirs: [
29
+ 'workflows',
30
+ path.join('templates', 'project'),
31
+ path.join('templates', 'phase'),
32
+ path.join('templates', 'architect'),
33
+ 'bin',
34
+ 'lib',
35
+ 'ui-components'
36
+ ],
37
+ // Detection: is this platform available on the current machine?
38
+ isAvailable: (home) => {
39
+ const h = home || os.homedir();
40
+ return fs.existsSync(path.join(h, '.claude'));
41
+ }
42
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ module.exports = {
7
+ id: 'codex',
8
+ name: 'Codex',
9
+ skillsDir: (home) => path.join(home, '.codex', 'skills'),
10
+ viepilotDir: (home) => path.join(home, '.codex', 'viepilot'),
11
+ // {envToolDir} in SKILL.md files resolves to this value at install time (ENH-035)
12
+ executionContextBase: '.codex/viepilot',
13
+ // NOTE: Codex uses $skill-name syntax (not /skill-name like other adapters)
14
+ // Codex reserves /command for built-in system controls (/plan, /clear, /diff, etc.)
15
+ postInstallHint: 'Open project and type $vp-status to get started',
16
+ hooks: {
17
+ configFile: null, // Codex uses AGENTS.md convention, not programmatic hooks
18
+ schema: 'codex',
19
+ supportedEvents: []
20
+ },
21
+ installSubdirs: [
22
+ 'workflows',
23
+ path.join('templates', 'project'),
24
+ path.join('templates', 'phase'),
25
+ path.join('templates', 'architect'),
26
+ 'bin',
27
+ 'lib',
28
+ 'ui-components'
29
+ ],
30
+ isAvailable: (home) => {
31
+ const h = home || os.homedir();
32
+ return fs.existsSync(path.join(h, '.codex'));
33
+ }
34
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ module.exports = {
7
+ id: 'cursor', // maps to cursor-agent and cursor-ide (same paths)
8
+ name: 'Cursor',
9
+ skillsDir: (home) => path.join(home, '.cursor', 'skills'),
10
+ viepilotDir: (home) => path.join(home, '.cursor', 'viepilot'),
11
+ executionContextBase: '.cursor/viepilot',
12
+ // Post-install hint shown in "Next actions" after viepilot install
13
+ postInstallHint: 'Open project and run /vp-status',
14
+ hooks: {
15
+ configFile: null, // Cursor uses .cursorrules/MDC, not settings.json hooks
16
+ schema: 'cursor',
17
+ supportedEvents: [] // no programmatic hook events
18
+ },
19
+ installSubdirs: [
20
+ 'workflows',
21
+ path.join('templates', 'project'),
22
+ path.join('templates', 'phase'),
23
+ path.join('templates', 'architect'),
24
+ 'bin',
25
+ 'lib',
26
+ 'ui-components'
27
+ ],
28
+ isAvailable: (home) => {
29
+ const h = home || os.homedir();
30
+ return fs.existsSync(path.join(h, '.cursor'));
31
+ }
32
+ };
@@ -0,0 +1,28 @@
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
+ 'antigravity': require('./antigravity.cjs'),
8
+ 'codex': require('./codex.cjs'),
9
+ };
10
+
11
+ /**
12
+ * Get adapter by id. Throws if unknown.
13
+ * @param {string} id
14
+ */
15
+ function getAdapter(id) {
16
+ const a = adapters[id];
17
+ if (!a) throw new Error(`Unknown adapter: "${id}". Known: ${Object.keys(adapters).join(', ')}`);
18
+ return a;
19
+ }
20
+
21
+ /**
22
+ * List unique adapters (deduplicated — aliases share the same object).
23
+ */
24
+ function listAdapters() {
25
+ return [...new Set(Object.values(adapters))];
26
+ }
27
+
28
+ 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
+ };