feat: add PHP development stack, atomic tasks, modular code rules, agent monitoring, fix target project detection
7 evolutionary tasks implemented: 1. PHP web development: php-developer agent + 6 skills (Laravel, Symfony, WordPress, security, testing, modular architecture) + 2 pipeline commands (/laravel, /wordpress) 2. Atomic task decomposition: 1 action = 1 task rule, task sizing guide, decomposition protocol for orchestrator, token budgets per complexity 3. Modular code rules: max 100 lines/file, max 30 lines/function, service/repository patterns, cross-module communication via events only 4. Gitea-centric workflow: mandatory issue creation before work, research with links, progress checkboxes, screenshots on test, git history as knowledge base 5. Fix: target project auto-detection — removed all hardcoded UniqueSoft/APAW from API calls, added get_target_repo() via git remote, GITEA_TARGET_REPO env override 6. Agent execution monitoring: agent-executions.jsonl logging, agent-stats.ts statistics script, required fields per invocation, Gitea comment includes duration/tokens 7. Token optimization: 1 action = 1 task principle, token budgets by task type, routing matrix, no scope creep, skip unnecessary pipeline steps
This commit is contained in:
192
scripts/agent-stats.ts
Normal file
192
scripts/agent-stats.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Agent Stats - Analyze agent execution logs
|
||||
*
|
||||
* Usage:
|
||||
* bun run scripts/agent-stats.ts
|
||||
* bun run scripts/agent-stats.ts --last 7
|
||||
* bun run scripts/agent-stats.ts --project UniqueSoft/my-shop
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
interface AgentExecution {
|
||||
ts: string;
|
||||
agent: string;
|
||||
issue: number;
|
||||
project: string;
|
||||
task: string;
|
||||
subtask_type: string;
|
||||
duration_ms: number;
|
||||
tokens_used: number;
|
||||
status: string;
|
||||
files: string[];
|
||||
score: number | null;
|
||||
next_agent: string | null;
|
||||
}
|
||||
|
||||
interface AgentStats {
|
||||
calls: number;
|
||||
avgDuration: number;
|
||||
avgTokens: number;
|
||||
avgScore: number;
|
||||
successRate: number;
|
||||
totalDuration: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
function parseArgs(): { lastDays: number; project: string | null } {
|
||||
const args = process.argv.slice(2);
|
||||
let lastDays = 30;
|
||||
let project: string | null = null;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--last' && args[i + 1]) {
|
||||
lastDays = parseInt(args[i + 1], 10);
|
||||
}
|
||||
if (args[i] === '--project' && args[i + 1]) {
|
||||
project = args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return { lastDays, project };
|
||||
}
|
||||
|
||||
function loadExecutions(logPath: string): AgentExecution[] {
|
||||
if (!existsSync(logPath)) {
|
||||
console.log('No execution log found. Start using agents to build history.');
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = readFileSync(logPath, 'utf-8');
|
||||
return content
|
||||
.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => {
|
||||
try { return JSON.parse(line); }
|
||||
catch { return null; }
|
||||
})
|
||||
.filter((e): e is AgentExecution => e !== null);
|
||||
}
|
||||
|
||||
function filterByDate(executions: AgentExecution[], days: number): AgentExecution[] {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
return executions.filter(e => new Date(e.ts) >= cutoff);
|
||||
}
|
||||
|
||||
function filterByProject(executions: AgentExecution[], project: string): AgentExecution[] {
|
||||
return executions.filter(e => e.project === project);
|
||||
}
|
||||
|
||||
function computeStats(executions: AgentExecution[]): Map<string, AgentStats> {
|
||||
const stats = new Map<string, AgentStats>();
|
||||
|
||||
for (const e of executions) {
|
||||
const existing = stats.get(e.agent) || {
|
||||
calls: 0,
|
||||
avgDuration: 0,
|
||||
avgTokens: 0,
|
||||
avgScore: 0,
|
||||
successRate: 0,
|
||||
totalDuration: 0,
|
||||
totalTokens: 0,
|
||||
};
|
||||
|
||||
existing.calls++;
|
||||
existing.totalDuration += e.duration_ms;
|
||||
existing.totalTokens += e.tokens_used;
|
||||
if (e.score) existing.avgScore = (existing.avgScore * (existing.calls - 1) + e.score) / existing.calls;
|
||||
if (e.status === 'success' || e.status === 'pass') {
|
||||
existing.successRate = (existing.successRate * (existing.calls - 1) + 1) / existing.calls;
|
||||
}
|
||||
|
||||
stats.set(e.agent, existing);
|
||||
}
|
||||
|
||||
// Compute averages
|
||||
for (const [, s] of stats) {
|
||||
s.avgDuration = s.calls > 0 ? s.totalDuration / s.calls : 0;
|
||||
s.avgTokens = s.calls > 0 ? s.totalTokens / s.calls : 0;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens < 1000) return `${tokens}`;
|
||||
return `${(tokens / 1000).toFixed(1)}k`;
|
||||
}
|
||||
|
||||
const logPath = join(process.cwd(), '.kilo', 'logs', 'agent-executions.jsonl');
|
||||
const { lastDays, project } = parseArgs();
|
||||
|
||||
let executions = loadExecutions(logPath);
|
||||
|
||||
if (executions.length === 0) {
|
||||
console.log('\n📊 Agent Stats - No data yet\n');
|
||||
console.log('Log file:', logPath);
|
||||
console.log('Start using agents to build execution history.\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (lastDays < 9999) {
|
||||
executions = filterByDate(executions, lastDays);
|
||||
}
|
||||
|
||||
if (project) {
|
||||
executions = filterByProject(executions, project);
|
||||
}
|
||||
|
||||
const stats = computeStats(executions);
|
||||
|
||||
console.log(`\n📊 Agent Stats (Last ${lastDays} days${project ? `, Project: ${project}` : ''})`);
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
const sortedStats = [...stats.entries()].sort((a, b) => b[1].calls - a[1].calls);
|
||||
|
||||
for (const [agent, s] of sortedStats) {
|
||||
console.log(
|
||||
`${agent.padEnd(20)} ${String(s.calls).padStart(3)} calls, ` +
|
||||
`avg ${formatDuration(s.avgDuration).padStart(6)}, ` +
|
||||
`score ${s.avgScore.toFixed(1)}/10, ` +
|
||||
`${(s.successRate * 100).toFixed(0)}% success, ` +
|
||||
`~${formatTokens(s.avgTokens)} tokens`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('═'.repeat(70));
|
||||
console.log(`Total: ${executions.length} executions\n`);
|
||||
|
||||
// Project breakdown
|
||||
const projects = new Map<string, number>();
|
||||
for (const e of executions) {
|
||||
projects.set(e.project, (projects.get(e.project) || 0) + 1);
|
||||
}
|
||||
|
||||
if (projects.size > 1) {
|
||||
console.log('📁 By Project:');
|
||||
for (const [proj, count] of projects) {
|
||||
console.log(` ${proj}: ${count} executions`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Status breakdown
|
||||
const statuses = new Map<string, number>();
|
||||
for (const e of executions) {
|
||||
statuses.set(e.status, (statuses.get(e.status) || 0) + 1);
|
||||
}
|
||||
|
||||
console.log('📈 By Status:');
|
||||
for (const [status, count] of statuses) {
|
||||
console.log(` ${status}: ${count}`);
|
||||
}
|
||||
console.log('');
|
||||
Reference in New Issue
Block a user