@avesta-hq/prevention 0.4.1 → 0.6.0-pre.3

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.
@@ -2,8 +2,8 @@
2
2
  name: avesta-acceptance-stage
3
3
  description: Set up acceptance stage workflow with version-based test execution
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"acceptance-stage\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-acceptance-stage
@@ -2,8 +2,9 @@
2
2
  name: avesta-acceptance-writer
3
3
  description: Write Executable Specifications in problem-domain language
4
4
  model: inherit
5
- tools: Read, Write, Edit, Glob, Grep
5
+ disallowedTools: Bash
6
6
  maxTurns: 25
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"acceptance-writer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-acceptance-writer
@@ -2,8 +2,8 @@
2
2
  name: avesta-bug-fixer
3
3
  description: Bug fix workflow - reproduce, failing test, fix, verify
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"bug-fixer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-bug-fixer
@@ -2,8 +2,8 @@
2
2
  name: avesta-commit-stage
3
3
  description: Set up commit stage CI/CD pipeline with test pyramid enforcement
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"commit-stage\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-commit-stage
@@ -2,8 +2,9 @@
2
2
  name: avesta-dependency-reviewer
3
3
  description: Review dependencies and generate gradual update plan
4
4
  model: inherit
5
- tools: Read, Bash, Glob, Grep
5
+ disallowedTools: Write, Edit
6
6
  maxTurns: 20
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"dependency-reviewer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-dependency-reviewer
@@ -2,8 +2,8 @@
2
2
  name: avesta-driver-builder
3
3
  description: Implement Protocol Driver for acceptance tests
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"driver-builder\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-driver-builder
@@ -2,8 +2,9 @@
2
2
  name: avesta-dsl-builder
3
3
  description: Implement Domain Specific Language for acceptance tests
4
4
  model: inherit
5
- tools: Read, Write, Edit, Glob, Grep
5
+ disallowedTools: Bash
6
6
  maxTurns: 25
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"dsl-builder\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-dsl-builder
@@ -2,8 +2,8 @@
2
2
  name: avesta-enhancer
3
3
  description: Enhancement workflow - plan, ATDD, TDD, driver, review
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"enhancer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-enhancer
@@ -2,8 +2,8 @@
2
2
  name: avesta-green
3
3
  description: TDD Green Phase - Write MINIMAL code to pass the failing test
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 25
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"green\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-green
@@ -2,8 +2,8 @@
2
2
  name: avesta-layer-worker
3
3
  description: Implement a single Clean Architecture layer with TDD (all tests first, then all code)
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"layer-worker\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-layer-worker
@@ -2,8 +2,8 @@
2
2
  name: avesta-layer
3
3
  description: Implement a full layer - all tests first, then all production code
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 50
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"layer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-layer
@@ -2,8 +2,9 @@
2
2
  name: avesta-mutation-tester
3
3
  description: Analyze test effectiveness with mutation testing patterns
4
4
  model: inherit
5
- tools: Read, Bash, Glob, Grep
5
+ disallowedTools: Write, Edit
6
6
  maxTurns: 25
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"mutation-tester\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-mutation-tester
@@ -2,8 +2,9 @@
2
2
  name: avesta-planner
3
3
  description: Create implementation plans using Example Mapping and behavioral analysis
4
4
  model: inherit
5
- tools: Read, Write, Edit, Glob, Grep
5
+ disallowedTools: Bash
6
6
  maxTurns: 30
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"planner\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-planner
@@ -2,8 +2,8 @@
2
2
  name: avesta-red
3
3
  description: TDD Red Phase - Write ONE failing test
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 25
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"red\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-red
@@ -2,8 +2,8 @@
2
2
  name: avesta-refactorer
3
3
  description: TDD Refactor Phase - Improve code structure while keeping tests green
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 25
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"refactorer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-refactorer
@@ -2,8 +2,8 @@
2
2
  name: avesta-release-stage
3
3
  description: Set up release stage CI/CD pipeline with contract verification
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"release-stage\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-release-stage
@@ -2,8 +2,9 @@
2
2
  name: avesta-reviewer
3
3
  description: Review code for XP/CD best practices and Clean Architecture
4
4
  model: inherit
5
- tools: Read, Glob, Grep
5
+ disallowedTools: Write, Edit, Bash
6
6
  maxTurns: 20
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"reviewer\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-reviewer
@@ -2,8 +2,9 @@
2
2
  name: avesta-scaffolder
3
3
  description: Scaffold project structure, dependencies, and config files
