@itz4blitz/agentful 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -16
- package/bin/cli.js +1031 -47
- package/bin/hooks/README.md +338 -82
- package/bin/hooks/analyze-trigger.js +69 -0
- package/bin/hooks/block-random-docs.js +77 -0
- package/bin/hooks/health-check.js +153 -0
- package/bin/hooks/post-agent.js +101 -0
- package/bin/hooks/post-feature.js +227 -0
- package/bin/hooks/pre-agent.js +118 -0
- package/bin/hooks/pre-feature.js +138 -0
- package/lib/VALIDATION_README.md +455 -0
- package/lib/atomic.js +350 -0
- package/lib/ci/claude-action-integration.js +641 -0
- package/lib/ci/index.js +10 -0
- package/lib/core/CLAUDE_EXECUTOR.md +371 -0
- package/lib/core/README.md +321 -0
- package/lib/core/analyzer.js +497 -0
- package/lib/core/claude-executor.example.js +210 -0
- package/lib/core/claude-executor.js +1046 -0
- package/lib/core/cli.js +141 -0
- package/lib/core/detectors/conventions.js +342 -0
- package/lib/core/detectors/framework.js +276 -0
- package/lib/core/detectors/index.js +15 -0
- package/lib/core/detectors/language.js +199 -0
- package/lib/core/detectors/patterns.js +356 -0
- package/lib/core/generator.js +626 -0
- package/lib/core/index.js +9 -0
- package/lib/core/output-parser.example.js +250 -0
- package/lib/core/output-parser.js +458 -0
- package/lib/core/storage.js +515 -0
- package/lib/core/templates.js +556 -0
- package/lib/index.js +32 -0
- package/lib/init.js +252 -21
- package/lib/pipeline/cli.js +423 -0
- package/lib/pipeline/engine.js +928 -0
- package/lib/pipeline/executor.js +440 -0
- package/lib/pipeline/index.js +33 -0
- package/lib/pipeline/integrations.js +559 -0
- package/lib/pipeline/schemas.js +288 -0
- package/lib/presets.js +207 -0
- package/lib/remote/client.js +361 -0
- package/lib/server/auth.js +286 -0
- package/lib/server/client-example.js +190 -0
- package/lib/server/executor.js +426 -0
- package/lib/server/index.js +469 -0
- package/lib/update-helpers.js +505 -0
- package/lib/validation.js +460 -0
- package/package.json +19 -2
- package/template/.claude/agents/architect.md +260 -0
- package/template/.claude/agents/backend.md +203 -0
- package/template/.claude/agents/fixer.md +244 -0
- package/template/.claude/agents/frontend.md +232 -0
- package/template/.claude/agents/orchestrator.md +528 -0
- package/template/.claude/agents/product-analyzer.md +1130 -0
- package/template/.claude/agents/reviewer.md +229 -0
- package/template/.claude/agents/tester.md +242 -0
- package/{.claude → template/.claude}/commands/agentful-analyze.md +151 -43
- package/template/.claude/commands/agentful-decide.md +470 -0
- package/{.claude → template/.claude}/commands/agentful-product.md +89 -5
- package/template/.claude/commands/agentful-start.md +432 -0
- package/{.claude → template/.claude}/commands/agentful-status.md +88 -3
- package/template/.claude/commands/agentful-update.md +402 -0
- package/template/.claude/commands/agentful-validate.md +369 -0
- package/{.claude → template/.claude}/commands/agentful.md +110 -183
- package/template/.claude/product/EXAMPLES.md +167 -0
- package/{.claude → template/.claude}/settings.json +9 -13
- package/{.claude → template/.claude}/skills/conversation/SKILL.md +13 -7
- package/template/.claude/skills/deployment/SKILL.md +116 -0
- package/template/.claude/skills/product-planning/SKILL.md +463 -0
- package/template/.claude/skills/testing/SKILL.md +228 -0
- package/template/.claude/skills/validation/SKILL.md +650 -0
- package/template/CLAUDE.md +73 -5
- package/template/bin/hooks/block-random-docs.js +121 -0
- package/version.json +1 -1
- package/.claude/agents/architect.md +0 -524
- package/.claude/agents/backend.md +0 -315
- package/.claude/agents/fixer.md +0 -263
- package/.claude/agents/frontend.md +0 -274
- package/.claude/agents/orchestrator.md +0 -283
- package/.claude/agents/product-analyzer.md +0 -792
- package/.claude/agents/reviewer.md +0 -332
- package/.claude/agents/tester.md +0 -410
- package/.claude/commands/agentful-decide.md +0 -214
- package/.claude/commands/agentful-start.md +0 -182
- package/.claude/commands/agentful-validate.md +0 -127
- package/.claude/product/EXAMPLES.md +0 -610
- package/.claude/product/README.md +0 -326
- package/.claude/skills/validation/SKILL.md +0 -271
- package/bin/hooks/analyze-trigger.sh +0 -57
- package/bin/hooks/health-check.sh +0 -36
- /package/{.claude → template/.claude}/commands/agentful-generate.md +0 -0
- /package/{.claude → template/.claude}/product/index.md +0 -0
- /package/{.claude → template/.claude}/skills/product-tracking/SKILL.md +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// health-check.js
|
|
3
|
+
// Comprehensive startup health check for agentful
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
let errors = 0;
|
|
9
|
+
let warnings = 0;
|
|
10
|
+
|
|
11
|
+
// === CRITICAL CHECKS (must pass) ===
|
|
12
|
+
|
|
13
|
+
// Check 1: .agentful directory
|
|
14
|
+
if (!fs.existsSync('.agentful')) {
|
|
15
|
+
console.log('❌ Agentful not initialized.');
|
|
16
|
+
console.log(' Run: npx @itz4blitz/agentful init');
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check 2: Core state files
|
|
21
|
+
const stateFiles = ['state.json', 'completion.json', 'decisions.json'];
|
|
22
|
+
for (const file of stateFiles) {
|
|
23
|
+
const filePath = `.agentful/${file}`;
|
|
24
|
+
if (!fs.existsSync(filePath)) {
|
|
25
|
+
console.log(`❌ Missing .agentful/${file}`);
|
|
26
|
+
errors++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check 3: .claude directory structure
|
|
31
|
+
const claudeDirs = ['agents', 'commands', 'product', 'skills'];
|
|
32
|
+
for (const dir of claudeDirs) {
|
|
33
|
+
const dirPath = `.claude/${dir}`;
|
|
34
|
+
if (!fs.existsSync(dirPath)) {
|
|
35
|
+
console.log(`❌ Missing .claude/${dir}/`);
|
|
36
|
+
errors++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check 4: Core agents
|
|
41
|
+
const coreAgents = [
|
|
42
|
+
'orchestrator',
|
|
43
|
+
'backend',
|
|
44
|
+
'frontend',
|
|
45
|
+
'tester',
|
|
46
|
+
'reviewer',
|
|
47
|
+
'fixer',
|
|
48
|
+
'architect',
|
|
49
|
+
'product-analyzer'
|
|
50
|
+
];
|
|
51
|
+
for (const agent of coreAgents) {
|
|
52
|
+
const agentPath = `.claude/agents/${agent}.md`;
|
|
53
|
+
if (!fs.existsSync(agentPath)) {
|
|
54
|
+
console.log(`❌ Missing core agent: ${agentPath}`);
|
|
55
|
+
errors++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check 5: Product specification
|
|
60
|
+
if (!fs.existsSync('.claude/product/index.md')) {
|
|
61
|
+
// Check for hierarchical structure
|
|
62
|
+
let hasHierarchicalSpec = false;
|
|
63
|
+
if (fs.existsSync('.claude/product/domains')) {
|
|
64
|
+
try {
|
|
65
|
+
const domains = fs.readdirSync('.claude/product/domains');
|
|
66
|
+
for (const domain of domains) {
|
|
67
|
+
if (fs.existsSync(`.claude/product/domains/${domain}/index.md`)) {
|
|
68
|
+
hasHierarchicalSpec = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Ignore errors
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!hasHierarchicalSpec) {
|
|
78
|
+
console.log('⚠️ No product specification found');
|
|
79
|
+
console.log(' Create .claude/product/index.md or run /agentful-product');
|
|
80
|
+
warnings++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check 6: Settings file
|
|
85
|
+
const settingsPath = '.claude/settings.json';
|
|
86
|
+
if (!fs.existsSync(settingsPath)) {
|
|
87
|
+
console.log('❌ Missing .claude/settings.json');
|
|
88
|
+
errors++;
|
|
89
|
+
} else {
|
|
90
|
+
try {
|
|
91
|
+
const settingsContent = fs.readFileSync(settingsPath, 'utf8');
|
|
92
|
+
JSON.parse(settingsContent);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.log('❌ Invalid JSON in .claude/settings.json');
|
|
95
|
+
errors++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// === WARNING CHECKS (nice to have) ===
|
|
100
|
+
|
|
101
|
+
// Check: Architecture analysis
|
|
102
|
+
const architecturePath = '.agentful/architecture.json';
|
|
103
|
+
if (!fs.existsSync(architecturePath)) {
|
|
104
|
+
console.log('⚠️ Tech stack not analyzed. Run /agentful-generate to:');
|
|
105
|
+
console.log(' - Detect your tech stack');
|
|
106
|
+
console.log(' - Discover business domains');
|
|
107
|
+
console.log(' - Generate specialized agents');
|
|
108
|
+
warnings++;
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
const archContent = fs.readFileSync(architecturePath, 'utf8');
|
|
112
|
+
const arch = JSON.parse(archContent);
|
|
113
|
+
if (!arch.techStack || !arch.domains) {
|
|
114
|
+
console.log('⚠️ .agentful/architecture.json is malformed');
|
|
115
|
+
console.log(' Run /agentful-generate to regenerate');
|
|
116
|
+
warnings++;
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.log('⚠️ .agentful/architecture.json is malformed');
|
|
120
|
+
console.log(' Run /agentful-generate to regenerate');
|
|
121
|
+
warnings++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check: Node version
|
|
126
|
+
try {
|
|
127
|
+
const nodeVersion = process.version.replace('v', '');
|
|
128
|
+
const majorVersion = parseInt(nodeVersion.split('.')[0], 10);
|
|
129
|
+
if (majorVersion < 22) {
|
|
130
|
+
console.log(`⚠️ Node.js ${nodeVersion} detected. Requires >=22.0.0`);
|
|
131
|
+
warnings++;
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Ignore errors
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// === SUMMARY ===
|
|
138
|
+
|
|
139
|
+
if (errors > 0) {
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(`❌ Found ${errors} critical issue(s)`);
|
|
142
|
+
console.log(' Run: npx @itz4blitz/agentful init');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (warnings > 0) {
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(`⚠️ Agentful ready with ${warnings} warning(s)`);
|
|
149
|
+
} else {
|
|
150
|
+
console.log('✅ Agentful ready');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
process.exit(0);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// post-agent.js
|
|
3
|
+
// Tracks agent execution metrics
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
const METRICS_FILE = '.agentful/agent-metrics.json';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Track agent execution metrics
|
|
11
|
+
* @param {Object} options - Tracking options
|
|
12
|
+
* @param {string} options.agentName - Name of the agent
|
|
13
|
+
* @param {string} [options.feature] - Feature being worked on
|
|
14
|
+
* @param {string} [options.domain] - Domain context
|
|
15
|
+
* @param {string} [options.timestamp] - ISO timestamp (defaults to current time)
|
|
16
|
+
* @returns {Object} - Result with success flag and optional error
|
|
17
|
+
*/
|
|
18
|
+
export function trackAgentMetrics({ agentName, feature = '', domain = '', timestamp = new Date().toISOString() }) {
|
|
19
|
+
// Exit successfully if no agent specified
|
|
20
|
+
if (!agentName) {
|
|
21
|
+
return { success: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Ensure .agentful directory exists
|
|
25
|
+
const metricsDir = METRICS_FILE.substring(0, METRICS_FILE.lastIndexOf('/'));
|
|
26
|
+
if (!fs.existsSync(metricsDir)) {
|
|
27
|
+
fs.mkdirSync(metricsDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create metrics file if it doesn't exist
|
|
31
|
+
if (!fs.existsSync(METRICS_FILE)) {
|
|
32
|
+
const initialMetrics = {
|
|
33
|
+
invocations: {},
|
|
34
|
+
last_invocation: null,
|
|
35
|
+
feature_hooks: []
|
|
36
|
+
};
|
|
37
|
+
fs.writeFileSync(METRICS_FILE, JSON.stringify(initialMetrics, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate and read existing metrics file
|
|
41
|
+
let metrics;
|
|
42
|
+
if (!fs.existsSync(METRICS_FILE)) {
|
|
43
|
+
// File doesn't exist, create new structure
|
|
44
|
+
metrics = {
|
|
45
|
+
invocations: {},
|
|
46
|
+
last_invocation: null,
|
|
47
|
+
feature_hooks: []
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
try {
|
|
51
|
+
const metricsContent = fs.readFileSync(METRICS_FILE, 'utf8');
|
|
52
|
+
metrics = JSON.parse(metricsContent);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.log('WARNING: agent-metrics.json is corrupted, recreating');
|
|
55
|
+
metrics = {
|
|
56
|
+
invocations: {},
|
|
57
|
+
last_invocation: null,
|
|
58
|
+
feature_hooks: []
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Update invocation count for this agent
|
|
64
|
+
const currentCount = metrics.invocations[agentName] || 0;
|
|
65
|
+
const newCount = currentCount + 1;
|
|
66
|
+
|
|
67
|
+
// Update metrics
|
|
68
|
+
metrics.invocations[agentName] = newCount;
|
|
69
|
+
metrics.last_invocation = {
|
|
70
|
+
agent: agentName,
|
|
71
|
+
timestamp: timestamp,
|
|
72
|
+
feature: feature,
|
|
73
|
+
domain: domain
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Write updated metrics
|
|
77
|
+
try {
|
|
78
|
+
fs.writeFileSync(METRICS_FILE, JSON.stringify(metrics, null, 2));
|
|
79
|
+
return { success: true };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('WARNING: Failed to write agent-metrics.json');
|
|
82
|
+
return { success: true, error: err.message };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// CLI entrypoint
|
|
87
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
88
|
+
const AGENT_NAME = process.env.AGENTFUL_AGENT || '';
|
|
89
|
+
const FEATURE = process.env.AGENTFUL_FEATURE || '';
|
|
90
|
+
const DOMAIN = process.env.AGENTFUL_DOMAIN || '';
|
|
91
|
+
const TIMESTAMP = new Date().toISOString();
|
|
92
|
+
|
|
93
|
+
trackAgentMetrics({
|
|
94
|
+
agentName: AGENT_NAME,
|
|
95
|
+
feature: FEATURE,
|
|
96
|
+
domain: DOMAIN,
|
|
97
|
+
timestamp: TIMESTAMP
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// post-feature.js
|
|
3
|
+
// Feature completion validation and tracking
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
// Helper function to run command and capture output
|
|
9
|
+
function runCommand(command, description) {
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync(command, {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
stdio: 'pipe',
|
|
14
|
+
timeout: 60000 // 60 second timeout
|
|
15
|
+
});
|
|
16
|
+
return { success: true, output };
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return { success: false, output: err.stdout || err.stderr || '' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate feature completion with automated checks
|
|
24
|
+
* @param {string} feature - Feature name
|
|
25
|
+
* @param {string} domain - Domain name (optional)
|
|
26
|
+
* @returns {object} - { errors, validationResults, exitCode }
|
|
27
|
+
*/
|
|
28
|
+
export function validateFeatureCompletion(feature, domain = '') {
|
|
29
|
+
// Exit successfully if no feature specified
|
|
30
|
+
if (!feature) {
|
|
31
|
+
return { errors: 0, validationResults: [], exitCode: 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TIMESTAMP = new Date().toISOString();
|
|
35
|
+
let errors = 0;
|
|
36
|
+
const validationResults = [];
|
|
37
|
+
|
|
38
|
+
console.log(`=== Post-Feature Validation: ${feature} ===`);
|
|
39
|
+
console.log('');
|
|
40
|
+
|
|
41
|
+
// Detect stack
|
|
42
|
+
const hasPackageJson = fs.existsSync('package.json');
|
|
43
|
+
const hasPyProject = fs.existsSync('pyproject.toml') || fs.existsSync('requirements.txt');
|
|
44
|
+
const hasGoMod = fs.existsSync('go.mod');
|
|
45
|
+
const hasPomXml = fs.existsSync('pom.xml');
|
|
46
|
+
const hasCargoToml = fs.existsSync('Cargo.toml');
|
|
47
|
+
|
|
48
|
+
// Check 1: Run tests
|
|
49
|
+
console.log('[1/4] Running tests...');
|
|
50
|
+
let testCmd = null;
|
|
51
|
+
if (hasPackageJson) testCmd = 'npm test 2>&1';
|
|
52
|
+
else if (hasPyProject) testCmd = 'pytest 2>&1';
|
|
53
|
+
else if (hasGoMod) testCmd = 'go test ./... 2>&1';
|
|
54
|
+
else if (hasPomXml) testCmd = 'mvn test 2>&1';
|
|
55
|
+
else if (hasCargoToml) testCmd = 'cargo test 2>&1';
|
|
56
|
+
|
|
57
|
+
if (testCmd) {
|
|
58
|
+
const testResult = runCommand(testCmd, 'tests');
|
|
59
|
+
if (testResult.success) {
|
|
60
|
+
console.log(' Tests: PASS');
|
|
61
|
+
validationResults.push('tests:pass');
|
|
62
|
+
} else {
|
|
63
|
+
console.log(' Tests: FAIL');
|
|
64
|
+
validationResults.push('tests:fail');
|
|
65
|
+
errors++;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
console.log(' Tests: SKIP (no test framework detected)');
|
|
69
|
+
validationResults.push('tests:skip');
|
|
70
|
+
}
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
// Check 2: Type checking
|
|
74
|
+
console.log('[2/4] Running type check...');
|
|
75
|
+
let typeCmd = null;
|
|
76
|
+
if (fs.existsSync('tsconfig.json')) typeCmd = 'npx tsc --noEmit 2>&1';
|
|
77
|
+
else if (hasPyProject) typeCmd = 'mypy . --ignore-missing-imports 2>&1';
|
|
78
|
+
else if (hasGoMod) typeCmd = 'go vet ./... 2>&1';
|
|
79
|
+
else if (hasCargoToml) typeCmd = 'cargo check 2>&1';
|
|
80
|
+
|
|
81
|
+
if (typeCmd) {
|
|
82
|
+
const tscResult = runCommand(typeCmd, 'type-check');
|
|
83
|
+
if (tscResult.success) {
|
|
84
|
+
console.log(' Type Check: PASS');
|
|
85
|
+
validationResults.push('types:pass');
|
|
86
|
+
} else {
|
|
87
|
+
console.log(' Type Check: FAIL');
|
|
88
|
+
validationResults.push('types:fail');
|
|
89
|
+
errors++;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.log(' Type Check: SKIP (no type checker detected)');
|
|
93
|
+
validationResults.push('types:skip');
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
// Check 3: Linting
|
|
98
|
+
console.log('[3/4] Running linter...');
|
|
99
|
+
let lintCmd = null;
|
|
100
|
+
if (hasPackageJson) lintCmd = 'npm run lint 2>&1';
|
|
101
|
+
else if (hasPyProject) lintCmd = 'ruff check . 2>&1 || pylint **/*.py 2>&1';
|
|
102
|
+
else if (hasGoMod) lintCmd = 'golangci-lint run 2>&1';
|
|
103
|
+
else if (hasCargoToml) lintCmd = 'cargo clippy -- -D warnings 2>&1';
|
|
104
|
+
|
|
105
|
+
if (lintCmd) {
|
|
106
|
+
const lintResult = runCommand(lintCmd, 'lint');
|
|
107
|
+
if (lintResult.success) {
|
|
108
|
+
console.log(' Lint: PASS');
|
|
109
|
+
validationResults.push('lint:pass');
|
|
110
|
+
} else {
|
|
111
|
+
console.log(' Lint: FAIL');
|
|
112
|
+
validationResults.push('lint:fail');
|
|
113
|
+
errors++;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
console.log(' Lint: SKIP (no linter detected)');
|
|
117
|
+
validationResults.push('lint:skip');
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
// Check 4: Coverage check
|
|
122
|
+
console.log('[4/4] Checking test coverage...');
|
|
123
|
+
console.log(' Coverage: SKIP (run /agentful-validate for full coverage report)');
|
|
124
|
+
validationResults.push('coverage:skip');
|
|
125
|
+
console.log('');
|
|
126
|
+
|
|
127
|
+
// Update completion.json with validation results
|
|
128
|
+
const completionJsonPath = '.agentful/completion.json';
|
|
129
|
+
if (fs.existsSync(completionJsonPath)) {
|
|
130
|
+
try {
|
|
131
|
+
const completionContent = fs.readFileSync(completionJsonPath, 'utf8');
|
|
132
|
+
const completion = JSON.parse(completionContent);
|
|
133
|
+
|
|
134
|
+
const validationStatus = errors > 0 ? 'failed' : 'passed';
|
|
135
|
+
const validationObject = {
|
|
136
|
+
status: validationStatus,
|
|
137
|
+
timestamp: TIMESTAMP,
|
|
138
|
+
errors: errors,
|
|
139
|
+
results: validationResults
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (domain) {
|
|
143
|
+
// Hierarchical structure
|
|
144
|
+
if (!completion.domains) completion.domains = {};
|
|
145
|
+
if (!completion.domains[domain]) completion.domains[domain] = {};
|
|
146
|
+
if (!completion.domains[domain].features) completion.domains[domain].features = {};
|
|
147
|
+
if (!completion.domains[domain].features[feature]) completion.domains[domain].features[feature] = {};
|
|
148
|
+
completion.domains[domain].features[feature].validation = validationObject;
|
|
149
|
+
} else {
|
|
150
|
+
// Flat structure
|
|
151
|
+
if (!completion.features) completion.features = {};
|
|
152
|
+
if (!completion.features[feature]) completion.features[feature] = {};
|
|
153
|
+
completion.features[feature].validation = validationObject;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(completionJsonPath, JSON.stringify(completion, null, 2));
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('WARNING: Failed to update completion.json');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Log to metrics
|
|
163
|
+
const metricsPath = '.agentful/agent-metrics.json';
|
|
164
|
+
if (fs.existsSync(metricsPath)) {
|
|
165
|
+
try {
|
|
166
|
+
const metricsContent = fs.readFileSync(metricsPath, 'utf8');
|
|
167
|
+
const metrics = JSON.parse(metricsContent);
|
|
168
|
+
|
|
169
|
+
if (!metrics.feature_hooks) metrics.feature_hooks = [];
|
|
170
|
+
metrics.feature_hooks.push({
|
|
171
|
+
hook: 'PostFeature',
|
|
172
|
+
feature: feature,
|
|
173
|
+
domain: domain,
|
|
174
|
+
timestamp: TIMESTAMP,
|
|
175
|
+
result: errors > 0 ? 'failed' : 'passed'
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
fs.writeFileSync(metricsPath, JSON.stringify(metrics, null, 2));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error('WARNING: Failed to update agent-metrics.json');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create git commit if all validations pass
|
|
185
|
+
if (errors === 0) {
|
|
186
|
+
console.log('=== All Validations Passed ===');
|
|
187
|
+
console.log('');
|
|
188
|
+
|
|
189
|
+
// Check if there are changes to commit
|
|
190
|
+
try {
|
|
191
|
+
execSync('git diff --quiet 2>/dev/null', { stdio: 'pipe' });
|
|
192
|
+
execSync('git diff --cached --quiet 2>/dev/null', { stdio: 'pipe' });
|
|
193
|
+
console.log('No changes to commit');
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// There are changes
|
|
196
|
+
const commitMsg = domain
|
|
197
|
+
? `feat(${domain}): complete ${feature} feature`
|
|
198
|
+
: `feat: complete ${feature} feature`;
|
|
199
|
+
|
|
200
|
+
console.log('Creating git commit...');
|
|
201
|
+
try {
|
|
202
|
+
execSync('git add -A', { stdio: 'pipe' });
|
|
203
|
+
execSync(`git commit -m "${commitMsg}"`, { stdio: 'pipe' });
|
|
204
|
+
console.log(` Commit created: ${commitMsg}`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.log('WARNING: Git commit failed (this is non-blocking)');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { errors: 0, validationResults, exitCode: 0 };
|
|
211
|
+
} else {
|
|
212
|
+
console.log(`=== Validation Failed (${errors} error(s)) ===`);
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log('Fix validation errors before completing feature.');
|
|
215
|
+
console.log('Run /agentful-validate for detailed output');
|
|
216
|
+
return { errors, validationResults, exitCode: 1 };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// CLI entrypoint
|
|
221
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
222
|
+
const FEATURE = process.env.AGENTFUL_FEATURE || '';
|
|
223
|
+
const DOMAIN = process.env.AGENTFUL_DOMAIN || '';
|
|
224
|
+
|
|
225
|
+
const result = validateFeatureCompletion(FEATURE, DOMAIN);
|
|
226
|
+
process.exit(result.exitCode);
|
|
227
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pre-agent.js
|
|
3
|
+
// Validates agent preconditions before invocation
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate agent preconditions
|
|
9
|
+
* @param {string} agentName - Agent name
|
|
10
|
+
* @param {string} feature - Feature name (optional)
|
|
11
|
+
* @param {string} domain - Domain name (optional)
|
|
12
|
+
* @returns {object} - { errors, exitCode }
|
|
13
|
+
*/
|
|
14
|
+
export function validateAgentPreconditions(agentName, feature = '', domain = '') {
|
|
15
|
+
// Exit successfully if no agent specified
|
|
16
|
+
if (!agentName) {
|
|
17
|
+
return { errors: 0, exitCode: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let errors = 0;
|
|
21
|
+
|
|
22
|
+
// Check 1: Agent file exists
|
|
23
|
+
const agentFile = `.claude/agents/${agentName}.md`;
|
|
24
|
+
if (!fs.existsSync(agentFile)) {
|
|
25
|
+
console.error(`ERROR: Agent file not found: ${agentFile}`);
|
|
26
|
+
errors++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check 2: Required state files exist
|
|
30
|
+
const stateFiles = ['state.json', 'completion.json', 'decisions.json'];
|
|
31
|
+
for (const file of stateFiles) {
|
|
32
|
+
const filePath = `.agentful/${file}`;
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
console.error(`ERROR: Missing state file: ${filePath}`);
|
|
35
|
+
errors++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check 3: Validate state.json structure
|
|
40
|
+
const stateJsonPath = '.agentful/state.json';
|
|
41
|
+
if (fs.existsSync(stateJsonPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const stateContent = fs.readFileSync(stateJsonPath, 'utf8');
|
|
44
|
+
const state = JSON.parse(stateContent);
|
|
45
|
+
if (!state.current_phase) {
|
|
46
|
+
console.error('ERROR: .agentful/state.json is malformed (missing current_phase)');
|
|
47
|
+
errors++;
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('ERROR: .agentful/state.json is invalid JSON');
|
|
51
|
+
errors++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check 4: If feature specified, verify it exists in product spec
|
|
56
|
+
if (feature) {
|
|
57
|
+
let featureFound = false;
|
|
58
|
+
|
|
59
|
+
// Check hierarchical structure
|
|
60
|
+
if (domain) {
|
|
61
|
+
const featureFile = `.claude/product/domains/${domain}/features/${feature}.md`;
|
|
62
|
+
if (fs.existsSync(featureFile)) {
|
|
63
|
+
featureFound = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check flat structure
|
|
68
|
+
if (!featureFound) {
|
|
69
|
+
const featureFile = `.claude/product/features/${feature}.md`;
|
|
70
|
+
if (fs.existsSync(featureFile)) {
|
|
71
|
+
featureFound = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!featureFound) {
|
|
76
|
+
console.log(`WARNING: Feature '${feature}' not found in product specification`);
|
|
77
|
+
// Warning only, don't block
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check 5: Verify orchestrator isn't blocked
|
|
82
|
+
if (fs.existsSync(stateJsonPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const stateContent = fs.readFileSync(stateJsonPath, 'utf8');
|
|
85
|
+
const state = JSON.parse(stateContent);
|
|
86
|
+
const blockedOn = state.blocked_on || [];
|
|
87
|
+
if (Array.isArray(blockedOn) && blockedOn.length > 0) {
|
|
88
|
+
console.log(`WARNING: Orchestrator is blocked on decisions: ${JSON.stringify(blockedOn)}`);
|
|
89
|
+
// Warning only, don't block
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Already handled in Check 3
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Exit with error if critical checks failed
|
|
97
|
+
if (errors > 0) {
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(`Pre-agent validation failed with ${errors} error(s)`);
|
|
100
|
+
console.log(`Agent: ${agentName}`);
|
|
101
|
+
if (feature) console.log(`Feature: ${feature}`);
|
|
102
|
+
if (domain) console.log(`Domain: ${domain}`);
|
|
103
|
+
return { errors, exitCode: 1 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// All checks passed
|
|
107
|
+
return { errors: 0, exitCode: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// CLI entrypoint
|
|
111
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
112
|
+
const AGENT_NAME = process.env.AGENTFUL_AGENT || '';
|
|
113
|
+
const FEATURE = process.env.AGENTFUL_FEATURE || '';
|
|
114
|
+
const DOMAIN = process.env.AGENTFUL_DOMAIN || '';
|
|
115
|
+
|
|
116
|
+
const result = validateAgentPreconditions(AGENT_NAME, FEATURE, DOMAIN);
|
|
117
|
+
process.exit(result.exitCode);
|
|
118
|
+
}
|