@itz4blitz/agentful 1.8.0 → 1.8.2

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
@@ -70,9 +70,6 @@ npx @itz4blitz/agentful init --preset=minimal
70
70
 
71
71
  # Custom: Choose components
72
72
  npx @itz4blitz/agentful init --agents=orchestrator,backend --skills=validation
73
-
74
- # Web configurator
75
- https://agentful.app/configure
76
73
  ```
77
74
 
78
75
  ## Commands
package/bin/cli.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  import fs from 'fs';
22
22
  import path from 'path';
23
+ import { spawnSync } from 'child_process';
23
24
  import { fileURLToPath } from 'url';
24
25
  import { initProject, isInitialized } from '../lib/init.js';
25
26
  import {
@@ -82,11 +83,11 @@ function showHelp() {
82
83
  console.log('');
83
84
  console.log('INIT OPTIONS (optional):');
84
85
  console.log(` ${colors.yellow}--preset=minimal${colors.reset} Minimal setup (orchestrator + backend only)`);
85
- console.log(` ${colors.yellow}--config=<url|id>${colors.reset} Use a shareable configuration`);
86
86
  console.log(` ${colors.yellow}--agents=<list>${colors.reset} Custom agents (comma-separated)`);
87
87
  console.log(` ${colors.yellow}--skills=<list>${colors.reset} Custom skills (comma-separated)`);
88
88
  console.log(` ${colors.yellow}--hooks=<list>${colors.reset} Custom hooks (comma-separated)`);
89
89
  console.log(` ${colors.yellow}--gates=<list>${colors.reset} Custom quality gates (comma-separated)`);
90
+ console.log(` ${colors.yellow}--skip-mcp${colors.reset} Skip automatic MCP server setup`);
90
91
  console.log('');
91
92
  console.log('EXAMPLES:');
92
93
  console.log(` ${colors.dim}# Install agentful (all components - recommended)${colors.reset}`);
@@ -183,50 +184,78 @@ function checkGitignore() {
183
184
  }
184
185
  }
185
186
 