4
4
  model: haiku
5
- tools: Read, Write, Bash, Glob
5
+ disallowedTools: Edit
6
6
  maxTurns: 15
7
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"scaffolder\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
8
  ---
8
9
 
9
10
  # avesta-scaffolder
@@ -2,8 +2,8 @@
2
2
  name: avesta-tech-debt-assessor
3
3
  description: Tech debt workflow - approval tests, refactor, verify
4
4
  model: inherit
5
- tools: Read, Write, Edit, Bash, Glob, Grep
6
5
  maxTurns: 30
6
+ initialPrompt: "FIRST: Call avesta_get_prompt(\"tech-debt-assessor\") to load your instructions — include project_type in context. Then call avesta_get_skill for EVERY skill in skills_to_load with _source=workflow. File writes are BLOCKED by the PreToolUse hook until all skills are loaded."
7
7
  ---
8
8
 
9
9
  # avesta-tech-debt-assessor
@@ -8,13 +8,13 @@ If blocked: Show the `error_message` to the user and stop.
8
8
 
9
9
  If `gate_check.allowed` is true, orchestrate per-layer workers:
10
10
 
11
- 1. **Parse arguments** from `$ARGUMENTS`:
11
+ 1. **Parse arguments and dispatch response**:
12
12
  - **Layer(s)**: If a specific layer is named (`domain`, `application`, `infrastructure`, `presentation`), implement only that layer. If `all` or no layer specified, implement all four in order: domain, application, infrastructure, presentation.
13
- - **Project type**: If `backend` or `frontend` is specified, use it. Default: `backend`.
13
+ - **Project type**: Use the `project_type` field from the dispatch response. This comes from the workflow state and reflects what the user set during `/avesta-init`. Do NOT default to backend — always use the dispatch response value.
14
14
 
15
15
  2. **For each layer** (in the determined order), spawn a subagent:
16
16
  - Use the `avesta-layer-worker` agent
17
- - Pass a structured prompt: `"LAYER: <layer_name>\nPROJECT_TYPE: <project_type>\nFEATURE: $ARGUMENTS"`
17
+ - Pass a structured prompt: `"LAYER: <layer_name>\nPROJECT_TYPE: <project_type from dispatch>\nFEATURE: $ARGUMENTS"`
18
18
  - Wait for the worker to complete before starting the next layer
19
19
  - After each worker completes, briefly report which layer finished and how many tests passed
20
20
 
package/CLAUDE.md CHANGED
@@ -68,7 +68,7 @@
68
68
 
69
69
  ## MCP Server Integration
70
70
 
71
- This project uses Prevention as an MCP server with 15 tools, 26 agents, and 34 skills that provide all methodology knowledge (TDD, Clean Architecture, test pyramid, CI/CD, etc.) on demand.
71
+ This project uses Prevention as two MCP servers: a local server (12 tools for workflow orchestration) and a remote server (3 tools for content delivery skills, prompts, catalog).
72
72
 
73
73
  **When starting work:**
74
74
 
@@ -76,10 +76,13 @@ This project uses Prevention as an MCP server with 15 tools, 26 agents, and 34 s
76
76
  2. The workflow tracks your phase (vision → plan → atdd → tdd → review → ship)
77
77
  3. Gates enforce that prerequisites are met before advancing
78
78
 
79
- **Key tools:**
79
+ **Key tools (local):**
80
80
 
81
81
  - `avesta_get_status` — Current phase, gates, next action
82
82
  - `avesta_dispatch` — Route tasks to the right workflow step
83
+
84
+ **Key tools (remote — `prevention-content`):**
85
+
83
86
  - `avesta_get_catalog` — All available commands and skills
84
87
  - `avesta_get_skill` — Deep knowledge on any practice (testing, architecture, CI/CD)
85
88
 
package/bin/cli.js CHANGED
@@ -16,9 +16,11 @@ Commands:
16
16
  init First-time setup: commands, agents, MCP server, hooks, CLAUDE.md
17
17
  update Update everything to latest version (commands, agents, MCP server, hooks)
18
18
  serve Start the MCP server (used by Claude Code)
19
+ test-local [dir] Set up a project to use local source (no npm publish needed)
19
20
  session-start Output session context (used by SessionStart hook)
20
21
  hook pre-tool-use Workflow enforcement hook (blocks unauthorized edits)
21
22
  hook prompt-submit Context injection hook (injects workflow state)
