@avesta-hq/prevention 0.1.0 → 0.2.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/README.md CHANGED
@@ -8,7 +8,7 @@ Build the product right. Ship value fast. Learn continuously.
8
8
 
9
9
  ## What is Prevention?
10
10
 
11
- Prevention turns [Claude Code](https://claude.ai/claude-code) into a disciplined engineering partner. Instead of just generating code, it enforces **TDD, Clean Architecture, ATDD, and Continuous Delivery** — the practices that separate elite teams from the rest.
11
+ Prevention turns [Claude Code](https://claude.com/product/claude-code) into a disciplined engineering partner. Instead of just generating code, it enforces **TDD, Clean Architecture, ATDD, and Continuous Delivery** — the practices that separate elite teams from the rest.
12
12
 
13
13
  One command to set up. Slash commands to drive the workflow. Gates to keep you honest.
14
14
 
@@ -37,9 +37,9 @@ Each phase has gates that ensure prerequisites are met before you move forward.
37
37
 
38
38
  ---
39
39
 
40
- ## Commands
40
+ ## Agents
41
41
 
42
- 27 slash commands organized by workflow phase:
42
+ 27 specialized agents organized by workflow phase:
43
43
 
44
44
  | Phase | Command | Purpose |
45
45
  | -------------------- | --------------------------------- | ---------------------------------------------------- |
@@ -77,13 +77,13 @@ Each phase has gates that ensure prerequisites are met before you move forward.
77
77
 
78
78
  | Feature | Free | Pro |
79
79
  | -------------------- | -------------------------------------------------- | -------------------- |
80
- | **Core TDD** | `/avesta-red`, `/avesta-green`, `/avesta-refactor` | All 27 commands |
81
- | **Setup** | `/avesta-init`, `/avesta-scaffold`, `/avesta-help` | All 28 agents |
80
+ | **Core TDD** | `/avesta-red`, `/avesta-green`, `/avesta-refactor` | All 27 agents |
81
+ | **Setup** | `/avesta-init`, `/avesta-scaffold`, `/avesta-help` | All 27 agents |
82
82
  | **Skills** | - | 27 contextual skills |
83
83
  | **Gate enforcement** | - | Automated gates |
84
84
  | **Projects** | 1 | Unlimited |
85
85
 
86
- Free tier gives you the core TDD loop. Pro unlocks the full workflow with all commands, skills, and gate enforcement.
86
+ Free tier gives you the core TDD loop. Pro unlocks the full workflow with all agents, skills, and gate enforcement.
87
87
 
88
88
  ---
89
89
 
@@ -109,5 +109,5 @@ Then restart Claude Code.
109
109
  ## Requirements
110
110
 
111
111
  - Node.js >= 18
112
- - [Claude Code](https://claude.ai/claude-code) CLI
112
+ - [Claude Code](https://claude.com/product/claude-code) CLI
113
113
 
package/bin/cli.js CHANGED
@@ -1,492 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync } = require('child_process');
6
-
7
3
  const args = process.argv.slice(2);
8
4
  const command = args[0];
9
5
 
10
- const COMMANDS_DIR = path.join(__dirname, '..', '.claude', 'commands');
11
- const AGENTS_DIR = path.join(__dirname, '..', '.claude', 'agents');
12
- const AVESTA_DIR = path.join(__dirname, '..', '.avesta');
13
- const CLAUDE_MD = path.join(__dirname, '..', 'CLAUDE.md');
14
-
15
6
  const GITHUB_RELEASES_REPO = 'avesta-hq/prevention-releases';
16
7
 
17
- const PREVENTION_SECTION_START = '<!-- prevention:start -->';
18
- const PREVENTION_SECTION_END = '<!-- prevention:end -->';
19
-
20
- const PREVENTION_CLAUDE_MD_SECTION = `${PREVENTION_SECTION_START}
21
- ## Prevention — MCP Server Integration
22
-
23
- This project uses [Prevention](https://github.com/avesta-hq/prevention) as an MCP server for XP/CD workflow enforcement.
24
-
25
- **When starting work:**
26
-
27
- 1. Run \`/avesta-help\` to see current status and next steps
28
- 2. The workflow tracks your phase (vision → plan → atdd → tdd → review → ship)
29
- 3. Gates enforce that prerequisites are met before advancing
30
-
31
- **Key commands:**
32
-
33
- - \`/avesta-plan <feature>\` — Break feature into TDD-ready tasks
34
- - \`/avesta-red <behavior>\` — Write ONE failing test
35
- - \`/avesta-green\` — Minimal code to make it pass
36
- - \`/avesta-refactor\` — Improve structure (tests must stay green)
37
- - \`/avesta-code-review\` — Domain-specific code review
38
- - \`/avesta-commit\` — Conventional commit
39
- - \`/avesta-ship\` — Merge to main
40
-
41
- Run \`/avesta-help\` for the full command list.
42
- ${PREVENTION_SECTION_END}`;
43
-
44
- function ensureClaudeMdSection(targetDir) {
45
- const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
46
-
47
- // Case 1: No CLAUDE.md — create with Prevention section
48
- if (!fs.existsSync(claudeMdPath)) {
49
- fs.writeFileSync(claudeMdPath, PREVENTION_CLAUDE_MD_SECTION + '\n', 'utf-8');
50
- console.log(' ✓ Created CLAUDE.md with Prevention section');
51
- return;
52
- }
53
-
54
- const content = fs.readFileSync(claudeMdPath, 'utf-8');
55
-
56
- // Case 2: CLAUDE.md exists with Prevention section — replace it (update)
57
- if (content.includes(PREVENTION_SECTION_START)) {
58
- const regex = new RegExp(
59
- escapeRegExp(PREVENTION_SECTION_START) + '[\\s\\S]*?' + escapeRegExp(PREVENTION_SECTION_END),
60
- 'm'
61
- );
62
- const updated = content.replace(regex, PREVENTION_CLAUDE_MD_SECTION);
63
- fs.writeFileSync(claudeMdPath, updated, 'utf-8');
64
- console.log(' ✓ Updated Prevention section in CLAUDE.md');
65
- return;
66
- }
67
-
68
- // Case 3: CLAUDE.md exists without Prevention section — append
69
- const separator = content.endsWith('\n') ? '\n' : '\n\n';
70
- fs.writeFileSync(claudeMdPath, content + separator + PREVENTION_CLAUDE_MD_SECTION + '\n', 'utf-8');
71
- console.log(' ✓ Added Prevention section to CLAUDE.md');
72
- }
73
-
74
- function escapeRegExp(str) {
75
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
76
- }
77
-
78
- function copyDir(src, dest) {
79
- if (!fs.existsSync(dest)) {
80
- fs.mkdirSync(dest, { recursive: true });
81
- }
82
-
83
- const entries = fs.readdirSync(src, { withFileTypes: true });
84
-
85
- for (const entry of entries) {
86
- const srcPath = path.join(src, entry.name);
87
- const destPath = path.join(dest, entry.name);
88
-
89
- if (entry.isDirectory()) {
90
- copyDir(srcPath, destPath);
91
- } else {
92
- fs.copyFileSync(srcPath, destPath);
93
- }
94
- }
95
- }
96
-
97
- function getPlatformBinaryName() {
98
- const platform = process.platform;
99
- const arch = process.arch;
100
-
101
- if (platform === 'linux' && arch === 'x64') return 'prevention-linux-x64';
102
- if (platform === 'darwin' && arch === 'arm64') return 'prevention-darwin-arm64';
103
- if (platform === 'darwin' && arch === 'x64') return 'prevention-darwin-x64';
104
-
105
- return null;
106
- }
107
-
108
- function getLatestVersion() {
109
- try {
110
- const result = execSync(
111
- `curl -sI "https://github.com/${GITHUB_RELEASES_REPO}/releases/latest" | grep -i ^location:`,
112
- { encoding: 'utf8', timeout: 10000 }
113
- );
114
- const match = result.match(/\/tag\/(v[^\s\r\n]+)/);
115
- return match ? match[1] : null;
116
- } catch {
117
- return null;
118
- }
119
- }
120
-
121
- function downloadBinary(targetDir) {
122
- const binaryName = getPlatformBinaryName();
123
- if (!binaryName) {
124
- console.error(` ⚠ Unsupported platform: ${process.platform}-${process.arch}`);
125
- console.error(` Supported: linux-x64, darwin-arm64, darwin-x64`);
126
- return null;
127
- }
128
-
129
- const binDir = path.join(targetDir, '.avesta', 'bin');
130
- const binaryPath = path.join(binDir, 'prevention');
131
-
132
- const version = getLatestVersion();
133
- if (!version) {
134
- console.error(' ⚠ Could not determine latest release version');
135
- return null;
136
- }
137
-
138
- const url = `https://github.com/${GITHUB_RELEASES_REPO}/releases/download/${version}/${binaryName}`;
139
-
140
- if (!fs.existsSync(binDir)) {
141
- fs.mkdirSync(binDir, { recursive: true });
142
- }
143
-
144
- try {
145
- console.log(` Downloading MCP server (${version}, ${process.platform}-${process.arch})...`);
146
- execSync(`curl -sL "${url}" -o "${binaryPath}"`, { timeout: 60000 });
147
- fs.chmodSync(binaryPath, 0o755);
148
- return binaryPath;
149
- } catch (e) {
150
- console.error(` ⚠ Failed to download binary from ${url}`);
151
- return null;
152
- }
153
- }
154
-
155
- function isInsideNodeModules() {
156
- return __dirname.includes('node_modules');
157
- }
158
-
159
- function findSessionStartCommand() {
160
- if (isInsideNodeModules()) {
161
- return 'npx @avesta-hq/prevention session-start';
162
- }
163
- return `node ${path.resolve(__dirname, 'cli.js')} session-start`;
164
- }
165
-
166
- function configureSessionStartHook(targetDir) {
167
- const claudeDir = path.join(targetDir, '.claude');
168
- const settingsPath = path.join(claudeDir, 'settings.json');
169
-
170
- if (!fs.existsSync(claudeDir)) {
171
- fs.mkdirSync(claudeDir, { recursive: true });
172
- }
173
-
174
- let settings = {};
175
- if (fs.existsSync(settingsPath)) {
176
- try {
177
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
178
- } catch {
179
- settings = {};
180
- }
181
- }
182
-
183
- if (!settings.hooks) {
184
- settings.hooks = {};
185
- }
186
-
187
- const command = findSessionStartCommand();
188
-
189
- const existingHooks = settings.hooks.SessionStart || [];
190
- const alreadyConfigured = existingHooks.some(entry =>
191
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('prevention session-start'))
192
- );
193
-
194
- if (!alreadyConfigured) {
195
- settings.hooks.SessionStart = [
196
- ...existingHooks,
197
- {
198
- matcher: '',
199
- hooks: [{ type: 'command', command }],
200
- },
201
- ];
202
- }
203
-
204
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
205
- return true;
206
- }
207
-
208
- function configureStatusLine(targetDir) {
209
- const claudeDir = path.join(targetDir, '.claude');
210
- const settingsPath = path.join(claudeDir, 'settings.json');
211
- const statusLineSrc = path.join(targetDir, '.avesta', 'statusLine.js');
212
-
213
- if (!fs.existsSync(statusLineSrc)) return;
214
-
215
- let settings = {};
216
- if (fs.existsSync(settingsPath)) {
217
- try {
218
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
219
- } catch {
220
- settings = {};
221
- }
222
- }
223
-
224
- // Always set/update the status line to point to our script
225
- settings.statusLine = {
226
- type: 'command',
227
- command: `node ${statusLineSrc}`,
228
- };
229
-
230
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
231
- }
232
-
233
- function configureMcpServer(targetDir, binaryPath) {
234
- const mcpJsonPath = path.join(targetDir, '.mcp.json');
235
-
236
- let mcpConfig = {};
237
- if (fs.existsSync(mcpJsonPath)) {
238
- try {
239
- mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
240
- } catch {
241
- mcpConfig = {};
242
- }
243
- }
244
-
245
- if (!mcpConfig.mcpServers) {
246
- mcpConfig.mcpServers = {};
247
- }
248
-
249
- if (binaryPath) {
250
- mcpConfig.mcpServers['prevention'] = { command: path.resolve(binaryPath) };
251
- } else {
252
- // Fallback: use npx serve (runs from source in dev)
253
- mcpConfig.mcpServers['prevention'] = {
254
- command: 'npx',
255
- args: ['@avesta-hq/prevention', 'serve'],
256
- };
257
- }
258
-
259
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
260
- return true;
261
- }
262
-
263
- // ── Session Start Hook ──────────────────────────────────────────────
264
- // Outputs {"additionalContext": "<banner>"} for Claude Code SessionStart hook.
265
- // Fail-open: always exits 0, never blocks the session.
266
-
267
- function syncFiles() {
268
- const cwd = process.cwd();
269
- const claudeDir = path.join(cwd, '.claude');
270
-
271
- if (!fs.existsSync(claudeDir)) return;
272
- const mcpJsonPath = path.join(cwd, '.mcp.json');
273
- if (!fs.existsSync(mcpJsonPath)) return;
274
- try {
275
- const cfg = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
276
- if (!cfg?.mcpServers?.['prevention']) return;
277
- } catch { return; }
278
-
279
- if (fs.existsSync(COMMANDS_DIR)) copyDir(COMMANDS_DIR, path.join(claudeDir, 'commands'));
280
- if (fs.existsSync(AGENTS_DIR)) copyDir(AGENTS_DIR, path.join(claudeDir, 'agents'));
281
- }
282
-
283
- function sessionStart() {
284
- try {
285
- syncFiles();
286
- const cwd = process.cwd();
287
- const statePath = path.join(cwd, '.avesta', 'workflow-state.json');
288
-
289
- if (!fs.existsSync(statePath)) {
290
- const banner = generateWelcomeBanner();
291
- process.stdout.write(JSON.stringify({ additionalContext: banner }));
292
- process.exit(0);
293
- }
294
-
295
- const raw = fs.readFileSync(statePath, 'utf-8');
296
- const state = JSON.parse(raw);
297
- const banner = generateStatusBanner(state);
298
- process.stdout.write(JSON.stringify({ additionalContext: banner }));
299
- process.exit(0);
300
- } catch {
301
- process.exit(0);
302
- }
303
- }
304
-
305
- function getVersion() {
306
- try {
307
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
308
- return pkg.version || '0.0.0';
309
- } catch {
310
- return '0.0.0';
311
- }
312
- }
313
-
314
- function getLicenseTier() {
315
- try {
316
- const homeDir = require('os').homedir();
317
- const cachePath = path.join(homeDir, '.avesta', 'license-cache.json');
318
- if (fs.existsSync(cachePath)) {
319
- const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
320
- if (cache.tier && cache.tier !== 'free') return cache.tier;
321
- }
322
- } catch {}
323
- return 'free';
324
- }
325
-
326
- const BOX_WIDTH = 57; // inner width between │ and │
327
-
328
- function boxLine(text) {
329
- // Account for Unicode characters that take visual space but have different string lengths
330
- // Common multi-byte: →(1 visual, 3 bytes), ◆(1 visual, 3 bytes), █░✓○(each 1 visual, 3 bytes)
331
- const visualLen = [...text].reduce((len, ch) => {
332
- const code = ch.codePointAt(0);
333
- // Most Unicode symbols above basic ASCII are single-width visually
334
- return len + 1;
335
- }, 0);
336
- const padding = Math.max(0, BOX_WIDTH - visualLen);
337
- return `│ ${text}${' '.repeat(padding)} │`;
338
- }
339
-
340
- function boxTop() { return '┌' + '─'.repeat(BOX_WIDTH + 2) + '┐'; }
341
- function boxMid() { return '├' + '─'.repeat(BOX_WIDTH + 2) + '┤'; }
342
- function boxBot() { return '└' + '─'.repeat(BOX_WIDTH + 2) + '┘'; }
343
- function boxEmpty() { return boxLine(''); }
344
-
345
- function generateWelcomeBanner() {
346
- const version = getVersion();
347
- const tier = getLicenseTier();
348
-
349
- return [
350
- '',
351
- boxTop(),
352
- boxEmpty(),
353
- boxLine(' ◆ A V E S T A H Q P R E V E N T I O N'),
354
- boxLine(' Continuous Delivery as a Learning System'),
355
- boxEmpty(),
356
- boxLine(` v${version} · ${tier.toUpperCase()} tier · AvestaHQ`),
357
- boxEmpty(),
358
- boxMid(),
359
- boxEmpty(),
360
- boxLine(' Get started:'),
361
- boxLine(' /avesta-init [backend|frontend] Init workflow'),
362
- boxLine(' /avesta-scaffold New project'),
363
- boxLine(' /avesta-help All commands'),
364
- boxEmpty(),
365
- boxLine(' Workflow:'),
366
- boxLine(' vision -> plan -> atdd -> tdd -> review -> ship'),
367
- boxEmpty(),
368
- boxBot(),
369
- '',
370
- ].join('\n');
371
- }
372
-
373
- function generateStatusBanner(state) {
374
- const phase = state.current_phase || 'idle';
375
- const feature = state.feature || '(none)';
376
- const gates = state.gates || {};
377
- const changeType = state.change_type || 'feature';
378
- const tddState = state.tdd_state;
379
- const mode = state.mode || 'normal';
380
-
381
- const GATE_ORDER = [
382
- 'vision_approved',
383
- 'plan_approved',
384
- 'atdd_approved',
385
- 'characterization_complete',
386
- 'tdd_complete',
387
- 'driver_complete',
388
- 'review_approved',
389
- ];
390
-
391
- const GATE_LABELS = {
392
- vision_approved: 'Vision',
393
- plan_approved: 'Plan',
394
- atdd_approved: 'ATDD',
395
- characterization_complete: 'Characterization',
396
- tdd_complete: 'TDD',
397
- driver_complete: 'Driver',
398
- review_approved: 'Review',
399
- };
400
-
401
- const CHANGE_TYPE_GATES = {
402
- feature: GATE_ORDER,
403
- bug_fix: ['plan_approved', 'tdd_complete', 'review_approved'],
404
- enhancement: ['plan_approved', 'atdd_approved', 'tdd_complete', 'driver_complete', 'review_approved'],
405
- tech_debt: ['plan_approved', 'characterization_complete', 'tdd_complete', 'review_approved'],
406
- };
407
- const requiredGates = CHANGE_TYPE_GATES[changeType] || GATE_ORDER;
408
-
409
- let passedCount = 0;
410
- const gateParts = [];
411
- for (const g of requiredGates) {
412
- const passed = gates[g] === true;
413
- if (passed) passedCount++;
414
- const icon = passed ? '✓' : '○';
415
- gateParts.push(`${icon} ${GATE_LABELS[g] || g}`);
416
- }
417
- const totalGates = requiredGates.length;
418
-
419
- const GATE_NEXT_ACTIONS = {
420
- vision_approved: 'Define product vision -> /avesta-vision',
421
- plan_approved: 'Create feature plan -> /avesta-plan',
422
- atdd_approved: 'Write acceptance tests -> /avesta-acceptance-test',
423
- tdd_complete: 'Complete TDD cycles -> /avesta-red, /avesta-green, /avesta-refactor or /avesta-layer (all-tests-first per layer)',
424
- driver_complete: 'Implement protocol drivers -> /avesta-driver',
425
- review_approved: 'Code review -> /avesta-code-review',
426
- };
427
-
428
- let nextAction = 'Ship the feature -> /avesta-ship';
429
- for (const g of requiredGates) {
430
- if (gates[g] !== true) {
431
- nextAction = GATE_NEXT_ACTIONS[g] || `Complete ${g}`;
432
- break;
433
- }
434
- }
435
-
436
- const PHASE_COMMANDS = {
437
- idle: '/avesta-vision /avesta-plan /avesta-bug-fix /avesta-enhance /avesta-tech-debt /avesta-help',
438
- vision: '/avesta-vision /avesta-plan /avesta-help',
439
- plan: '/avesta-plan /avesta-acceptance-test /avesta-help',
440
- atdd: '/avesta-acceptance-test /avesta-dsl /avesta-driver /avesta-help',
441
- tdd: '/avesta-red /avesta-green /avesta-refactor /avesta-cycle /avesta-layer /avesta-help',
442
- driver: '/avesta-driver /avesta-help',
443
- review: '/avesta-code-review /avesta-ship /avesta-help',
444
- ship: '/avesta-ship /avesta-commit /avesta-help',
445
- };
446
- const commands = PHASE_COMMANDS[phase] || '/avesta-help';
447
-
448
- const version = getVersion();
449
- const tier = getLicenseTier();
450
-
451
- // Progress bar
452
- const progressPct = totalGates > 0 ? Math.round((passedCount / totalGates) * 100) : 0;
453
- const barTotal = 20;
454
- const barFilled = Math.round((passedCount / totalGates) * barTotal);
455
- const progressBar = '#'.repeat(barFilled) + '-'.repeat(barTotal - barFilled);
456
-
457
- // Phase indicator
458
- const PHASES = ['vision', 'plan', 'atdd', 'tdd', 'driver', 'review', 'ship'];
459
- const phaseDisplay = PHASES.map(p => p === phase ? `[${p.toUpperCase()}]` : p).join(' > ');
460
-
461
- // Tags
462
- const tags = [];
463
- if (mode === 'spike') tags.push('SPIKE');
464
- if (changeType && changeType !== 'feature') tags.push(changeType.toUpperCase().replace('_', ' '));
465
- if (phase === 'tdd' && tddState) tags.push(`${tddState.cycle.toUpperCase()} (${tddState.test_count} tests)`);
466
- const tagLine = tags.length > 0 ? ` ${tags.map(t => `[${t}]`).join(' ')}` : '';
467
-
468
- // Gate icons
469
- const gateIcons = requiredGates.map(g => gates[g] === true ? '[x]' : '[ ]').join(' ');
470
-
471
- return [
472
- '',
473
- boxTop(),
474
- boxLine(' ◆ A V E S T A H Q P R E V E N T I O N'),
475
- boxLine(` v${version} · ${tier.toUpperCase()} tier · AvestaHQ`),
476
- boxMid(),
477
- boxLine(` Feature: ${feature.substring(0, 44)}`),
478
- boxLine(` Phase: ${phase.toUpperCase()}${tagLine}`),
479
- boxLine(` Progress: [${progressBar}] ${progressPct}%`),
480
- boxLine(` Gates: ${passedCount}/${totalGates} ${gateIcons}`),
481
- boxMid(),
482
- boxLine(` ${phaseDisplay}`),
483
- boxMid(),
484
- boxLine(` Next: ${nextAction.substring(0, 48)}`),
485
- boxBot(),
486
- '',
487
- ].join('\n');
488
- }
489
-
490
8
  function printHelp() {
491
9
  console.log(`
492
10
  Avesta Prevention - XP/CD Development Commands for Claude Code
@@ -495,9 +13,11 @@ Usage:
495
13
  npx @avesta-hq/prevention <command>
496
14
 
497
15
  Commands:
498
- init Full setup: commands, agents, MCP server, CLAUDE.md
16
+ init Full setup: commands, agents, MCP server, hooks, CLAUDE.md
499
17
  serve Start the MCP server (used by Claude Code)
500
18
  session-start Output session context (used by SessionStart hook)
19
+ hook pre-tool-use Workflow enforcement hook (blocks unauthorized edits)
20
+ hook prompt-submit Context injection hook (injects workflow state)
501
21
  update Update MCP server binary to latest version
502
22
 
503
23
  Examples:
@@ -505,7 +25,7 @@ Examples:
505
25
  npx @avesta-hq/prevention init
506
26
 
507
27
  After initialization:
508
- 1. Start Claude Code in your project
28
+ 1. Start Claude Code in this project
509
29
  2. Run /avesta-help to see status and available commands
510
30
  3. Run /avesta-init backend to initialize workflow tracking
511
31
  4. Run /avesta-scaffold to scaffold project structure (greenfield)
@@ -515,120 +35,31 @@ Documentation:
515
35
  `);
516
36
  }
517
37
 
518
- function init() {
519
- const targetDir = process.cwd();
520
- const claudeDir = path.join(targetDir, '.claude');
521
- const commandsTarget = path.join(claudeDir, 'commands');
522
-
523
- // Create .claude directory
524
- if (!fs.existsSync(claudeDir)) {
525
- fs.mkdirSync(claudeDir, { recursive: true });
526
- }
527
-
528
- // Copy commands
529
- if (fs.existsSync(COMMANDS_DIR)) {
530
- copyDir(COMMANDS_DIR, commandsTarget);
531
- } else {
532
- console.error('✗ Commands directory not found');
533
- process.exit(1);
534
- }
535
-
536
- // Copy agents
537
- if (fs.existsSync(AGENTS_DIR)) {
538
- const agentsTarget = path.join(claudeDir, 'agents');
539
- copyDir(AGENTS_DIR, agentsTarget);
540
- }
541
-
542
- // Copy .avesta directory (workflow state, schemas)
543
- if (fs.existsSync(AVESTA_DIR)) {
544
- const cdAgentTarget = path.join(targetDir, '.avesta');
545
- copyDir(AVESTA_DIR, cdAgentTarget);
546
- }
547
-
548
- // Download MCP server binary and configure
549
- const binaryPath = downloadBinary(targetDir);
550
- configureMcpServer(targetDir, binaryPath);
551
- configureSessionStartHook(targetDir);
552
- configureStatusLine(targetDir);
553
-
554
- // Manage CLAUDE.md Prevention section
555
- ensureClaudeMdSection(targetDir);
556
-
557
- const commandCount = fs.existsSync(COMMANDS_DIR) ? fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')).length : 0;
558
- const agentCount = fs.existsSync(AGENTS_DIR) ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md')).length : 0;
559
- const mcpStatus = binaryPath ? 'MCP server' : 'MCP server (download failed — run npx @avesta-hq/prevention update to retry)';
560
-
561
- console.log(`
562
- ✅ Avesta Prevention ready!
563
-
564
- Installed: ${commandCount} commands · ${agentCount} agents · ${mcpStatus}
565
-
566
- Next steps:
567
- 1. Open Claude Code in this project
568
- 2. Run /avesta-help to see available commands
569
- 3. Run /avesta-vision to define what you're building
570
-
571
- Docs: https://github.com/${GITHUB_RELEASES_REPO}
572
- `);
573
- }
574
-
575
- function update() {
576
- const targetDir = process.cwd();
577
- const binaryPath = downloadBinary(targetDir);
578
- if (binaryPath) {
579
- configureMcpServer(targetDir, binaryPath);
580
- console.log('\n ✅ MCP server updated successfully!\n');
581
- } else {
582
- console.error('\n ✗ Update failed. Check your internet connection and try again.\n');
583
- process.exit(1);
584
- }
585
- }
586
-
587
- function serve() {
588
- const packageRoot = path.join(__dirname, '..');
589
-
590
- // Try compiled binary first (local dev)
591
- const binaryPath = path.join(packageRoot, 'dist', 'prevention');
592
- if (fs.existsSync(binaryPath)) {
593
- const { execFileSync } = require('child_process');
594
- try {
595
- execFileSync(binaryPath, { stdio: 'inherit' });
596
- } catch (e) {
597
- process.exit(e.status || 1);
598
- }
599
- return;
600
- }
601
-
602
- // Fall back to source via tsx (local dev)
603
- const mcpServerPath = path.join(packageRoot, 'src', 'mcp-server.ts');
604
- if (fs.existsSync(mcpServerPath)) {
605
- const { execFileSync } = require('child_process');
606
- try {
607
- execFileSync('npx', ['tsx', mcpServerPath], { stdio: 'inherit' });
608
- } catch (e) {
609
- process.exit(e.status || 1);
610
- }
611
- return;
612
- }
613
-
614
- console.error('✗ No MCP server binary or source found');
615
- console.error(' Run: npx @avesta-hq/prevention update');
616
- process.exit(1);
617
- }
618
-
619
- // Main
620
38
  switch (command) {
621
39
  case 'init':
622
- init();
40
+ require('./lib/init').init();
623
41
  break;
624
42
  case 'update':
625
- update();
43
+ require('./lib/init').update();
626
44
  break;
627
45
  case 'serve':
628
- serve();
46
+ require('./lib/init').serve();
629
47
  break;
630
48
  case 'session-start':
631
- sessionStart();
49
+ require('./lib/session-start').sessionStart();
50
+ break;
51
+ case 'hook':
52
+ switch (args[1]) {
53
+ case 'pre-tool-use':
54
+ require('./lib/hooks').hookPreToolUse();
55
+ break;
56
+ case 'prompt-submit':
57
+ require('./lib/hooks').hookPromptSubmit();
58
+ break;
59
+ default:
60
+ console.error(`Unknown hook: ${args[1]}`);
61
+ process.exit(1);
62
+ }
632
63
  break;
633
64
  case 'help':
634
65
  case '--help':
@@ -0,0 +1,132 @@
1
+ const { getVersion, getLicenseTier } = require('./utils');
2
+
3
+ const BOX_WIDTH = 57;
4
+
5
+ function boxLine(text) {
6
+ const visualLen = [...text].length;
7
+ const padding = Math.max(0, BOX_WIDTH - visualLen);
8
+ return `│ ${text}${' '.repeat(padding)} │`;
9
+ }
10
+
11
+ function boxTop() { return '┌' + '─'.repeat(BOX_WIDTH + 2) + '┐'; }
12
+ function boxMid() { return '├' + '─'.repeat(BOX_WIDTH + 2) + '┤'; }
13
+ function boxBot() { return '└' + '─'.repeat(BOX_WIDTH + 2) + '┘'; }
14
+ function boxEmpty() { return boxLine(''); }
15
+
16
+ function generateWelcomeBanner() {
17
+ const version = getVersion();
18
+ const tier = getLicenseTier();
19
+
20
+ return [
21
+ '',
22
+ boxTop(),
23
+ boxEmpty(),
24
+ boxLine(' ◆ A V E S T A H Q P R E V E N T I O N'),
25
+ boxLine(' Continuous Delivery as a Learning System'),
26
+ boxEmpty(),
27
+ boxLine(` v${version} · ${tier.toUpperCase()} tier · AvestaHQ`),
28
+ boxEmpty(),
29
+ boxMid(),
30
+ boxEmpty(),
31
+ boxLine(' Get started:'),
32
+ boxLine(' /avesta-init [backend|frontend] Init workflow'),
33
+ boxLine(' /avesta-scaffold New project'),
34
+ boxLine(' /avesta-help All commands'),
35
+ boxEmpty(),
36
+ boxLine(' Workflow:'),
37
+ boxLine(' vision -> plan -> atdd -> tdd -> review -> ship'),
38
+ boxEmpty(),
39
+ boxBot(),
40
+ '',
41
+ ].join('\n');
42
+ }
43
+
44
+ function generateStatusBanner(state) {
45
+ const phase = state.current_phase || 'idle';
46
+ const feature = state.feature || '(none)';
47
+ const gates = state.gates || {};
48
+ const changeType = state.change_type || 'feature';
49
+ const tddState = state.tdd_state;
50
+ const mode = state.mode || 'normal';
51
+
52
+ const GATE_ORDER = [
53
+ 'vision_approved', 'plan_approved', 'atdd_approved',
54
+ 'characterization_complete', 'tdd_complete', 'driver_complete', 'review_approved',
55
+ ];
56
+
57
+ const GATE_LABELS = {
58
+ vision_approved: 'Vision', plan_approved: 'Plan', atdd_approved: 'ATDD',
59
+ characterization_complete: 'Characterization', tdd_complete: 'TDD',
60
+ driver_complete: 'Driver', review_approved: 'Review',
61
+ };
62
+
63
+ const CHANGE_TYPE_GATES = {
64
+ feature: GATE_ORDER,
65
+ bug_fix: ['plan_approved', 'tdd_complete', 'review_approved'],
66
+ enhancement: ['plan_approved', 'atdd_approved', 'tdd_complete', 'driver_complete', 'review_approved'],
67
+ tech_debt: ['plan_approved', 'characterization_complete', 'tdd_complete', 'review_approved'],
68
+ };
69
+
70
+ const requiredGates = CHANGE_TYPE_GATES[changeType] || GATE_ORDER;
71
+ let passedCount = 0;
72
+ for (const g of requiredGates) {
73
+ if (gates[g] === true) passedCount++;
74
+ }
75
+ const totalGates = requiredGates.length;
76
+
77
+ const GATE_NEXT_ACTIONS = {
78
+ vision_approved: 'Define product vision -> /avesta-vision',
79
+ plan_approved: 'Create feature plan -> /avesta-plan',
80
+ atdd_approved: 'Write acceptance tests -> /avesta-acceptance-test',
81
+ tdd_complete: 'Complete TDD cycles -> /avesta-red, /avesta-green, /avesta-refactor or /avesta-layer (all-tests-first per layer)',
82
+ driver_complete: 'Implement protocol drivers -> /avesta-driver',
83
+ review_approved: 'Code review -> /avesta-code-review',
84
+ };
85
+
86
+ let nextAction = 'Ship the feature -> /avesta-ship';
87
+ for (const g of requiredGates) {
88
+ if (gates[g] !== true) {
89
+ nextAction = GATE_NEXT_ACTIONS[g] || `Complete ${g}`;
90
+ break;
91
+ }
92
+ }
93
+
94
+ const version = getVersion();
95
+ const tier = getLicenseTier();
96
+
97
+ const progressPct = totalGates > 0 ? Math.round((passedCount / totalGates) * 100) : 0;
98
+ const barTotal = 20;
99
+ const barFilled = Math.round((passedCount / totalGates) * barTotal);
100
+ const progressBar = '#'.repeat(barFilled) + '-'.repeat(barTotal - barFilled);
101
+
102
+ const PHASES = ['vision', 'plan', 'atdd', 'tdd', 'driver', 'review', 'ship'];
103
+ const phaseDisplay = PHASES.map(p => p === phase ? `[${p.toUpperCase()}]` : p).join(' > ');
104
+
105
+ const tags = [];
106
+ if (mode === 'spike') tags.push('SPIKE');
107
+ if (changeType && changeType !== 'feature') tags.push(changeType.toUpperCase().replace('_', ' '));
108
+ if (phase === 'tdd' && tddState) tags.push(`${tddState.cycle.toUpperCase()} (${tddState.test_count} tests)`);
109
+ const tagLine = tags.length > 0 ? ` ${tags.map(t => `[${t}]`).join(' ')}` : '';
110
+
111
+ const gateIcons = requiredGates.map(g => gates[g] === true ? '[x]' : '[ ]').join(' ');
112
+
113
+ return [
114
+ '',
115
+ boxTop(),
116
+ boxLine(' ◆ A V E S T A H Q P R E V E N T I O N'),
117
+ boxLine(` v${version} · ${tier.toUpperCase()} tier · AvestaHQ`),
118
+ boxMid(),
119
+ boxLine(` Feature: ${feature.substring(0, 44)}`),
120
+ boxLine(` Phase: ${phase.toUpperCase()}${tagLine}`),
121
+ boxLine(` Progress: [${progressBar}] ${progressPct}%`),
122
+ boxLine(` Gates: ${passedCount}/${totalGates} ${gateIcons}`),
123
+ boxMid(),
124
+ boxLine(` ${phaseDisplay}`),
125
+ boxMid(),
126
+ boxLine(` Next: ${nextAction.substring(0, 48)}`),
127
+ boxBot(),
128
+ '',
129
+ ].join('\n');
130
+ }
131
+
132
+ module.exports = { generateWelcomeBanner, generateStatusBanner };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Hook Handlers
3
+ *
4
+ * Dumb enforcers that read the policy file generated by the MCP server.
5
+ * All business logic (phase rules, gate checks) lives in the MCP server's
6
+ * PolicyGenerator — hooks just apply the pre-computed allow/deny decisions.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function readPolicy(cwd) {
13
+ const policyPath = path.join(cwd, '.avesta', 'hook-policy.json');
14
+ if (!fs.existsSync(policyPath)) return null;
15
+ try {
16
+ return JSON.parse(fs.readFileSync(policyPath, 'utf-8'));
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function matchesPattern(filePath, pattern) {
23
+ // Convert glob-like pattern to regex
24
+ const regex = pattern
25
+ .replace(/\./g, '\\.')
26
+ .replace(/\*\*/g, '§§') // placeholder for **
27
+ .replace(/\*/g, '[^/]*') // * = anything except /
28
+ .replace(/§§/g, '.*'); // ** = anything including /
29
+ return new RegExp(regex).test(filePath);
30
+ }
31
+
32
+ // ── PreToolUse Hook ───────────────────────────────────────────────
33
+
34
+ function hookPreToolUse() {
35
+ try {
36
+ const data = JSON.parse(fs.readFileSync(0, 'utf8'));
37
+ const { tool_name: toolName, tool_input: toolInput = {}, cwd = process.cwd() } = data;
38
+
39
+ const policy = readPolicy(cwd);
40
+ if (!policy) process.exit(0); // No policy = fail-open
41
+
42
+ // Edit / Write enforcement
43
+ if (toolName === 'Edit' || toolName === 'Write') {
44
+ const editPolicy = policy.edit_policy;
45
+ if (!editPolicy || editPolicy.allow_all) process.exit(0);
46
+
47
+ const filePath = toolInput.file_path || '';
48
+ const allowed = editPolicy.allow_patterns.some(p => matchesPattern(filePath, p));
49
+
50
+ if (!allowed) {
51
+ process.stderr.write(editPolicy.block_message || '⛔ Prevention: Edit blocked by policy.\n');
52
+ process.exit(2);
53
+ }
54
+ process.exit(0);
55
+ }
56
+
57
+ // Bash enforcement
58
+ if (toolName === 'Bash') {
59
+ const bashPolicy = policy.bash_policy;
60
+ if (!bashPolicy || !bashPolicy.blocked_commands) process.exit(0);
61
+
62
+ const command = toolInput.command || '';
63
+ for (const rule of bashPolicy.blocked_commands) {
64
+ if (new RegExp(rule.pattern).test(command)) {
65
+ process.stderr.write(rule.message || '⛔ Prevention: Command blocked by policy.\n');
66
+ process.exit(2);
67
+ }
68
+ }
69
+ process.exit(0);
70
+ }
71
+
72
+ process.exit(0);
73
+ } catch {
74
+ process.exit(0); // Fail-open
75
+ }
76
+ }
77
+
78
+ // ── UserPromptSubmit Hook ─────────────────────────────────────────
79
+
80
+ function hookPromptSubmit() {
81
+ try {
82
+ const data = JSON.parse(fs.readFileSync(0, 'utf8'));
83
+ const cwd = data.cwd || process.cwd();
84
+
85
+ const policy = readPolicy(cwd);
86
+ if (!policy) {
87
+ process.stdout.write(JSON.stringify({
88
+ additionalContext:
89
+ '[Prevention] Workflow not initialized. ' +
90
+ 'Run /avesta-init to enable TDD/CD enforcement, or proceed without it.',
91
+ }));
92
+ process.exit(0);
93
+ }
94
+
95
+ if (policy.prompt_context) {
96
+ process.stdout.write(JSON.stringify({ additionalContext: policy.prompt_context }));
97
+ }
98
+ process.exit(0);
99
+ } catch {
100
+ process.exit(0);
101
+ }
102
+ }
103
+
104
+ module.exports = { hookPreToolUse, hookPromptSubmit };
@@ -0,0 +1,88 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { COMMANDS_DIR, AGENTS_DIR, AVESTA_DIR, GITHUB_RELEASES_REPO, copyDir, downloadBinary } = require('./utils');
4
+ const { configureMcpServer, configureSessionStartHook, configureEnforcementHooks, configureStatusLine, ensureClaudeMdSection } = require('./settings');
5
+
6
+ function init() {
7
+ const targetDir = process.cwd();
8
+ const claudeDir = path.join(targetDir, '.claude');
9
+
10
+ if (!fs.existsSync(claudeDir)) {
11
+ fs.mkdirSync(claudeDir, { recursive: true });
12
+ }
13
+
14
+ if (fs.existsSync(COMMANDS_DIR)) {
15
+ copyDir(COMMANDS_DIR, path.join(claudeDir, 'commands'));
16
+ } else {
17
+ console.error('✗ Commands directory not found');
18
+ process.exit(1);
19
+ }
20
+
21
+ if (fs.existsSync(AGENTS_DIR)) {
22
+ copyDir(AGENTS_DIR, path.join(claudeDir, 'agents'));
23
+ }
24
+
25
+ if (fs.existsSync(AVESTA_DIR)) {
26
+ copyDir(AVESTA_DIR, path.join(targetDir, '.avesta'));
27
+ }
28
+
29
+ const binaryPath = downloadBinary(targetDir);
30
+ configureMcpServer(targetDir, binaryPath);
31
+ configureSessionStartHook(targetDir);
32
+ configureEnforcementHooks(targetDir);
33
+ configureStatusLine(targetDir);
34
+ ensureClaudeMdSection(targetDir);
35
+
36
+ const commandCount = fs.existsSync(COMMANDS_DIR) ? fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')).length : 0;
37
+ const agentCount = fs.existsSync(AGENTS_DIR) ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md')).length : 0;
38
+ const mcpStatus = binaryPath ? 'MCP server' : 'MCP server (download failed — run npx @avesta-hq/prevention update to retry)';
39
+
40
+ console.log(`
41
+ ✅ Avesta Prevention ready!
42
+
43
+ Installed: ${commandCount} commands · ${agentCount} agents · ${mcpStatus}
44
+
45
+ Next steps:
46
+ 1. Open Claude Code in this project
47
+ 2. Run /avesta-help to see available commands
48
+ 3. Run /avesta-vision to define what you're building
49
+
50
+ Docs: https://github.com/${GITHUB_RELEASES_REPO}
51
+ `);
52
+ }
53
+
54
+ function update() {
55
+ const targetDir = process.cwd();
56
+ const binaryPath = downloadBinary(targetDir);
57
+ if (binaryPath) {
58
+ configureMcpServer(targetDir, binaryPath);
59
+ console.log('\n ✅ MCP server updated successfully!\n');
60
+ } else {
61
+ console.error('\n ✗ Update failed. Check your internet connection and try again.\n');
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ function serve() {
67
+ const { PACKAGE_ROOT } = require('./utils');
68
+ const binaryPath = path.join(PACKAGE_ROOT, 'dist', 'prevention');
69
+
70
+ if (fs.existsSync(binaryPath)) {
71
+ const { execFileSync } = require('child_process');
72
+ try { execFileSync(binaryPath, { stdio: 'inherit' }); } catch (e) { process.exit(e.status || 1); }
73
+ return;
74
+ }
75
+
76
+ const mcpServerPath = path.join(PACKAGE_ROOT, 'src', 'mcp-server.ts');
77
+ if (fs.existsSync(mcpServerPath)) {
78
+ const { execFileSync } = require('child_process');
79
+ try { execFileSync('npx', ['tsx', mcpServerPath], { stdio: 'inherit' }); } catch (e) { process.exit(e.status || 1); }
80
+ return;
81
+ }
82
+
83
+ console.error('✗ No MCP server binary or source found');
84
+ console.error(' Run: npx @avesta-hq/prevention update');
85
+ process.exit(1);
86
+ }
87
+
88
+ module.exports = { init, update, serve };
@@ -0,0 +1,34 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { COMMANDS_DIR, AGENTS_DIR, copyDir, readWorkflowState } = require('./utils');
4
+ const { generateWelcomeBanner, generateStatusBanner } = require('./banner');
5
+
6
+ function syncFiles() {
7
+ const cwd = process.cwd();
8
+ const claudeDir = path.join(cwd, '.claude');
9
+ const mcpJsonPath = path.join(cwd, '.mcp.json');
10
+
11
+ if (!fs.existsSync(claudeDir) || !fs.existsSync(mcpJsonPath)) return;
12
+ try {
13
+ const cfg = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
14
+ if (!cfg?.mcpServers?.['prevention']) return;
15
+ } catch { return; }
16
+
17
+ if (fs.existsSync(COMMANDS_DIR)) copyDir(COMMANDS_DIR, path.join(claudeDir, 'commands'));
18
+ if (fs.existsSync(AGENTS_DIR)) copyDir(AGENTS_DIR, path.join(claudeDir, 'agents'));
19
+ }
20
+
21
+ function sessionStart() {
22
+ try {
23
+ syncFiles();
24
+ const cwd = process.cwd();
25
+ const state = readWorkflowState(cwd);
26
+ const banner = state ? generateStatusBanner(state) : generateWelcomeBanner();
27
+ process.stdout.write(JSON.stringify({ additionalContext: banner }));
28
+ process.exit(0);
29
+ } catch {
30
+ process.exit(0);
31
+ }
32
+ }
33
+
34
+ module.exports = { sessionStart };
@@ -0,0 +1,173 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { isInsideNodeModules, escapeRegExp } = require('./utils');
4
+
5
+ const PREVENTION_SECTION_START = '<!-- prevention:start -->';
6
+ const PREVENTION_SECTION_END = '<!-- prevention:end -->';
7
+
8
+ const PREVENTION_CLAUDE_MD_SECTION = `${PREVENTION_SECTION_START}
9
+ ## Prevention — MCP Server Integration
10
+
11
+ This project uses [Prevention](https://github.com/avesta-hq/prevention) as an MCP server for XP/CD workflow enforcement.
12
+
13
+ **When starting work:**
14
+
15
+ 1. Run \`/avesta-help\` to see current status and next steps
16
+ 2. The workflow tracks your phase (vision → plan → atdd → tdd → review → ship)
17
+ 3. Gates enforce that prerequisites are met before advancing
18
+
19
+ **Key commands:**
20
+
21
+ - \`/avesta-plan <feature>\` — Break feature into TDD-ready tasks
22
+ - \`/avesta-red <behavior>\` — Write ONE failing test
23
+ - \`/avesta-green\` — Minimal code to make it pass
24
+ - \`/avesta-refactor\` — Improve structure (tests must stay green)
25
+ - \`/avesta-code-review\` — Domain-specific code review
26
+ - \`/avesta-commit\` — Conventional commit
27
+ - \`/avesta-ship\` — Merge to main
28
+
29
+ Run \`/avesta-help\` for the full command list.
30
+ ${PREVENTION_SECTION_END}`;
31
+
32
+ // ── Helpers ───────────────────────────────────────────────────────
33
+
34
+ function readSettings(targetDir) {
35
+ const settingsPath = path.join(targetDir, '.claude', 'settings.json');
36
+ if (!fs.existsSync(settingsPath)) return {};
37
+ try {
38
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ function writeSettings(targetDir, settings) {
45
+ const claudeDir = path.join(targetDir, '.claude');
46
+ if (!fs.existsSync(claudeDir)) {
47
+ fs.mkdirSync(claudeDir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(
50
+ path.join(claudeDir, 'settings.json'),
51
+ JSON.stringify(settings, null, 2) + '\n'
52
+ );
53
+ }
54
+
55
+ function findCliCommand(subcommand) {
56
+ if (isInsideNodeModules()) {
57
+ return `npx @avesta-hq/prevention ${subcommand}`;
58
+ }
59
+ return `node ${path.resolve(__dirname, '..', 'cli.js')} ${subcommand}`;
60
+ }
61
+
62
+ // ── Configurators ─────────────────────────────────────────────────
63
+
64
+ function configureSessionStartHook(targetDir) {
65
+ const settings = readSettings(targetDir);
66
+ if (!settings.hooks) settings.hooks = {};
67
+
68
+ const command = findCliCommand('session-start');
69
+ const existing = settings.hooks.SessionStart || [];
70
+ const alreadyConfigured = existing.some(entry =>
71
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('prevention session-start'))
72
+ );
73
+
74
+ if (!alreadyConfigured) {
75
+ settings.hooks.SessionStart = [
76
+ ...existing,
77
+ { matcher: '', hooks: [{ type: 'command', command }] },
78
+ ];
79
+ }
80
+
81
+ writeSettings(targetDir, settings);
82
+ }
83
+
84
+ function configureEnforcementHooks(targetDir) {
85
+ const settings = readSettings(targetDir);
86
+ if (!settings.hooks) settings.hooks = {};
87
+
88
+ const preToolUseCmd = findCliCommand('hook pre-tool-use');
89
+ const promptSubmitCmd = findCliCommand('hook prompt-submit');
90
+
91
+ // PreToolUse: remove old prevention hooks, add fresh ones
92
+ const existing = (settings.hooks.PreToolUse || []).filter(entry =>
93
+ !(entry.hooks && entry.hooks.some(h => h.command && h.command.includes('prevention hook')))
94
+ );
95
+ existing.push(
96
+ { matcher: 'Edit|Write', hooks: [{ type: 'command', command: preToolUseCmd }] },
97
+ { matcher: 'Bash', hooks: [{ type: 'command', command: preToolUseCmd }] },
98
+ );
99
+ settings.hooks.PreToolUse = existing;
100
+
101
+ // UserPromptSubmit
102
+ const promptHooks = settings.hooks.UserPromptSubmit || [];
103
+ const promptConfigured = promptHooks.some(entry =>
104
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('prevention hook prompt-submit'))
105
+ );
106
+ if (!promptConfigured) {
107
+ promptHooks.push({ matcher: '', hooks: [{ type: 'command', command: promptSubmitCmd }] });
108
+ settings.hooks.UserPromptSubmit = promptHooks;
109
+ }
110
+
111
+ writeSettings(targetDir, settings);
112
+ console.log(' ✓ Configured workflow enforcement hooks (PreToolUse, UserPromptSubmit)');
113
+ }
114
+
115
+ function configureStatusLine(targetDir) {
116
+ const statusLineSrc = path.join(targetDir, '.avesta', 'statusLine.js');
117
+ if (!fs.existsSync(statusLineSrc)) return;
118
+
119
+ const settings = readSettings(targetDir);
120
+ settings.statusLine = { type: 'command', command: `node ${statusLineSrc}` };
121
+ writeSettings(targetDir, settings);
122
+ }
123
+
124
+ function configureMcpServer(targetDir, binaryPath) {
125
+ const mcpJsonPath = path.join(targetDir, '.mcp.json');
126
+ let mcpConfig = {};
127
+ if (fs.existsSync(mcpJsonPath)) {
128
+ try { mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8')); } catch { mcpConfig = {}; }
129
+ }
130
+
131
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
132
+
133
+ if (binaryPath) {
134
+ mcpConfig.mcpServers['prevention'] = { command: path.resolve(binaryPath) };
135
+ } else {
136
+ mcpConfig.mcpServers['prevention'] = { command: 'npx', args: ['@avesta-hq/prevention', 'serve'] };
137
+ }
138
+
139
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
140
+ }
141
+
142
+ function ensureClaudeMdSection(targetDir) {
143
+ const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
144
+
145
+ if (!fs.existsSync(claudeMdPath)) {
146
+ fs.writeFileSync(claudeMdPath, PREVENTION_CLAUDE_MD_SECTION + '\n', 'utf-8');
147
+ console.log(' ✓ Created CLAUDE.md with Prevention section');
148
+ return;
149
+ }
150
+
151
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
152
+
153
+ if (content.includes(PREVENTION_SECTION_START)) {
154
+ const regex = new RegExp(
155
+ escapeRegExp(PREVENTION_SECTION_START) + '[\\s\\S]*?' + escapeRegExp(PREVENTION_SECTION_END), 'm'
156
+ );
157
+ fs.writeFileSync(claudeMdPath, content.replace(regex, PREVENTION_CLAUDE_MD_SECTION), 'utf-8');
158
+ console.log(' ✓ Updated Prevention section in CLAUDE.md');
159
+ return;
160
+ }
161
+
162
+ const separator = content.endsWith('\n') ? '\n' : '\n\n';
163
+ fs.writeFileSync(claudeMdPath, content + separator + PREVENTION_CLAUDE_MD_SECTION + '\n', 'utf-8');
164
+ console.log(' ✓ Added Prevention section to CLAUDE.md');
165
+ }
166
+
167
+ module.exports = {
168
+ configureSessionStartHook,
169
+ configureEnforcementHooks,
170
+ configureStatusLine,
171
+ configureMcpServer,
172
+ ensureClaudeMdSection,
173
+ };
@@ -0,0 +1,131 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ const PACKAGE_ROOT = path.join(__dirname, '..', '..');
6
+ const COMMANDS_DIR = path.join(PACKAGE_ROOT, '.claude', 'commands');
7
+ const AGENTS_DIR = path.join(PACKAGE_ROOT, '.claude', 'agents');
8
+ const AVESTA_DIR = path.join(PACKAGE_ROOT, '.avesta');
9
+ const GITHUB_RELEASES_REPO = 'avesta-hq/prevention-releases';
10
+
11
+ function copyDir(src, dest) {
12
+ if (!fs.existsSync(dest)) {
13
+ fs.mkdirSync(dest, { recursive: true });
14
+ }
15
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
16
+ const srcPath = path.join(src, entry.name);
17
+ const destPath = path.join(dest, entry.name);
18
+ if (entry.isDirectory()) {
19
+ copyDir(srcPath, destPath);
20
+ } else {
21
+ fs.copyFileSync(srcPath, destPath);
22
+ }
23
+ }
24
+ }
25
+
26
+ function escapeRegExp(str) {
27
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ }
29
+
30
+ function getVersion() {
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
33
+ return pkg.version || '0.0.0';
34
+ } catch {
35
+ return '0.0.0';
36
+ }
37
+ }
38
+
39
+ function getLicenseTier() {
40
+ try {
41
+ const cachePath = path.join(require('os').homedir(), '.avesta', 'license-cache.json');
42
+ if (fs.existsSync(cachePath)) {
43
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
44
+ if (cache.tier && cache.tier !== 'free') return cache.tier;
45
+ }
46
+ } catch {}
47
+ return 'free';
48
+ }
49
+
50
+ function isInsideNodeModules() {
51
+ return __dirname.includes('node_modules');
52
+ }
53
+
54
+ function readWorkflowState(cwd) {
55
+ const statePath = path.join(cwd, '.avesta', 'workflow-state.json');
56
+ if (!fs.existsSync(statePath)) return null;
57
+ try {
58
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function getPlatformBinaryName() {
65
+ const { platform, arch } = process;
66
+ if (platform === 'linux' && arch === 'x64') return 'prevention-linux-x64';
67
+ if (platform === 'darwin' && arch === 'arm64') return 'prevention-darwin-arm64';
68
+ if (platform === 'darwin' && arch === 'x64') return 'prevention-darwin-x64';
69
+ return null;
70
+ }
71
+
72
+ function getLatestVersion() {
73
+ try {
74
+ const result = execSync(
75
+ `curl -sI "https://github.com/${GITHUB_RELEASES_REPO}/releases/latest" | grep -i ^location:`,
76
+ { encoding: 'utf8', timeout: 10000 }
77
+ );
78
+ const match = result.match(/\/tag\/(v[^\s\r\n]+)/);
79
+ return match ? match[1] : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function downloadBinary(targetDir) {
86
+ const binaryName = getPlatformBinaryName();
87
+ if (!binaryName) {
88
+ console.error(` ⚠ Unsupported platform: ${process.platform}-${process.arch}`);
89
+ console.error(` Supported: linux-x64, darwin-arm64, darwin-x64`);
90
+ return null;
91
+ }
92
+
93
+ const binDir = path.join(targetDir, '.avesta', 'bin');
94
+ const binaryPath = path.join(binDir, 'prevention');
95
+ const version = getLatestVersion();
96
+
97
+ if (!version) {
98
+ console.error(' ⚠ Could not determine latest release version');
99
+ return null;
100
+ }
101
+
102
+ const url = `https://github.com/${GITHUB_RELEASES_REPO}/releases/download/${version}/${binaryName}`;
103
+ if (!fs.existsSync(binDir)) {
104
+ fs.mkdirSync(binDir, { recursive: true });
105
+ }
106
+
107
+ try {
108
+ console.log(` Downloading MCP server (${version}, ${process.platform}-${process.arch})...`);
109
+ execSync(`curl -sL "${url}" -o "${binaryPath}"`, { timeout: 60000 });
110
+ fs.chmodSync(binaryPath, 0o755);
111
+ return binaryPath;
112
+ } catch (e) {
113
+ console.error(` ⚠ Failed to download binary from ${url}`);
114
+ return null;
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ PACKAGE_ROOT,
120
+ COMMANDS_DIR,
121
+ AGENTS_DIR,
122
+ AVESTA_DIR,
123
+ GITHUB_RELEASES_REPO,
124
+ copyDir,
125
+ escapeRegExp,
126
+ getVersion,
127
+ getLicenseTier,
128
+ isInsideNodeModules,
129
+ readWorkflowState,
130
+ downloadBinary,
131
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avesta-hq/prevention",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "XP/CD development agent commands for Claude Code - achieve Elite DORA metrics through disciplined TDD and Continuous Delivery practices",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -38,7 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "tsc",
40
40
  "dev": "tsx src/mcp-server.ts",
41
- "test": "echo 'No tests yet'",
41
+ "test": "jest",
42
42
  "test:evals": "cd src/_deprecated/evals && python -m pytest test_cases/orchestrator/ -v",
43
43
  "prebuild:assets": "tsx scripts/embed-assets.ts",
44
44
  "build:binary": "npm run prebuild:assets && bun build src/mcp-server.ts --compile --outfile dist/prevention",
@@ -52,7 +52,10 @@
52
52
  "zod": "^4.3.5"
53
53
  },
54
54
  "devDependencies": {
55
+ "@types/jest": "^30.0.0",
55
56
  "@types/node": "^25.0.5",
57
+ "jest": "^30.3.0",
58
+ "ts-jest": "^29.4.6",
56
59
  "tsx": "^4.21.0",
57
60
  "typescript": "^5.9.3"
58
61
  }