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.
- package/CHANGELOG.md +191 -0
- package/README.md +27 -17
- package/bin/viepilot.cjs +19 -9
- package/bin/vp-tools.cjs +193 -0
- package/docs/user/features/adapters.md +74 -0
- package/docs/user/features/hooks.md +93 -0
- package/lib/adapters/antigravity.cjs +33 -0
- package/lib/adapters/claude-code.cjs +42 -0
- package/lib/adapters/codex.cjs +34 -0
- package/lib/adapters/cursor.cjs +32 -0
- package/lib/adapters/index.cjs +28 -0
- package/lib/hooks/brainstorm-staleness.cjs +231 -0
- package/lib/viepilot-config.cjs +103 -0
- package/lib/viepilot-install.cjs +125 -152
- package/package.json +1 -3
- package/skills/vp-audit/SKILL.md +23 -23
- package/skills/vp-auto/SKILL.md +23 -9
- package/skills/vp-brainstorm/SKILL.md +44 -38
- package/skills/vp-crystallize/SKILL.md +25 -19
- package/skills/vp-debug/SKILL.md +4 -4
- package/skills/vp-docs/SKILL.md +8 -8
- package/skills/vp-evolve/SKILL.md +26 -13
- package/skills/vp-info/SKILL.md +24 -24
- package/skills/vp-pause/SKILL.md +7 -7
- package/skills/vp-request/SKILL.md +14 -14
- package/skills/vp-resume/SKILL.md +6 -6
- package/skills/vp-rollback/SKILL.md +4 -4
- package/skills/vp-status/SKILL.md +4 -4
- package/skills/vp-task/SKILL.md +2 -2
- package/skills/vp-ui-components/SKILL.md +14 -14
- package/skills/vp-update/SKILL.md +18 -18
- 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 +70 -5
- package/workflows/brainstorm.md +398 -222
- package/workflows/crystallize.md +51 -33
- package/workflows/debug.md +9 -9
- package/workflows/documentation.md +5 -5
- package/workflows/evolve.md +46 -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
- package/dev-install.sh +0 -150
- 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, '"').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
|
+
};
|