@itz4blitz/agentful 1.8.1 → 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/mcp-health-check.js +56 -0
- package/bin/hooks/package-metadata-guard.js +118 -0
- package/bin/hooks/session-start.js +32 -5
- package/lib/presets.js +22 -1
- package/package.json +6 -8
- package/template/.claude/settings.json +20 -9
- package/template/CLAUDE.md +10 -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,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
|
+
})();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Package Metadata Guard Hook
|
|
5
|
+
*
|
|
6
|
+
* Protects package.json metadata from accidental ownership corruption.
|
|
7
|
+
*
|
|
8
|
+
* Catches:
|
|
9
|
+
* - repository.type being changed away from "git"
|
|
10
|
+
* - repository.url being changed to github.com/itz4blitz/* in repos owned by someone else
|
|
11
|
+
*
|
|
12
|
+
* Run: PostToolUse (Write|Edit)
|
|
13
|
+
* Action: Exit non-zero so the agent corrects the change immediately.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
|
|
20
|
+
const AGENTFUL_OWNER = 'itz4blitz';
|
|
21
|
+
|
|
22
|
+
function getOriginOwner() {
|
|
23
|
+
try {
|
|
24
|
+
const remote = execSync('git config --get remote.origin.url', {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
27
|
+
}).trim();
|
|
28
|
+
|
|
29
|
+
if (!remote) return null;
|
|
30
|
+
|
|
31
|
+
const httpsMatch = remote.match(/github\.com[:/]+([^/]+)\/[^/]+(?:\.git)?$/i);
|
|
32
|
+
if (httpsMatch?.[1]) {
|
|
33
|
+
return httpsMatch[1].toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveFilePath(filePath) {
|
|
43
|
+
if (!filePath) return null;
|
|
44
|
+
return path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isPackageJsonTarget(filePath) {
|
|
48
|
+
if (!filePath) return false;
|
|
49
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
50
|
+
return path.basename(normalized) === 'package.json' && !normalized.includes('/node_modules/');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inspectPackageMetadata(packageJsonPath, originOwner) {
|
|
54
|
+
const issues = [];
|
|
55
|
+
|
|
56
|
+
let pkg;
|
|
57
|
+
try {
|
|
58
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
59
|
+
} catch {
|
|
60
|
+
return issues;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!pkg.repository || typeof pkg.repository !== 'object') {
|
|
64
|
+
return issues;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const repoType = pkg.repository.type;
|
|
68
|
+
if (typeof repoType === 'string' && repoType.trim().toLowerCase() !== 'git') {
|
|
69
|
+
issues.push(`repository.type is "${repoType}" (expected "git")`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const repoUrl = pkg.repository.url;
|
|
73
|
+
if (typeof repoUrl === 'string') {
|
|
74
|
+
const ownerInUrl = repoUrl.match(/github\.com[:/]+([^/]+)\//i)?.[1]?.toLowerCase();
|
|
75
|
+
if (ownerInUrl === AGENTFUL_OWNER && originOwner && originOwner !== AGENTFUL_OWNER) {
|
|
76
|
+
issues.push(`repository.url points to "${AGENTFUL_OWNER}" but git remote owner is "${originOwner}"`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return issues;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
const filePath = process.env.FILE || '';
|
|
85
|
+
const toolName = process.env.TOOL_NAME || '';
|
|
86
|
+
|
|
87
|
+
if (toolName !== 'Write' && toolName !== 'Edit') {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isPackageJsonTarget(filePath)) {
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const absolutePath = resolveFilePath(filePath);
|
|
96
|
+
if (!absolutePath || !fs.existsSync(absolutePath)) {
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const originOwner = getOriginOwner();
|
|
101
|
+
const issues = inspectPackageMetadata(absolutePath, originOwner);
|
|
102
|
+
|
|
103
|
+
if (issues.length === 0) {
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.error('\n❌ BLOCKED: Suspicious package.json metadata change\n');
|
|
108
|
+
console.error(`File: ${filePath}`);
|
|
109
|
+
for (const issue of issues) {
|
|
110
|
+
console.error(`- ${issue}`);
|
|
111
|
+
}
|
|
112
|
+
console.error('\nExpected behavior: preserve project ownership metadata and keep repository.type as "git".');
|
|
113
|
+
console.error('Action: restore the package.json metadata to match the current project repository.\n');
|
|
114
|
+
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main();
|
|
@@ -29,15 +29,42 @@ try {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Detect if TeammateTool (parallel execution) is enabled
|
|
32
|
+
* Supports Windows, macOS, and Linux
|
|
32
33
|
*/
|
|
33
34
|
function detectParallelExecution() {
|
|
35
|
+
// Check environment variable first (user can set AGENTFUL_PARALLEL=true)
|
|
36
|
+
if (process.env.AGENTFUL_PARALLEL === 'true') {
|
|
37
|
+
return { enabled: true, method: 'env_var' };
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
try {
|
|
35
|
-
// Find Claude Code binary
|
|
36
|
-
|
|
37
|
-
const
|
|
41
|
+
// Find Claude Code binary - try multiple paths for Windows/Unix
|
|
42
|
+
let cliPath = null;
|
|
43
|
+
const possiblePaths = [
|
|
44
|
+
// Unix npm global
|
|
45
|
+
'/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
46
|
+
// Homebrew on macOS
|
|
47
|
+
'/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// npm root -g can throw on Windows if npm isn't in PATH
|
|
51
|
+
try {
|
|
52
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
53
|
+
possiblePaths.unshift(path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js'));
|
|
54
|
+
} catch {
|
|
55
|
+
// npm not available - continue with static paths
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const p of possiblePaths) {
|
|
59
|
+
if (fs.existsSync(p)) {
|
|
60
|
+
cliPath = p;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
38
64
|
|
|
39
|
-
if (!
|
|
40
|
-
|
|
65
|
+
if (!cliPath) {
|
|
66
|
+
// Assume enabled if we can't find CLI (newer versions have it by default)
|
|
67
|
+
return { enabled: true, method: 'assumed' };
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
// Check for TeammateTool pattern
|
package/lib/presets.js
CHANGED
|
@@ -19,7 +19,7 @@ export const presets = {
|
|
|
19
19
|
description: 'Complete agentful installation (recommended)',
|
|
20
20
|
agents: ['orchestrator', 'architect', 'backend', 'frontend', 'tester', 'reviewer', 'fixer', 'product-analyzer'],
|
|
21
21
|
skills: ['product-tracking', 'validation', 'testing', 'conversation', 'product-planning', 'deployment', 'research'],
|
|
22
|
-
hooks: ['session-start', 'health-check', 'block-random-docs', 'block-file-creation', 'product-spec-watcher', 'architect-drift-detector', 'analyze-trigger'],
|
|
22
|
+
hooks: ['session-start', 'health-check', 'mcp-health-check', 'block-random-docs', 'block-file-creation', 'product-spec-watcher', 'architect-drift-detector', 'analyze-trigger', 'package-metadata-guard'],
|
|
23
23
|
gates: ['types', 'tests', 'coverage', 'lint', 'security', 'dead-code']
|
|
24
24
|
},
|
|
25
25
|
|
|
@@ -47,6 +47,16 @@ export const hookConfigurations = {
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
|
|
50
|
+
'mcp-health-check': {
|
|
51
|
+
event: 'SessionStart',
|
|
52
|
+
config: {
|
|
53
|
+
type: 'command',
|
|
54
|
+
command: 'node bin/hooks/mcp-health-check.js',
|
|
55
|
+
timeout: 3,
|
|
56
|
+
description: 'Verify agentful MCP server is configured'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
50
60
|
'block-random-docs': {
|
|
51
61
|
event: 'PreToolUse',
|
|
52
62
|
matcher: 'Write',
|
|
@@ -151,6 +161,17 @@ export const hookConfigurations = {
|
|
|
151
161
|
timeout: 3,
|
|
152
162
|
description: 'Watch for product spec changes and auto-trigger generation'
|
|
153
163
|
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
'package-metadata-guard': {
|
|
167
|
+
event: 'PostToolUse',
|
|
168
|
+
matcher: 'Write|Edit',
|
|
169
|
+
config: {
|
|
170
|
+
type: 'command',
|
|
171
|
+
command: 'node bin/hooks/package-metadata-guard.js',
|
|
172
|
+
timeout: 3,
|
|
173
|
+
description: 'Protect package.json ownership metadata from accidental corruption'
|
|
174
|
+
}
|
|
154
175
|
}
|
|
155
176
|
};
|
|
156
177
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itz4blitz/agentful",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Pre-configured AI toolkit with self-hosted execution - works with any LLM, any tech stack, any platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,16 +16,14 @@
|
|
|
16
16
|
"lint": "eslint .",
|
|
17
17
|
"release": "semantic-release",
|
|
18
18
|
"release:dry-run": "semantic-release --dry-run",
|
|
19
|
-
"docs:dev": "vocs dev",
|
|
19
|
+
"docs:dev": "vocs dev --host 127.0.0.1 --port 8080",
|
|
20
20
|
"docs:build": "vocs build",
|
|
21
21
|
"docs:preview": "vocs preview",
|
|
22
|
-
"build:all": "npm run build:
|
|
23
|
-
"build:cli": "cd packages/cli && npm run build",
|
|
24
|
-
"build:studio": "cd packages/studio && npm run build",
|
|
22
|
+
"build:all": "npm run build:mcp",
|
|
25
23
|
"build:mcp": "cd packages/mcp-server && npm run build",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"install:all": "npm install && cd packages/
|
|
24
|
+
"test:mcp": "cd packages/mcp-server && npm run test",
|
|
25
|
+
"dev:mcp": "cd packages/mcp-server && npm run dev",
|
|
26
|
+
"install:all": "npm install && cd packages/mcp-server && npm install"
|
|
29
27
|
},
|
|
30
28
|
"exports": {
|
|
31
29
|
".": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"PreToolUse": [
|
|
27
27
|
{
|
|
28
|
-
"
|
|
28
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
29
29
|
"hooks": [
|
|
30
30
|
{
|
|
31
31
|
"type": "command",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
]
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
"
|
|
39
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
40
40
|
"hooks": [
|
|
41
41
|
{
|
|
42
42
|
"type": "command",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
]
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
|
-
"
|
|
50
|
+
"matcher": "Write",
|
|
51
51
|
"hooks": [
|
|
52
52
|
{
|
|
53
53
|
"type": "command",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
],
|
|
61
61
|
"PostToolUse": [
|
|
62
62
|
{
|
|
63
|
-
"matcher": "Write|Edit",
|
|
63
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
64
64
|
"hooks": [
|
|
65
65
|
{
|
|
66
66
|
"type": "command",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
]
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
|
-
"matcher": "Write|Edit",
|
|
74
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
75
75
|
"hooks": [
|
|
76
76
|
{
|
|
77
77
|
"type": "command",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
]
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
|
-
"matcher": "Write|Edit",
|
|
85
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
86
86
|
"hooks": [
|
|
87
87
|
{
|
|
88
88
|
"type": "command",
|
|
@@ -93,7 +93,18 @@
|
|
|
93
93
|
]
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
|
-
"matcher": "Write|Edit",
|
|
96
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
97
|
+
"hooks": [
|
|
98
|
+
{
|
|
99
|
+
"type": "command",
|
|
100
|
+
"command": "node bin/hooks/package-metadata-guard.js",
|
|
101
|
+
"timeout": 3,
|
|
102
|
+
"description": "Protect package.json ownership metadata from accidental corruption"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
97
108
|
"hooks": [
|
|
98
109
|
{
|
|
99
110
|
"type": "command",
|
|
@@ -102,7 +113,7 @@
|
|
|
102
113
|
]
|
|
103
114
|
},
|
|
104
115
|
{
|
|
105
|
-
"matcher": "Write|Edit",
|
|
116
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
106
117
|
"hooks": [
|
|
107
118
|
{
|
|
108
119
|
"type": "command",
|
|
@@ -132,4 +143,4 @@
|
|
|
132
143
|
"Bash(mkfs:*)"
|
|
133
144
|
]
|
|
134
145
|
}
|
|
135
|
-
}
|
|
146
|
+
}
|
package/template/CLAUDE.md
CHANGED
|
@@ -20,7 +20,7 @@ npx @itz4blitz/agentful init --agents=orchestrator,backend --skills=validation
|
|
|
20
20
|
npx @itz4blitz/agentful presets
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Use `npx @itz4blitz/agentful presets` to see local installation options.
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
@@ -78,6 +78,15 @@ Without MCP: agents start from scratch every session. With MCP: agents compound
|
|
|
78
78
|
|
|
79
79
|
See [Hooks System](#hooks-system) below for manual setup or other editors.
|
|
80
80
|
|
|
81
|
+
## Metadata Safety (Critical)
|
|
82
|
+
|
|
83
|
+
When editing `package.json` (or other repo metadata files), preserve project ownership:
|
|
84
|
+
|
|
85
|
+
- Never change `repository.url`, `homepage`, `bugs.url`, `name` scope, or `author` to agentful maintainer values unless the project explicitly asks.
|
|
86
|
+
- Keep `repository.type` as `"git"` when a `repository` object exists.
|
|
87
|
+
- Do not infer repo owner from agentful package names/docs (`@itz4blitz/agentful`); derive owner from the current repository remote instead.
|
|
88
|
+
- Prefer structured JSON edits (JSON path / `jq` / `npm pkg`) instead of broad text replace on `"type"` or `"url"`.
|
|
89
|
+
|
|
81
90
|
## Commands
|
|
82
91
|
|
|
83
92
|
| Command | Description | When to Use |
|
|
@@ -286,7 +295,6 @@ Agentful uses Claude Code hooks for automation, protection, and intelligent cont
|
|
|
286
295
|
- ✅ `.claude/skills/*/SKILL.md` - Skill documentation
|
|
287
296
|
- ✅ `.claude/product/**/*.md` - Product specifications
|
|
288
297
|
- ✅ `template/**/*.md` - Template files
|
|
289
|
-
- ✅ `examples/**/*.md` - Example documentation
|
|
290
298
|
- **Allowed if parent directory exists**:
|
|
291
299
|
- 📁 `docs/*.md`, `docs/pages/*.mdx` - Requires `docs/` directory
|
|
292
300
|
- 📁 `documentation/*.md` - Requires `documentation/` directory
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* - .claude/skills/*\/SKILL.md (skill documentation)
|
|
17
17
|
* - .claude/product/**\/*.md (product specifications)
|
|
18
18
|
* - template/**\/*.md (template files)
|
|
19
|
-
* - examples/**\/*.md (example documentation)
|
|
20
19
|
*
|
|
21
20
|
* Blocked:
|
|
22
21
|
* - Random *.md files in project root
|
package/version.json
CHANGED