23
+ hook subagent-start Skill-loading injection hook (fires when agents spawn)
22
24
 
23
25
  Examples:
24
26
  cd my-project
@@ -44,6 +46,9 @@ switch (command) {
44
46
  case 'serve':
45
47
  require('./lib/init').serve();
46
48
  break;
49
+ case 'test-local':
50
+ require('./lib/test-local').testLocal(args[1]);
51
+ break;
47
52
  case 'session-start':
48
53
  require('./lib/session-start').sessionStart();
49
54
  break;
@@ -55,6 +60,9 @@ switch (command) {
55
60
  case 'prompt-submit':
56
61
  require('./lib/hooks').hookPromptSubmit();
57
62
  break;
63
+ case 'subagent-start':
64
+ require('./lib/hooks').hookSubagentStart();
65
+ break;
58
66
  default:
59
67
  console.error(`Unknown hook: ${args[1]}`);
60
68
  process.exit(1);
package/bin/lib/hooks.js CHANGED
@@ -109,6 +109,14 @@ function extractBashWriteContent(command) {
109
109
 
110
110
  // ── PreToolUse Hook ───────────────────────────────────────────────
111
111
 
112
+ function readWorkflowState(cwd) {
113
+ const statePath = path.join(cwd, '.avesta', 'workflow-state.json');
114
+ if (!fs.existsSync(statePath)) return null;
115
+ try {
116
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
117
+ } catch { return null; }
118
+ }
119
+
112
120
  function hookPreToolUse() {
113
121
  try {
114
122
  const data = JSON.parse(fs.readFileSync(0, 'utf8'));
@@ -187,6 +195,22 @@ function hookPreToolUse() {
187
195
  }
188
196
  }
189
197
 
198
+ // State-based fallback: block git commit/push when policy is missing
199
+ // but workflow state exists (e.g., policy file deleted or stale)
200
+ if (!policy && /\bgit\s+(commit|push)\b/.test(command)) {
201
+ const state = readWorkflowState(cwd);
202
+ // Bypass for scaffold, spike, or idle (no feature in progress)
203
+ const bypass = state?.needs_scaffold || state?.mode === 'spike' || state?.current_phase === 'idle';
204
+ if (state && !bypass && !state.gates?.review_approved) {
205
+ const verb = /\bgit\s+commit\b/.test(command) ? 'commit' : 'push';
206
+ process.stderr.write(
207
+ `⛔ Prevention: Cannot ${verb} without completing code review.\n` +
208
+ `Run /avesta-code-review first.\n`
209
+ );
210
+ process.exit(2);
211
+ }
212
+ }
213
+
190
214
  // Secret scanning for Bash commands that write file content
191
215
  const inlineContent = extractBashWriteContent(command);
192
216
  if (inlineContent) {
@@ -226,8 +250,13 @@ function hookPromptSubmit() {
226
250
  process.exit(0);
227
251
  }
228
252
 
229
- if (policy.prompt_context) {
230
- process.stdout.write(JSON.stringify({ additionalContext: policy.prompt_context }));
253
+ const parts = [];
254
+ if (policy.prompt_context) parts.push(policy.prompt_context);
255
+ if (policy.project_type) parts.push(`Project type: ${policy.project_type}`);
256
+ if (policy.current_agent) parts.push(`Active agent: avesta-${policy.current_agent}`);
257
+
258
+ if (parts.length > 0) {
259
+ process.stdout.write(JSON.stringify({ additionalContext: parts.join(' | ') }));
231
260
  }
232
261
  process.exit(0);
233
262
  } catch {
@@ -235,4 +264,55 @@ function hookPromptSubmit() {
235
264
  }
236
265
  }
237
266
 
238
- module.exports = { hookPreToolUse, hookPromptSubmit };
267
+ // ── SubagentStart Hook ───────────────────────────────────────────
268
+
269
+ function hookSubagentStart() {
270
+ try {
271
+ const data = JSON.parse(fs.readFileSync(0, 'utf8'));
272
+ const { agent_type, cwd = process.cwd() } = data;
273
+
274
+ if (!agent_type) {
275
+ process.exit(0);
276
+ }
277
+
278
+ // Read workflow state to get project_type and current agent's required skills
279
+ const statePath = path.join(cwd, '.avesta', 'workflow-state.json');
280
+ if (!fs.existsSync(statePath)) {
281
+ process.exit(0);
282
+ }
283
+
284
+ let state;
285
+ try {
286
+ state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
287
+ } catch {
288
+ process.exit(0);
289
+ }
290
+
291
+ const requiredSkills = state.required_skills_for_agent || [];
292
+ if (requiredSkills.length === 0) {
293
+ // Agent has no skills to load (committer, shipper, etc.)
294
+ process.exit(0);
295
+ }
296
+
297
+ const projectType = state.project?.type || 'backend';
298
+ const agentName = state.current_agent || agent_type.replace(/^avesta-/, '');
299
+
300
+ process.stdout.write(JSON.stringify({
301
+ hookSpecificOutput: {
302
+ hookEventName: "SubagentStart",
303
+ additionalContext:
304
+ `[Prevention] MANDATORY — Skills must be loaded before writing any files.\n` +
305
+ `The PreToolUse hook will BLOCK all file writes until skills are loaded.\n` +
306
+ `1. Call avesta_get_prompt("${agentName}", { context: { project_type: "${projectType}" } })\n` +
307
+ `2. Call avesta_get_skill for EVERY skill in skills_to_load with _source="workflow"\n` +
308
+ `Required skills (${requiredSkills.length}): ${requiredSkills.join(', ')}\n` +
309
+ `Project type: ${projectType}`
310
+ }
311
+ }));
312
+ process.exit(0);
313
+ } catch {
314
+ process.exit(0); // Fail-open
315
+ }
316
+ }
317
+
318
+ module.exports = { hookPreToolUse, hookPromptSubmit, hookSubagentStart };
@@ -87,6 +87,7 @@ function configureEnforcementHooks(targetDir) {
87
87
 
88
88
  const preToolUseCmd = findCliCommand('hook pre-tool-use');
89
89
  const promptSubmitCmd = findCliCommand('hook prompt-submit');
90
+ const subagentStartCmd = findCliCommand('hook subagent-start');
90
91
 
91
92
  // PreToolUse: remove old prevention hooks, add fresh ones
92
93
  const existing = (settings.hooks.PreToolUse || []).filter(entry =>
@@ -108,8 +109,17 @@ function configureEnforcementHooks(targetDir) {
108
109
  settings.hooks.UserPromptSubmit = promptHooks;
109
110
  }
110
111
 
112
+ // SubagentStart: inject skill-loading context when agents spawn
113
+ const subagentHooks = (settings.hooks.SubagentStart || []).filter(entry =>
114
+ !(entry.hooks && entry.hooks.some(h => h.command && h.command.includes('prevention hook')))
115
+ );
116
+ subagentHooks.push(
117
+ { matcher: '', hooks: [{ type: 'command', command: subagentStartCmd }] },
118
+ );
119
+ settings.hooks.SubagentStart = subagentHooks;
120
+
111
121
  writeSettings(targetDir, settings);
112
- console.log(' ✓ Configured workflow enforcement hooks (PreToolUse, UserPromptSubmit)');
122
+ console.log(' ✓ Configured workflow enforcement hooks (PreToolUse, UserPromptSubmit, SubagentStart)');
113
123
  }
114
124
 
115
125
  function configureStatusLine(targetDir) {
@@ -121,7 +131,7 @@ function configureStatusLine(targetDir) {
121
131
  writeSettings(targetDir, settings);
122
132
  }
123
133
 
124
- function configureMcpServer(targetDir, binaryPath) {
134
+ function configureMcpServer(targetDir, binaryPath, options = {}) {
125
135
  const mcpJsonPath = path.join(targetDir, '.mcp.json');
126
136
  let mcpConfig = {};
127
137
  if (fs.existsSync(mcpJsonPath)) {
@@ -130,11 +140,51 @@ function configureMcpServer(targetDir, binaryPath) {
130
140
 
131
141
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
132
142
 
133
- if (binaryPath) {
134
- mcpConfig.mcpServers['prevention'] = { command: path.resolve(binaryPath) };
143
+ // Local MCP server: workflow orchestration tools
144
+ // Pass through API/MCP URLs so the server process can reach them
145
+ const env = {};
146
+ if (process.env.AVESTA_API_URL) env.AVESTA_API_URL = process.env.AVESTA_API_URL;
147
+ if (process.env.AVESTA_MCP_URL) env.AVESTA_MCP_URL = process.env.AVESTA_MCP_URL;
148
+ const hasEnv = Object.keys(env).length > 0;
149
+
150
+ if (options.localSource) {
151
+ // Dev mode: run from source via tsx
152
+ const entry = { command: 'npx', args: ['tsx', options.localSource] };
153
+ if (hasEnv) entry.env = env;
154
+ mcpConfig.mcpServers['prevention'] = entry;
155
+ } else if (binaryPath) {
156
+ const entry = { command: path.resolve(binaryPath) };
157
+ if (hasEnv) entry.env = env;
158
+ mcpConfig.mcpServers['prevention'] = entry;
135
159
  } else {
136
- mcpConfig.mcpServers['prevention'] = { command: 'npx', args: ['@avesta-hq/prevention', 'serve'] };
160
+ const entry = { command: 'npx', args: ['@avesta-hq/prevention', 'serve'] };
161
+ if (hasEnv) entry.env = env;
162
+ mcpConfig.mcpServers['prevention'] = entry;
163
+ }
164
+
165
+ // Remote MCP server: content delivery (skills, prompts, catalog)
166
+ // Pre-populate auth token from existing session if available
167
+ const remoteUrl = process.env.AVESTA_MCP_URL || 'https://prevention-production.up.railway.app/mcp';
168
+ const existingRemote = mcpConfig.mcpServers['prevention-content'];
169
+ const existingToken = existingRemote?.headers?.Authorization;
170
+ let authToken = existingToken || '';
171
+ if (!authToken) {
172
+ try {
173
+ const sessionPath = path.join(require('os').homedir(), '.avesta', 'session.json');
174
+ if (fs.existsSync(sessionPath)) {
175
+ const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
176
+ if (session.access_token) {
177
+ authToken = `Bearer ${session.access_token}`;
178
+ }
179
+ }
180
+ } catch {}
137
181
  }
182
+ const headers = authToken ? { Authorization: authToken } : {};
183
+ mcpConfig.mcpServers['prevention-content'] = {
184
+ type: 'http',
185
+ url: remoteUrl,
186
+ headers,
187
+ };
138
188
 
139
189
  fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
140
190
  }
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local Testing Setup
4
+ *
5
+ * Sets up a target project to use the local Prevention source code
6
+ * instead of the published npm package. Copies agents, commands,
7
+ * configures hooks, and points MCP server at local source.
8
+ *
9
+ * Usage:
10
+ * cd ~/Projects/my-test-project
11
+ * node ~/Projects/cd-agent/bin/lib/test-local.js
12
+ *
13
+ * Or via CLI:
14
+ * node ~/Projects/cd-agent/bin/cli.js test-local [target-dir]
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { execSync } = require('child_process');
20
+ const { PACKAGE_ROOT, COMMANDS_DIR, AGENTS_DIR, AVESTA_DIR, copyDir, getVersion } = require('./utils');
21
+ const {
22
+ configureMcpServer,
23
+ configureSessionStartHook,
24
+ configureEnforcementHooks,
25
+ configureStatusLine,
26
+ ensureClaudeMdSection,
27
+ } = require('./settings');
28
+
29
+ function testLocal(targetDir) {
30
+ targetDir = targetDir || process.cwd();
31
+ const cdAgentRoot = PACKAGE_ROOT;
32
+
33
+ console.log(`\n Setting up local Prevention test environment...`);
34
+ console.log(` Source: ${cdAgentRoot}`);
35
+ console.log(` Target: ${targetDir}\n`);
36
+
37
+ // Step 1: Rebuild embedded assets
38
+ console.log(' [1/6] Rebuilding catalog + embedded assets...');
39
+ try {
40
+ execSync('pnpm run prebuild:assets', { cwd: cdAgentRoot, stdio: 'pipe' });
41
+ console.log(' ✓ Assets rebuilt');
42
+ } catch (e) {
43
+ console.error(' ✗ Asset rebuild failed:', e.message);
44
+ console.error(' Try running manually: cd ' + cdAgentRoot + ' && pnpm run prebuild:assets');
45
+ process.exit(1);
46
+ }
47
+
48
+ // Step 2: Copy agents + commands
49
+ console.log(' [2/6] Copying agents and commands...');
50
+ const claudeDir = path.join(targetDir, '.claude');
51
+ if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
52
+
53
+ if (fs.existsSync(COMMANDS_DIR)) {
54
+ copyDir(COMMANDS_DIR, path.join(claudeDir, 'commands'));
55
+ }
56
+ if (fs.existsSync(AGENTS_DIR)) {
57
+ copyDir(AGENTS_DIR, path.join(claudeDir, 'agents'));
58
+ }
59
+ if (fs.existsSync(AVESTA_DIR)) {
60
+ copyDir(AVESTA_DIR, path.join(targetDir, '.avesta'));
61
+ }
62
+ const commandCount = fs.existsSync(COMMANDS_DIR) ? fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')).length : 0;
63
+ const agentCount = fs.existsSync(AGENTS_DIR) ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md')).length : 0;
64
+ console.log(` ✓ Copied ${commandCount} commands, ${agentCount} agents`);
65
+
66
+ // Step 3: Configure MCP servers (local source + remote content)
67
+ console.log(' [3/6] Configuring MCP servers (local source + remote content)...');
68
+ const mcpServerPath = path.join(cdAgentRoot, 'src', 'mcp-server.ts');
69
+ configureMcpServer(targetDir, null, { localSource: mcpServerPath });
70
+ console.log(` ✓ Local MCP server → npx tsx ${mcpServerPath}`);
71
+ const remoteUrl = process.env.AVESTA_MCP_URL || 'https://prevention-production.up.railway.app/mcp';
72
+ console.log(` ✓ Remote MCP server → ${remoteUrl}`);
73
+
74
+ // Step 4: Configure hooks (pointing at local CLI)
75
+ console.log(' [4/6] Configuring hooks...');
76
+ configureSessionStartHook(targetDir);
77
+ configureEnforcementHooks(targetDir);
78
+ configureStatusLine(targetDir);
79
+ ensureClaudeMdSection(targetDir);
80
+
81
+ // Step 5: Write version
82
+ console.log(' [5/6] Writing version...');
83
+ const versionFile = path.join(targetDir, '.avesta', 'version.json');
84
+ const versionDir = path.dirname(versionFile);
85
+ if (!fs.existsSync(versionDir)) fs.mkdirSync(versionDir, { recursive: true });
86
+ fs.writeFileSync(versionFile, JSON.stringify({ version: getVersion(), updated_at: new Date().toISOString() }, null, 2));
87
+ console.log(` ✓ Version ${getVersion()}`);
88
+
89
+ // Step 6: Show result
90
+ console.log(' [6/6] Verifying setup...');
91
+ const settings = JSON.parse(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf8'));
92
+ const hookTypes = Object.keys(settings.hooks || {});
93
+ console.log(` ✓ Hooks configured: ${hookTypes.join(', ')}`);
94
+
95
+ const mcpJsonPath = path.join(targetDir, '.mcp.json');
96
+ const mcpFinal = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
97
+ const mcpCmd = mcpFinal.mcpServers?.prevention?.command || '?';
98
+ const mcpArgs = (mcpFinal.mcpServers?.prevention?.args || []).join(' ');
99
+ console.log(` ✓ Local MCP: ${mcpCmd} ${mcpArgs}`);
100
+ const remoteEntry = mcpFinal.mcpServers?.['prevention-content'];
101
+ console.log(` ✓ Remote MCP: ${remoteEntry?.url || 'not configured'}`);
102
+
103
+ console.log(`
104
+ ✅ Local test environment ready! (v${getVersion()})
105
+
106
+ Changes from source at ${cdAgentRoot} are active.
107
+ No npm publish needed — MCP server runs from source via tsx.
108
+
109
+ To test:
110
+ 1. Open Claude Code in ${targetDir}
111
+ 2. Run avesta login to authenticate (connects remote content server)
112
+ 3. Run /avesta-init backend (or frontend)
113
+ 4. Test skill enforcement: /avesta-red should block writes until skills loaded
114
+ 5. Test gate enforcement: git commit should be blocked without review gate
115
+
116
+ To revert to published version:
117
+ node ${path.join(cdAgentRoot, 'bin', 'cli.js')} update
118
+ `);
119
+ }
120
+
121
+ if (require.main === module) {
122
+ testLocal(process.argv[2]);
123
+ }
124
+
125
+ module.exports = { testLocal };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avesta-hq/prevention",
3
- "version": "0.4.1",
3
+ "version": "0.6.0-pre.3",
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",
@@ -16,14 +16,7 @@
16
16
  ],
17
17
  "author": "Avesta Technologies",
18
18
  "license": "UNLICENSED",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/avesta-hq/prevention.git"
22
- },
23
- "homepage": "https://github.com/avesta-hq/prevention#readme",
24
- "bugs": {
25
- "url": "https://github.com/avesta-hq/prevention/issues"
26
- },
19
+ "homepage": "https://avestahq.com/prevention",
27
20
  "bin": {
28
21
  "prevention": "./bin/cli.js"
29
22
  },