@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 +0 -3
- package/bin/cli.js +165 -109
- package/bin/hooks/architect-drift-detector.js +3 -1
- package/bin/hooks/block-random-docs.js +0 -2
- package/bin/hooks/ensure-worktree.js +234 -0
- package/bin/hooks/mcp-health-check.js +56 -0
- package/bin/hooks/package-metadata-guard.js +118 -0
- package/bin/hooks/session-start.js +32 -5
- package/bin/hooks/worktree-service.js +514 -0
- package/lib/presets.js +22 -1
- package/package.json +7 -8
- package/template/.claude/agents/backend.md +19 -0
- package/template/.claude/agents/frontend.md +19 -0
- package/template/.claude/agents/orchestrator.md +182 -0
- package/template/.claude/agents/reviewer.md +18 -0
- package/template/.claude/commands/agentful-worktree.md +106 -0
- package/template/.claude/settings.json +32 -9
- package/template/CLAUDE.md +37 -2
- package/template/bin/hooks/block-random-docs.js +0 -1
- package/version.json +1 -1
package/README.md
CHANGED
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
+
function parseMcpInstallState() {
|
|
192
|
+
const result = spawnSync('claude', ['mcp', 'list'], {
|
|
193
|
+
encoding: 'utf8'
|
|
194
|
+
});
|
|
211
195
|
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
+
})();
|