186
- /**
187
- * Fetch configuration from shareable URL or ID
188
- * @param {string} configParam - URL or ID
189
- * @returns {Promise<Object|null>}
190
- */
191
- async function fetchShareableConfig(configParam) {
192
- try {
193
- // Determine if it's a full URL or just an ID
194
- let apiUrl;
195
- if (configParam.startsWith('http://') || configParam.startsWith('https://')) {
196
- // Extract ID from URL
197
- const match = configParam.match(/\/c\/([a-f0-9]{8})$/i);
198
- if (!match) {
199
- throw new Error('Invalid config URL format. Expected: https://agentful.app/c/{id}');
200
- }
201
- const id = match[1];
202
- apiUrl = `https://agentful.app/api/get-config/${id}`;
203
- } else if (/^[a-f0-9]{8}$/i.test(configParam)) {
204
- // It's just the ID
205
- apiUrl = `https://agentful.app/api/get-config/${configParam}`;
206
- } else {
207
- throw new Error('Invalid config parameter. Provide either a full URL or an 8-character ID.');
208
- }
187
+ const MCP_SERVER_NAME = 'agentful';
188
+ const MCP_SERVER_PACKAGE = '@itz4blitz/agentful-mcp-server';
189
+ const MCP_SETUP_COMMAND = `claude mcp add ${MCP_SERVER_NAME} -- npx -y ${MCP_SERVER_PACKAGE}`;
209
190
 
210
- log(colors.dim, `Fetching configuration from ${apiUrl}...`);
191
+ function parseMcpInstallState() {
192
+ const result = spawnSync('claude', ['mcp', 'list'], {
193
+ encoding: 'utf8'
194
+ });
211
195
 
212
- const response = await fetch(apiUrl);
196
+ if (result.error) {
197
+ return {
198
+ ok: false,
199
+ reason: result.error.code === 'ENOENT' ? 'claude-not-found' : result.error.message
200
+ };
201
+ }
213
202
 
214
- if (!response.ok) {
215
- if (response.status === 404) {
216
- throw new Error('Configuration not found. The config may have expired or the ID is invalid.');
217
- }
218
- if (response.status === 410) {
219
- throw new Error('Configuration has expired (1 year TTL).');
220
- }
221
- throw new Error(`Failed to fetch config: HTTP ${response.status}`);
222
- }
203
+ if (result.status !== 0) {
204
+ return {
205
+ ok: false,
206
+ reason: (result.stderr || result.stdout || '').trim() || 'claude mcp list failed'
207
+ };
208
+ }
223
209
 
224
- const data = await response.json();
225
- return data.config;
226
- } catch (error) {
227
- log(colors.red, `Error fetching shareable config: ${error.message}`);
228
- return null;
210
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
211
+ const installed = new RegExp(`^\\s*${MCP_SERVER_NAME}(\\s|$)`, 'm').test(output);
212
+
213
+ return {
214
+ ok: true,
215
+ installed
216
+ };
217
+ }
218
+
219
+ function ensureAgentfulMcpInstalled() {
220
+ const state = parseMcpInstallState();
221
+ if (!state.ok) {
222
+ return {
223
+ status: 'unavailable',
224
+ reason: state.reason
225
+ };
229
226
  }
227
+
228
+ if (state.installed) {
229
+ return { status: 'already-installed' };
230
+ }
231
+
232
+ const addResult = spawnSync('claude', ['mcp', 'add', MCP_SERVER_NAME, '--', 'npx', '-y', MCP_SERVER_PACKAGE], {
233
+ encoding: 'utf8'
234
+ });
235
+
236
+ if (addResult.error) {
237
+ return {
238
+ status: 'failed',
239
+ reason: addResult.error.message
240
+ };
241
+ }
242
+
243
+ if (addResult.status !== 0) {
244
+ return {
245
+ status: 'failed',
246
+ reason: (addResult.stderr || addResult.stdout || '').trim() || 'claude mcp add failed'
247
+ };
248
+ }
249
+
250
+ const verify = parseMcpInstallState();
251
+ if (!verify.ok || !verify.installed) {
252
+ return {
253
+ status: 'failed',
254
+ reason: !verify.ok ? verify.reason : 'MCP server not visible after install'
255
+ };
256
+ }
257
+
258
+ return { status: 'installed' };
230
259
  }
231
260
 
232
261
  async function init(args) {
@@ -241,77 +270,72 @@ async function init(args) {
241
270
  // Build configuration from preset and/or flags
242
271
  let config = null;
243
272
 
244
- // Check for shareable config first
245
273
  if (flags.config) {
246
- config = await fetchShareableConfig(flags.config);
247
- if (!config) {
248
- log(colors.red, 'Failed to load shareable configuration.');
249
- process.exit(1);
250
- }
251
- log(colors.green, 'Loaded shareable configuration successfully!');
252
- console.log('');
253
- } else {
254
- // Default to "default" preset if no flags provided
255
- let presetConfig = null;
256
- const hasCustomFlags = flags.agents || flags.skills || flags.hooks || flags.gates;
257
-
258
- if (flags.preset) {
259
- // User explicitly specified a preset
260
- presetConfig = getPreset(flags.preset);
261
- if (!presetConfig) {
262
- log(colors.red, `Unknown preset: ${flags.preset}`);
263
- console.log('');
264
- log(colors.dim, 'Available presets:');
265
- listPresets().forEach(p => log(colors.dim, ` - ${p.name}`));
266
- console.log('');
267
- log(colors.dim, 'Run: agentful presets');
268
- process.exit(1);
269
- }
270
- log(colors.dim, `Using preset: ${flags.preset}`);
271
- } else if (!hasCustomFlags) {
272
- // No preset and no custom flags = use default preset
273
- presetConfig = getPreset('default');
274
- log(colors.dim, 'Installing agentful (all components)');
275
- }
276
-
277
- // Parse individual flags
278
- const flagConfig = {
279
- agents: flags.agents ? parseArrayFlag(flags.agents) : null,
280
- skills: flags.skills ? parseArrayFlag(flags.skills) : null,
281
- hooks: flags.hooks ? parseArrayFlag(flags.hooks) : null,
282
- gates: flags.gates ? parseArrayFlag(flags.gates) : null
283
- };
274
+ log(colors.red, '--config is no longer supported.');
275
+ log(colors.dim, 'Use local CLI flags instead: --preset, --agents, --skills, --hooks, --gates');
276
+ process.exit(1);
277
+ }
284
278
 
285
- // Merge preset with flags (flags override preset)
286
- if (presetConfig) {
287
- config = mergePresetWithFlags(presetConfig, flagConfig);
288
- } else {
289
- // Custom configuration with no preset
290
- config = {
291
- agents: flagConfig.agents || ['orchestrator'],
292
- skills: flagConfig.skills || [],
293
- hooks: flagConfig.hooks || [],
294
- gates: flagConfig.gates || []
295
- };
296
- }
279
+ // Default to "default" preset if no flags provided
280
+ let presetConfig = null;
281
+ const hasCustomFlags = flags.agents || flags.skills || flags.hooks || flags.gates;
297
282
 
298
- // Validate configuration
299
- const validation = validateConfiguration(config);
300
- if (!validation.valid) {
301
- log(colors.yellow, 'Configuration warnings:');
302
- validation.errors.forEach(err => log(colors.yellow, ` - ${err}`));
283
+ if (flags.preset) {
284
+ // User explicitly specified a preset
285
+ presetConfig = getPreset(flags.preset);
286
+ if (!presetConfig) {
287
+ log(colors.red, `Unknown preset: ${flags.preset}`);
288
+ console.log('');
289
+ log(colors.dim, 'Available presets:');
290
+ listPresets().forEach(p => log(colors.dim, ` - ${p.name}`));
303
291
  console.log('');
292
+ log(colors.dim, 'Run: agentful presets');
293
+ process.exit(1);
304
294
  }
295
+ log(colors.dim, `Using preset: ${flags.preset}`);
296
+ } else if (!hasCustomFlags) {
297
+ // No preset and no custom flags = use default preset
298
+ presetConfig = getPreset('default');
299
+ log(colors.dim, 'Installing agentful (all components)');
300
+ }
305
301
 
306
- // Show what will be installed
307
- log(colors.dim, 'Configuration:');
308
- log(colors.dim, ` Agents: ${config.agents.join(', ')}`);
309
- log(colors.dim, ` Skills: ${config.skills.join(', ') || 'none'}`);
310
- log(colors.dim, ` Hooks: ${config.hooks.join(', ') || 'none'}`);
311
- log(colors.dim, ` Gates: ${config.gates.join(', ') || 'none'}`);
302
+ // Parse individual flags
303
+ const flagConfig = {
304
+ agents: flags.agents ? parseArrayFlag(flags.agents) : null,
305
+ skills: flags.skills ? parseArrayFlag(flags.skills) : null,
306
+ hooks: flags.hooks ? parseArrayFlag(flags.hooks) : null,
307
+ gates: flags.gates ? parseArrayFlag(flags.gates) : null
308
+ };
309
+
310
+ // Merge preset with flags (flags override preset)
311
+ if (presetConfig) {
312
+ config = mergePresetWithFlags(presetConfig, flagConfig);
313
+ } else {
314
+ // Custom configuration with no preset
315
+ config = {
316
+ agents: flagConfig.agents || ['orchestrator'],
317
+ skills: flagConfig.skills || [],
318
+ hooks: flagConfig.hooks || [],
319
+ gates: flagConfig.gates || []
320
+ };
321
+ }
322
+
323
+ // Validate configuration
324
+ const validation = validateConfiguration(config);
325
+ if (!validation.valid) {
326
+ log(colors.yellow, 'Configuration warnings:');
327
+ validation.errors.forEach(err => log(colors.yellow, ` - ${err}`));
312
328
  console.log('');
313
329
  }
314
330
 
331
+ // Show what will be installed
332
+ log(colors.dim, 'Configuration:');
333
+ log(colors.dim, ` Agents: ${config.agents.join(', ')}`);
334
+ log(colors.dim, ` Skills: ${config.skills.join(', ') || 'none'}`);
335
+ log(colors.dim, ` Hooks: ${config.hooks.join(', ') || 'none'}`);
336
+ log(colors.dim, ` Gates: ${config.gates.join(', ') || 'none'}`);
337
+ console.log('');
338
+
315
339
  // Check if already initialized
316
340
  if (await isInitialized(targetDir)) {
317
341
  log(colors.yellow, 'agentful is already initialized in this directory!');
@@ -391,6 +415,28 @@ async function init(args) {
391
415
  // Update .gitignore
392
416
  checkGitignore();
393
417
 
418
+ // Configure Agentful MCP server by default (non-fatal)
419
+ const skipMcpSetup = Boolean(flags['skip-mcp']);
420
+ const mcpSetupResult = skipMcpSetup ? { status: 'skipped' } : ensureAgentfulMcpInstalled();
421
+
422
+ if (skipMcpSetup) {
423
+ log(colors.dim, 'Skipped MCP setup (--skip-mcp)');
424
+ console.log('');
425
+ } else if (mcpSetupResult.status === 'installed') {
426
+ log(colors.green, '✓ Agentful MCP server configured');
427
+ console.log('');
428
+ } else if (mcpSetupResult.status === 'already-installed') {
429
+ log(colors.green, '✓ Agentful MCP server already configured');
430
+ console.log('');
431
+ } else {
432
+ log(colors.yellow, '⚠ Could not auto-configure Agentful MCP server');
433
+ if (mcpSetupResult.reason) {
434
+ log(colors.dim, ` Reason: ${mcpSetupResult.reason}`);
435
+ }
436
+ log(colors.dim, ` Run manually: ${MCP_SETUP_COMMAND}`);
437
+ console.log('');
438
+ }
439
+
394
440
  // Show next steps
395
441
  console.log('');
396
442
  log(colors.bright, 'Next Steps:');
@@ -411,13 +457,23 @@ async function init(args) {
411
457
  }
412
458
  log(colors.dim, 'Optional: Edit CLAUDE.md and .claude/product/index.md first to customize.');
413
459
  console.log('');
414
- log(colors.bright, 'Recommended: Enable Pattern Learning');
415
- console.log('');
416
- log(colors.dim, ' Agents get smarter when they can store and reuse patterns.');
417
- log(colors.dim, ' Run this once to enable:');
418
- console.log('');
419
- log(colors.cyan, ' claude mcp add agentful -- npx -y @itz4blitz/agentful-mcp-server');
420
- console.log('');
460
+ if (skipMcpSetup) {
461
+ log(colors.bright, 'Pattern Learning (Skipped)');
462
+ console.log('');
463
+ log(colors.dim, ' You skipped automatic MCP setup with --skip-mcp.');
464
+ log(colors.dim, ' Enable it later with:');
465
+ console.log('');
466
+ log(colors.cyan, ` ${MCP_SETUP_COMMAND}`);
467
+ console.log('');
468
+ } else if (mcpSetupResult.status !== 'installed' && mcpSetupResult.status !== 'already-installed') {
469
+ log(colors.bright, 'Pattern Learning (Manual Step Required)');
470
+ console.log('');
471
+ log(colors.dim, ' Agents get smarter when they can store and reuse patterns.');
472
+ log(colors.dim, ' Run this once to enable:');
473
+ console.log('');
474
+ log(colors.cyan, ` ${MCP_SETUP_COMMAND}`);
475
+ console.log('');
476
+ }
421
477
  }
422
478
 
423
479
  function showStatus() {
@@ -239,4 +239,6 @@ function markForReanalysis(arch, reasons) {
239
239
 
240
240
  // Run detection
241
241
  const driftDetected = detectArchitectDrift();
242
- process.exit(driftDetected ? 1 : 0);
242
+ // Always exit 0 - drift is informational, not an error
243
+ // Non-zero exit codes are interpreted as hook errors by Claude Code
244
+ process.exit(0);
@@ -12,7 +12,6 @@
12
12
  * - .claude/skills/*\/SKILL.md (skill documentation)
13
13
  * - .claude/product/**\/*.md (product specifications)
14
14
  * - template/**\/*.md (template files)
15
- * - examples/**\/*.md (example documentation)
16
15
  *
17
16
  * Blocked:
18
17
  * - Random *.md files in project root
@@ -42,7 +41,6 @@ const ALLOWED_PATTERNS = [
42
41
  /^\.claude\/skills\/[^\/]+\/SKILL\.md$/, // Skill docs
43
42
  /^\.claude\/product\/.*\.md$/, // Product specs
44
43
  /^template\/.*\.md$/, // Template files
45
- /^examples\/.*\.md$/, // Examples
46
44
  /^\.agentful\/.*\.md$/ // Internal agentful state docs (rare)
47
45
  ];
48
46
 
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Ensure Worktree Hook
5
+ *
6
+ * PreToolUse hook that enforces git worktree usage for file modifications.
7
+ *
8
+ * Modes:
9
+ * off - Allow all edits (backward compatible)
10
+ * block - Require existing worktree, reject if not in one
11
+ * auto - Create worktree automatically if not in one
12
+ *
13
+ * Environment Variables:
14
+ * AGENTFUL_WORKTREE_MODE - off|block|auto (default: auto)
15
+ * AGENTFUL_WORKTREE_DIR - Where to create worktrees (default: ../)
16
+ * AGENTFUL_WORKTREE_AUTO_CLEANUP - Auto-remove after completion (default: true)
17
+ * AGENTFUL_WORKTREE_RETENTION_DAYS - Days before cleanup (default: 7)
18
+ * AGENTFUL_WORKTREE_MAX_ACTIVE - Max active worktrees (default: 5)
19
+ *
20
+ * To disable this hook:
21
+ * Temporary: export AGENTFUL_WORKTREE_MODE=off
22
+ * Permanent: Remove from .claude/settings.json PreToolUse hooks
23
+ * Customize: Edit bin/hooks/ensure-worktree.js
24
+ */
25
+
26
+ import fs from 'fs';
27
+ import path from 'path';
28
+ import { execSync } from 'child_process';
29
+ import { fileURLToPath } from 'url';
30
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
31
+
32
+ // Get configuration
33
+ const MODE = process.env.AGENTFUL_WORKTREE_MODE || 'auto';
34
+ const WORKTREE_DIR = process.env.AGENTFUL_WORKTREE_DIR || '../';
35
+ const AUTO_CLEANUP = process.env.AGENTFUL_WORKTREE_AUTO_CLEANUP !== 'false';
36
+ const RETENTION_DAYS = parseInt(process.env.AGENTFUL_WORKTREE_RETENTION_DAYS || '7', 10);
37
+ const MAX_ACTIVE = parseInt(process.env.AGENTFUL_WORKTREE_MAX_ACTIVE || '5', 10);
38
+ const AGENT_TYPE = process.env.AGENTFUL_AGENT_TYPE || 'general';
39
+ const TASK_TYPE = process.env.AGENTFUL_TASK_TYPE || 'general';
40
+
41
+ /**
42
+ * Detect if currently in a git worktree
43
+ */
44
+ function isInWorktree() {
45
+ try {
46
+ const cwd = process.cwd();
47
+ const gitFile = path.join(cwd, '.git');
48
+
49
+ // If .git is a file (not a directory), we're in a worktree
50
+ if (existsSync(gitFile) && statSync(gitFile).isFile()) {
51
+ return true;
52
+ }
53
+ } catch (error) {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Determine worktree purpose from agent/task context
60
+ */
61
+ function determinePurpose() {
62
+ // Agent-based purposes
63
+ const agentPurposes = {
64
+ 'fixer': 'fix',
65
+ 'reviewer': 'review',
66
+ 'tester': 'test',
67
+ 'backend': 'backend',
68
+ 'frontend': 'frontend',
69
+ 'architect': 'architect',
70
+ 'orchestrator': 'orchestrator',
71
+ };
72
+
73
+ // Task-based purposes
74
+ const taskPurposes = {
75
+ 'feature': 'feature',
76
+ 'hotfix': 'hotfix',
77
+ 'bugfix': 'bugfix',
78
+ 'experiment': 'experiment',
79
+ };
80
+
81
+ // Determine purpose based on agent type, then task type
82
+ if (agentPurposes[AGENT_TYPE]) {
83
+ return agentPurposes[AGENT_TYPE];
84
+ }
85
+
86
+ if (taskPurposes[TASK_TYPE]) {
87
+ return taskPurposes[TASK_TYPE];
88
+ }
89
+
90
+ // Default to general
91
+ return 'general';
92
+ }
93
+
94
+ /**
95
+ * Show block message with helpful error
96
+ */
97
+ function showBlockMessage() {
98
+ const branch = getCurrentBranch();
99
+
100
+ console.error(`
101
+ :::error
102
+ ═════════════════════════════════════════════════
103
+ 🚫 Blocked: Direct Repository Edits
104
+ ═════════════════════════════════════════════════
105
+
106
+ You are attempting to edit files in the root repository, but AGENTFUL_WORKTREE_MODE
107
+ is set to "block" which requires working in a git worktree.
108
+
109
+ Current branch: ${branch}
110
+ Current directory: ${process.cwd()}
111
+
112
+ To proceed, choose one option:
113
+
114
+ 1. 🌳 Create a worktree (recommended):
115
+ git worktree add ../my-worktree -b ${branch}
116
+ cd ../my-worktree
117
+
118
+ 2. ⚙️ Change mode to auto (creates worktrees automatically):
119
+ export AGENTFUL_WORKTREE_MODE=auto
120
+
121
+ 3. 🚫 Disable worktree protection (not recommended):
122
+ export AGENTFUL_WORKTREE_MODE=off
123
+
124
+ For more information, see: /agentful-worktree or docs/pages/concepts/git-worktrees.mdx
125
+ :::`);
126
+
127
+ process.exit(1);
128
+ }
129
+
130
+ /**
131
+ * Create worktree automatically
132
+ */
133
+ function createWorktree() {
134
+ const repoRoot = findRepoRoot();
135
+ if (!repoRoot) {
136
+ console.error(':::error Not in a git repository:::');
137
+ process.exit(1);
138
+ }
139
+
140
+ const purpose = determinePurpose();
141
+ const branch = getCurrentBranch();
142
+ const timestamp = Date.now();
143
+
144
+ // Generate worktree name
145
+ const worktreeName = `agentful-${purpose}-${sanitizeBranchName(branch)}-${timestamp}`;
146
+
147
+ // Create worktree
148
+ const worktreePath = path.join(repoRoot, WORKTREE_DIR, worktreeName);
149
+
150
+ console.log(`🌳 Creating worktree: ${worktreeName}`);
151
+ console.log(` Branch: ${branch}`);
152
+ console.log(` Path: ${worktreePath}`);
153
+
154
+ try {
155
+ execSync(
156
+ `git worktree add "${worktreePath}" -b "${branch}"`,
157
+ { cwd: repoRoot, stdio: 'inherit' }
158
+ );
159
+ } catch (error) {
160
+ console.error(`:::error Failed to create worktree: ${error.message}:::`);
161
+ process.exit(1);
162
+ }
163
+
164
+ // Track in state.json (would happen in orchestrator, but this is standalone)
165
+ try {
166
+ const stateFile = path.join(repoRoot, '.agentful', 'state.json');
167
+ if (existsSync(stateFile)) {
168
+ const state = JSON.parse(readFileSync(stateFile, 'utf8'));
169
+ state.current_worktree = {
170
+ name: worktreeName,
171
+ path: worktreePath,
172
+ branch: branch,
173
+ purpose: purpose,
174
+ created_at: new Date().toISOString(),
175
+ };
176
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
177
+ }
178
+ } catch (error) {
179
+ // State file might not exist yet, that's okay
180
+ }
181
+
182
+ // Export for caller to capture
183
+ console.log(`export AGENTFUL_WORKTREE_DIR="${worktreePath}"`);
184
+
185
+ process.exit(0);
186
+ }
187
+
188
+ /**
189
+ * Main execution
190
+ */
191
+ (() => {
192
+ // Get tool and file from environment
193
+ const tool = process.env.TOOL || '';
194
+ const file = process.env.FILE || '';
195
+
196
+ // Only check Write and Edit tools
197
+ if (tool !== 'Write' && tool !== 'Edit') {
198
+ process.exit(0);
199
+ }
200
+
201
+ // Auto-disable in CI environments
202
+ if (isCIEnvironment()) {
203
+ process.exit(0);
204
+ }
205
+
206
+ // Check if already in worktree (explicitly set or detected)
207
+ if (WORKTREE_DIR) {
208
+ // In worktree - allow operation
209
+ process.exit(0);
210
+ }
211
+
212
+ // Not in worktree - handle based on mode
213
+ switch (MODE) {
214
+ case 'off':
215
+ // Allow all edits silently
216
+ process.exit(0);
217
+ break;
218
+
219
+ case 'block':
220
+ // Require existing worktree
221
+ showBlockMessage();
222
+ break;
223
+
224
+ case 'auto':
225
+ // Create worktree automatically
226
+ createWorktree();
227
+ break;
228
+
229
+ default:
230
+ console.error(`:::warning Invalid AGENTFUL_WORKTREE_MODE: ${MODE}:::`);
231
+ console.error(`:::warning Valid options: off, block, auto:::`);
232
+ process.exit(1);
233
+ }
234
+ })();
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Health Check Hook
4
+ *
5
+ * Verifies the "agentful" MCP server is configured in Claude Code.
6
+ * Non-blocking: warns only, never exits with failure.
7
+ */
8
+
9
+ import { spawnSync } from 'child_process';
10
+
11
+ const MCP_NAME = 'agentful';
12
+ const SETUP_COMMAND = 'claude mcp add agentful -- npx -y @itz4blitz/agentful-mcp-server';
13
+
14
+ function runClaudeMcpList() {
15
+ return spawnSync('claude', ['mcp', 'list'], {
16
+ encoding: 'utf8'
17
+ });
18
+ }
19
+
20
+ function hasAgentfulMcp(text) {
21
+ return new RegExp(`^\\s*${MCP_NAME}(\\s|$)`, 'm').test(text);
22
+ }
23
+
24
+ function printMissingMcpWarning() {
25
+ console.log('⚠️ Agentful MCP server is not configured.');
26
+ console.log(` Run: ${SETUP_COMMAND}`);
27
+ }
28
+
29
+ (() => {
30
+ const result = runClaudeMcpList();
31
+
32
+ if (result.error) {
33
+ if (result.error.code === 'ENOENT') {
34
+ console.log('⚠️ Claude CLI not found; MCP setup check skipped.');
35
+ return;
36
+ }
37
+
38
+ if (process.env.VERBOSE) {
39
+ console.log(`⚠️ MCP setup check failed: ${result.error.message}`);
40
+ }
41
+ return;
42
+ }
43
+
44
+ if (result.status !== 0) {
45
+ if (process.env.VERBOSE) {
46
+ const reason = (result.stderr || result.stdout || '').trim();
47
+ console.log(`⚠️ MCP setup check failed${reason ? `: ${reason}` : ''}`);
48
+ }
49
+ return;
50
+ }
51
+
52
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
53
+ if (!hasAgentfulMcp(output)) {
54
+ printMissingMcpWarning();
55
+ }
56
+ })();