config: full APAW agent infrastructure + Phantom project files
- Added all agent definitions (.kile/agents/*.md) - Added commands, rules, skills, shared modules - Added src/, scripts/, tests/, docker/, agent-evolution/ - Extracted 3 archives: website/, workspace/, release/ - Created .env with Gitea creds for UniqueSoft/Phantom - Created docs/ with project-specific guides - Added .gitignore for node_modules
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('');
|
||||
190
scripts/e2e-gns2-test.py
Normal file
190
scripts/e2e-gns2-test.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GNS-2 End-to-End Integration Test"""
|
||||
import urllib.request
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
USER, PASS, REPO, ISSUE = 'NW', 'eshkink0t', 'UniqueSoft/APAW', 110
|
||||
|
||||
class GiteaAPI:
|
||||
def __init__(self):
|
||||
self.base = 'https://git.softuniq.eu/api/v1'
|
||||
self.token = os.environ.get('GITEA_TOKEN', '')
|
||||
|
||||
|
||||
def api(self, path, data=None, method='GET'):
|
||||
url = f"{self.base}/repos/{REPO}{path}"
|
||||
req = urllib.request.Request(
|
||||
url, data=json.dumps(data).encode() if data else None,
|
||||
headers={'Content-Type': 'application/json'}, method=method)
|
||||
req.add_header('Authorization', f'token {self.token}')
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read()) if r.status != 204 else None
|
||||
|
||||
gitea = GiteaAPI()
|
||||
|
||||
|
||||
def update_checkpoint(phase, depth, consumed, remaining, last_agent, next_agent, history_append):
|
||||
issue = gitea.api(f"/issues/{ISSUE}")
|
||||
body = issue['body']
|
||||
checkpoint_yaml = (
|
||||
f"checkpoint:\n version: 2\n issue: {ISSUE}\n phase: {phase}\n"
|
||||
f" depth: {depth}\n last_agent: {last_agent}\n"
|
||||
f" last_invocation: {last_agent}-110-{int(time.time())}\n"
|
||||
f" budget:\n total: 8000\n consumed: {consumed}\n"
|
||||
f" remaining: {remaining}\n state:\n"
|
||||
f" labels: [status::{phase}, budget::{'sufficient' if remaining > 2000 else 'warning' if remaining > 0 else 'exhausted'}, cascade::depth-{depth}]\n"
|
||||
f" assignee: {next_agent}\n milestone: 67\n history:\n"
|
||||
f" - {{agent: orchestrator, invocation: orch-110-001, action: create_e2e_test}}\n{history_append}\n"
|
||||
f" next_agent: {next_agent}\n next_estimated_tokens: 1000\n"
|
||||
f" created_at: {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}\n")
|
||||
import re
|
||||
new_body = re.sub(
|
||||
r'## GNS Checkpoint\s*```yaml\s*[\s\S]*?```',
|
||||
f"## GNS Checkpoint\n```yaml\n{checkpoint_yaml}```", body)
|
||||
gitea.api(f"/issues/{ISSUE}", {"body": new_body}, 'PATCH')
|
||||
|
||||
|
||||
def post_comment(agent, evtype, depth, consumed, remaining, next_agent, extras=""):
|
||||
inv = int(time.time())
|
||||
comment = (
|
||||
f"## 🔄 {agent} | phase:executing | depth:{depth}\n\n"
|
||||
f"**Event Type**: {evtype}\n**Parent**: orch-110-001\n"
|
||||
f"**Invocation**: {agent}-110-{inv}\n"
|
||||
f"**Budget**: 8000 → {consumed} → {remaining}\n\n"
|
||||
f"### Action Taken\n{agent} processed checkpoint.\n\n"
|
||||
f"### Next Decision\n**Recommended next**: @{next_agent}\n"
|
||||
f"**Estimated tokens**: 1000\n**Budget remaining**: {remaining}\n\n{extras}\n---\n"
|
||||
f"<!-- GNS_EVENT: {{\n \"type\": \"{evtype}\",\n"
|
||||
f' "agent": "{agent}",\n'
|
||||
f' "invocation_id": "{agent}-110-{inv}",\n'
|
||||
f' "parent_id": "orch-110-001",\n'
|
||||
f' "depth": {depth},\n'
|
||||
f' "budget": {{"before": 8000, "consumed": {consumed}, "remaining": {remaining}}},\n'
|
||||
f' "state_changes": {{\n'
|
||||
f' "labels_add": ["phase::executing"],\n'
|
||||
f' "assignee": "{next_agent}",\n'
|
||||
f' "is_locked": false\n }},\n'
|
||||
f' "cascade_log": [],\n'
|
||||
f' "next_agent": "{next_agent}",\n'
|
||||
f' "estimated_next_tokens": 1000,\n'
|
||||
f' "timestamp": "{time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}"\n'
|
||||
f"}} -->")
|
||||
gitea.api(f"/issues/{ISSUE}/comments", {"body": comment}, 'POST')
|
||||
|
||||
|
||||
def add_label(label):
|
||||
try:
|
||||
gitea.api(f"/issues/{ISSUE}/labels", {"labels": [label]}, 'POST')
|
||||
except Exception as e:
|
||||
print(f" (Label {label}: {e})")
|
||||
|
||||
|
||||
def replace_scoped_label(scope, new_label):
|
||||
issue = gitea.api(f"/issues/{ISSUE}")
|
||||
for l in issue.get('labels', []):
|
||||
if l['name'].startswith(f"{scope}::"):
|
||||
try:
|
||||
gitea.api(f"/issues/{ISSUE}/labels/{l['id']}", method='DELETE')
|
||||
except Exception:
|
||||
pass
|
||||
add_label(new_label)
|
||||
|
||||
|
||||
def e2e_test():
|
||||
print("="*60)
|
||||
print("GNS-2 End-to-End Test")
|
||||
print(f"Issue: #{ISSUE}")
|
||||
print("="*60)
|
||||
|
||||
print("\n[1] Init...", end=' ')
|
||||
issue = gitea.api(f"/issues/{ISSUE}")
|
||||
print("OK")
|
||||
|
||||
print("\n[2] Requirement Refiner...", end=' ')
|
||||
update_checkpoint('planned', 0, 500, 7500, 'requirement-refiner', 'capability-analyst',
|
||||
' - {agent: req-refiner, invocation: req-110-001, action: refine}')
|
||||
post_comment('requirement-refiner', 'state_change', 0, 500, 7500, 'capability-analyst')
|
||||
replace_scoped_label('status', 'status::planned')
|
||||
add_label('agent::capability-analyst')
|
||||
print("OK")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n[3] Capability-Analyst spawns HistoryMiner (depth 0→1)...", end=' ')
|
||||
update_checkpoint('researching', 1, 1500, 6500, 'capability-analyst', 'history-miner',
|
||||
' - {agent: cap-analyst, invocation: cap-110-001, action: subagent_call, target: history-miner}')
|
||||
cascade = "### Cascade Log\n| Agent | Task | Result | Tokens | Verdict |\n|-------|------|--------|--------|---------|\n| history-miner | git search | found 3 commits | 1000 | ✅ |"
|
||||
post_comment('capability-analyst', 'subagent_result', 1, 1500, 6500, 'agent-architect', cascade)
|
||||
replace_scoped_label('status', 'status::researching')
|
||||
add_label('cascade::depth-1')
|
||||
print("OK")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n[4] History Miner (Tier 0, leaf)...", end=' ')
|
||||
post_comment('history-miner', 'subagent_result', 1, 2500, 5500, 'agent-architect', "### Findings\n- Found `47b027a`\n- 2 related issues")
|
||||
print("OK")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n[5] Agent Architect completes spec (Tier 2, depth 1→2)...", end=' ')
|
||||
update_checkpoint('designed', 2, 3500, 4500, 'agent-architect', 'capability-analyst',
|
||||
' - {agent: arch, invocation: arch-110-001, action: design_spec}')
|
||||
post_comment('agent-architect', 'subagent_result', 2, 3500, 4500, 'capability-analyst',
|
||||
"### Spec Designed\n- gitea-client.ts\n- docker-compose.yml")
|
||||
replace_scoped_label('status', 'status::designed')
|
||||
add_label('cascade::depth-2')
|
||||
print("OK")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n[6] Capability Analyst reviews and closes...", end=' ')
|
||||
update_checkpoint('completed', 2, 4000, 4000, 'capability-analyst', 'orchestrator',
|
||||
' - {agent: cap-analyst, invocation: cap-110-002, action: review_complete}')
|
||||
post_comment('capability-analyst', 'state_change', 2, 4000, 4000, 'orchestrator',
|
||||
"### Review Complete\n✅ All criteria met. Closing.")
|
||||
replace_scoped_label('status', 'status::done')
|
||||
add_label('budget::sufficient')
|
||||
add_label('quality::pass')
|
||||
gitea.api(f"/issues/{ISSUE}", {"state": "closed"}, 'PATCH')
|
||||
print("OK")
|
||||
|
||||
# Verification
|
||||
issue = gitea.api(f"/issues/{ISSUE}")
|
||||
comments = gitea.api(f"/issues/{ISSUE}/comments")
|
||||
timeline = gitea.api(f"/issues/{ISSUE}/timeline")
|
||||
labels = [l['name'] for l in issue['labels']]
|
||||
|
||||
print("\n"+"="*60+"\nVerification\n"+"="*60)
|
||||
print(f"State: {issue['state']}")
|
||||
print(f"Labels: {labels}")
|
||||
print(f"Comments: {len(comments)}, Timeline: {len(timeline)}")
|
||||
import re
|
||||
events = re.findall(r'<!-- GNS_EVENT: ({.*?}) -->', issue['body'] + '\n'.join(c['body'] for c in comments), re.DOTALL)
|
||||
print(f"GNS_EVENTs: {len(events)}")
|
||||
print(f"Checkpoint: {'✅' if '## GNS Checkpoint' in issue['body'] else '❌'}")
|
||||
|
||||
failures = []
|
||||
if issue['state'] != 'closed':
|
||||
failures.append("Issue not closed")
|
||||
if len(events) < 5:
|
||||
failures.append(f"Too few events ({len(events)})")
|
||||
if 'status::done' not in labels:
|
||||
failures.append("No completed")
|
||||
if 'cascade::depth-2' not in labels:
|
||||
failures.append("No depth-2")
|
||||
if 'budget::sufficient' not in labels:
|
||||
failures.append("No budget")
|
||||
if 'quality::pass' not in labels:
|
||||
failures.append("No quality")
|
||||
|
||||
if failures:
|
||||
print("\n❌ FAILED")
|
||||
for f in failures:
|
||||
print(f" - {f}")
|
||||
return 1
|
||||
print("\n✅ ALL E2E TESTS PASSED\n"+"="*60)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(e2e_test())
|
||||
117
scripts/init-gns-labels.py
Normal file
117
scripts/init-gns-labels.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GNS-2 Label Initialization Script
|
||||
Idempotent creation of Gitea labels for GNS-2 semantic routing.
|
||||
"""
|
||||
import urllib.request
|
||||
import json
|
||||
import os
|
||||
|
||||
GITEA_API = os.environ.get('GITEA_API_URL', 'https://git.softuniq.eu/api/v1')
|
||||
REPO = 'UniqueSoft/APAW'
|
||||
USER = 'NW'
|
||||
PASS = 'eshkink0t'
|
||||
|
||||
def api(path, data=None, method='GET'):
|
||||
url = f"{GITEA_API}/repos/{REPO}{path}"
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(data).encode() if data else None,
|
||||
headers=headers,
|
||||
method=method
|
||||
)
|
||||
# Basic Auth
|
||||
import base64
|
||||
creds = base64.b64encode(f"{USER}:{PASS}".encode()).decode()
|
||||
req.add_header('Authorization', f'Basic {creds}')
|
||||
try:
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f" HTTP {e.code}: {body}")
|
||||
return None
|
||||
|
||||
LABELS = [
|
||||
# Phase labels
|
||||
{"name": "phase::gathering-evidence", "color": "c2e0c6", "description": "Agent is gathering data"},
|
||||
{"name": "phase::drafting-spec", "color": "0052cc", "description": "Agent is drafting specification"},
|
||||
{"name": "phase::refining-prompt", "color": "fbca04", "description": "Agent is refining prompts"},
|
||||
{"name": "phase::awaiting-review", "color": "d93f0b", "description": "Agent awaits review"},
|
||||
{"name": "phase::executing", "color": "0e8a16", "description": "Agent is executing task"},
|
||||
{"name": "phase::verifying", "color": "5319e7", "description": "Agent is verifying results"},
|
||||
# Agent labels
|
||||
{"name": "agent::orchestrator", "color": "7C3AED", "description": "Owned by orchestrator"},
|
||||
{"name": "agent::capability-analyst", "color": "6366F1", "description": "Owned by capability-analyst"},
|
||||
{"name": "agent::agent-architect", "color": "10B981", "description": "Owned by agent-architect"},
|
||||
{"name": "agent::lead-developer", "color": "DC2626", "description": "Owned by lead-developer"},
|
||||
{"name": "agent::code-skeptic", "color": "059669", "description": "Owned by code-skeptic"},
|
||||
{"name": "agent::the-fixer", "color": "D97706", "description": "Owned by the-fixer"},
|
||||
{"name": "agent::evaluator", "color": "8B5CF6", "description": "Owned by evaluator"},
|
||||
{"name": "agent::history-miner", "color": "6B7280", "description": "Owned by history-miner"},
|
||||
{"name": "agent::system-analyst", "color": "2563EB", "description": "Owned by system-analyst"},
|
||||
{"name": "agent::sdet-engineer", "color": "0891B2", "description": "Owned by sdet-engineer"},
|
||||
# Budget labels
|
||||
{"name": "budget::sufficient", "color": "0e8a16", "description": "Token budget sufficient"},
|
||||
{"name": "budget::warning", "color": "fbca04", "description": "Token budget low"},
|
||||
{"name": "budget::exhausted", "color": "b60205", "description": "Token budget exhausted"},
|
||||
# Permission labels
|
||||
{"name": "permission::read-only", "color": "cfd3d7", "description": "Read-only access"},
|
||||
{"name": "permission::write-code", "color": "0052cc", "description": "Can write code"},
|
||||
{"name": "permission::write-config", "color": "5319e7", "description": "Can write config"},
|
||||
{"name": "permission::evolve-system", "color": "b60205", "description": "Can evolve system"},
|
||||
{"name": "permission::violation", "color": "b60205", "description": "Security violation"},
|
||||
# Cascade labels
|
||||
{"name": "cascade::depth-0", "color": "cfd3d7", "description": "No subagent calls"},
|
||||
{"name": "cascade::depth-1", "color": "c2e0c6", "description": "1-level subagent calls"},
|
||||
{"name": "cascade::depth-2", "color": "0052cc", "description": "2-level subagent calls"},
|
||||
{"name": "cascade::depth-n", "color": "5319e7", "description": "Unlimited subagent calls"},
|
||||
{"name": "cascade::depth-exceeded", "color": "b60205", "description": "Depth limit exceeded"},
|
||||
# Quality labels
|
||||
{"name": "quality::pass", "color": "0e8a16", "description": "Quality check passed"},
|
||||
{"name": "quality::fail", "color": "b60205", "description": "Quality check failed"},
|
||||
{"name": "quality::needs-fix", "color": "fbca04", "description": "Needs fixes"},
|
||||
{"name": "quality::blocked", "color": "d73a4a", "description": "Blocked by quality"},
|
||||
# Evolution labels
|
||||
{"name": "evolution::model-change", "color": "8B5CF6", "description": "Model change evolution"},
|
||||
{"name": "evolution::new-agent", "color": "10B981", "description": "New agent evolution"},
|
||||
{"name": "evolution::new-skill", "color": "2563EB", "description": "New skill evolution"},
|
||||
{"name": "evolution::new-workflow", "color": "7C3AED", "description": "New workflow evolution"},
|
||||
{"name": "evolution::prompt-opt", "color": "D97706", "description": "Prompt optimization evolution"},
|
||||
# Memory labels
|
||||
{"name": "memory::checkpoint", "color": "0052cc", "description": "Checkpoint stored"},
|
||||
{"name": "memory::stale", "color": "fbca04", "description": "Checkpoint stale"},
|
||||
{"name": "memory::fresh", "color": "0e8a16", "description": "Checkpoint fresh"},
|
||||
{"name": "memory::recoverable", "color": "c2e0c6", "description": "Checkpoint recoverable"},
|
||||
]
|
||||
|
||||
def main():
|
||||
print("GNS-2 Label Initialization")
|
||||
print(f"Target: {REPO}")
|
||||
print()
|
||||
|
||||
existing = api("/labels")
|
||||
existing_names = {l['name'] for l in (existing or [])}
|
||||
print(f"Existing labels: {len(existing_names)}")
|
||||
|
||||
created = 0
|
||||
skipped = 0
|
||||
for label in LABELS:
|
||||
if label['name'] in existing_names:
|
||||
print(f" SKIP: {label['name']}")
|
||||
skipped += 1
|
||||
continue
|
||||
result = api("/labels", label, 'POST')
|
||||
if result:
|
||||
print(f" CREATE: {label['name']} ({label['color']})")
|
||||
created += 1
|
||||
else:
|
||||
print(f" FAIL: {label['name']}")
|
||||
|
||||
print()
|
||||
print(f"Done: {created} created, {skipped} skipped")
|
||||
print(f"Total labels: {len(existing_names) + created}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
41
scripts/log-execution.cjs
Normal file
41
scripts/log-execution.cjs
Normal file
@@ -0,0 +1,41 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOG_FILE = '.kilo/logs/agent-executions.jsonl';
|
||||
|
||||
function logExecution(data) {
|
||||
const entry = {
|
||||
ts: new Date().toISOString(),
|
||||
agent: data.agent || 'unknown',
|
||||
issue: data.issue || 0,
|
||||
project: data.project || 'UniqueSoft/APAW',
|
||||
task: data.task || 'unknown',
|
||||
subtask_type: data.subtask_type || 'general',
|
||||
duration_ms: data.duration_ms || 0,
|
||||
tokens_used: data.tokens_used || 0,
|
||||
status: data.status || 'unknown',
|
||||
files: data.files || [],
|
||||
score: data.score || 0,
|
||||
next_agent: data.next_agent || null
|
||||
};
|
||||
|
||||
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
||||
return entry;
|
||||
}
|
||||
|
||||
// CLI usage
|
||||
if (require.main === module) {
|
||||
const args = {};
|
||||
for (let i = 2; i < process.argv.length; i += 2) {
|
||||
const key = process.argv[i].replace(/^--/, '');
|
||||
const val = process.argv[i + 1];
|
||||
if (key === 'files') args[key] = val.split(',');
|
||||
else if (key === 'issue' || key === 'duration_ms' || key === 'tokens_used' || key === 'score') args[key] = parseInt(val) || 0;
|
||||
else args[key] = val;
|
||||
}
|
||||
|
||||
const entry = logExecution(args);
|
||||
console.log('Logged:', entry.ts, entry.agent, entry.status);
|
||||
}
|
||||
|
||||
module.exports = { logExecution };
|
||||
246
scripts/mass-update-gns-agents.py
Normal file
246
scripts/mass-update-gns-agents.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GNS-2 Agent Mass Update Script
|
||||
Updates all remaining Tier 0/1 agents with GNS-2 protocol:
|
||||
- Checkpoint read requirement (read-only for Tier 0)
|
||||
- Event footer template (mandatory)
|
||||
- Tier classification (Tier 0 or 1)
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import glob
|
||||
|
||||
# Root directory of agents
|
||||
AGENTS_DIR = '.kilo/agents'
|
||||
|
||||
# Tier classification
|
||||
TIER_0_AGENTS = [
|
||||
'history-miner', 'code-skeptic', 'performance-engineer',
|
||||
'security-auditor', 'visual-tester', 'browser-automation',
|
||||
'markdown-validator', 'planner', 'reflector', 'memory-manager',
|
||||
'pipeline-judge', 'architect-indexer'
|
||||
]
|
||||
|
||||
TIER_1_AGENTS = [
|
||||
'lead-developer', 'the-fixer', 'sdet-engineer',
|
||||
'frontend-developer', 'backend-developer', 'go-developer',
|
||||
'flutter-developer', 'php-developer', 'python-developer',
|
||||
'devops-engineer', 'release-manager', 'requirement-refiner',
|
||||
'product-owner', 'prompt-optimizer', 'system-analyst',
|
||||
'workflow-architect', 'orchestrator'
|
||||
]
|
||||
|
||||
def get_tier(agent_name: str) -> int:
|
||||
if agent_name in TIER_0_AGENTS:
|
||||
return 0
|
||||
if agent_name in TIER_1_AGENTS:
|
||||
return 1
|
||||
return -1 # Unknown
|
||||
|
||||
def extract_frontmatter(content: str) -> tuple:
|
||||
"""Extract YAML frontmatter from markdown content."""
|
||||
if not content.startswith('---'):
|
||||
return None, content
|
||||
|
||||
parts = content.split('---', 2)
|
||||
if len(parts) < 3:
|
||||
return None, content
|
||||
|
||||
return parts[1].strip(), parts[2].strip()
|
||||
|
||||
def update_frontmatter(fm: str, tier: int) -> str:
|
||||
"""Update frontmatter with GNS-2 metadata."""
|
||||
lines = fm.split('\n')
|
||||
new_lines = []
|
||||
|
||||
# Add tier comment
|
||||
new_lines.append(f"# GNS-2 Agent (Tier {tier})")
|
||||
|
||||
for line in lines:
|
||||
# Ensure permission.task exists
|
||||
if line.strip().startswith('permission:'):
|
||||
new_lines.append(line)
|
||||
continue
|
||||
new_lines.append(line)
|
||||
|
||||
return '\n'.join(new_lines)
|
||||
|
||||
def generate_gns_protocol(tier: int) -> str:
|
||||
"""Generate GNS-2 protocol section for an agent."""
|
||||
|
||||
if tier == 0:
|
||||
return """## GNS-2 Protocol
|
||||
|
||||
### Tier
|
||||
Tier 0 (Leaf Agent / No Cascade)
|
||||
- `max_cascade_depth: 0` (no subagent calls)
|
||||
- Read checkpoint only (do not modify)
|
||||
- Write event footer on completion
|
||||
|
||||
### On Entry (MANDATORY)
|
||||
1. Read issue body from Gitea API
|
||||
2. Parse `## GNS Checkpoint` YAML block
|
||||
3. Extract task from checkpoint or last event
|
||||
|
||||
### During Work
|
||||
- Execute atomic task as specified in checkpoint
|
||||
- Follow existing behavior guidelines
|
||||
- Do NOT spawn subagents
|
||||
|
||||
### On Exit (MANDATORY)
|
||||
1. Post comment with result + GNS_EVENT footer
|
||||
2. Do NOT modify checkpoint (read-only)
|
||||
3. Set `next_agent` recommendation in event footer
|
||||
|
||||
### Next Recommendation
|
||||
After completion, recommend next agent in event footer:
|
||||
- `code-skeptic`: after code written
|
||||
- `performance-engineer`: after code tested
|
||||
- `security-auditor`: after performance reviewed
|
||||
"""
|
||||
|
||||
elif tier == 1:
|
||||
return """## GNS-2 Protocol
|
||||
|
||||
### Tier
|
||||
Tier 1 (Task Agent / Orchestrator-Mediated Cascade)
|
||||
- `max_cascade_depth: 1` (request orchestrator to spawn, do not spawn directly)
|
||||
- Can read checkpoint and recommend next agent
|
||||
- Event footer triggers orchestrator polling
|
||||
|
||||
### On Entry (MANDATORY)
|
||||
1. Read issue body from Gitea API
|
||||
2. Parse `## GNS Checkpoint` YAML block
|
||||
3. Verify `checkpoint.budget.remaining > estimated_cost`
|
||||
|
||||
### During Work
|
||||
- Execute task as specified
|
||||
- If subagent needed, write recommendation in event footer
|
||||
- Do NOT call `task` tool directly (Tier 1)
|
||||
|
||||
### On Exit (MANDATORY)
|
||||
1. Update labels if needed (quality::*, phase::*)
|
||||
2. Post comment with result + GNS_EVENT footer
|
||||
3. Include `next_agent` recommendation
|
||||
|
||||
### GNS Event Footer Template
|
||||
```markdown
|
||||
---
|
||||
<!-- GNS_EVENT: {
|
||||
"type": "subagent_result",
|
||||
"agent": "AGENT_NAME",
|
||||
"invocation_id": "AGENT-{issue}-{seq}",
|
||||
"parent_id": "{parent_invocation}",
|
||||
"depth": 1,
|
||||
"budget": {"remaining": {remaining}},
|
||||
"state_changes": {
|
||||
"labels_add": ["phase::{phase}"],
|
||||
"labels_remove": ["phase::{old_phase}"],
|
||||
"assignee": "{next_agent}",
|
||||
"is_locked": false
|
||||
},
|
||||
"next_agent": "{next_agent}",
|
||||
"estimated_next_tokens": {estimate},
|
||||
"timestamp": "{iso8601}"
|
||||
} -->
|
||||
```
|
||||
"""
|
||||
|
||||
return ""
|
||||
|
||||
def update_agent_file(filepath: str) -> bool:
|
||||
"""Update a single agent file with GNS-2 protocol."""
|
||||
|
||||
agent_name = os.path.basename(filepath).replace('.md', '')
|
||||
tier = get_tier(agent_name)
|
||||
|
||||
if tier < 0:
|
||||
print(f"⚠️ Unknown agent: {agent_name}, skipping")
|
||||
return False
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if already updated
|
||||
if 'GNS-2 Protocol' in content:
|
||||
print(f"⏭️ {agent_name} already has GNS-2 protocol")
|
||||
return False
|
||||
|
||||
fm_raw, body = extract_frontmatter(content)
|
||||
|
||||
if fm_raw is None:
|
||||
print(f"❌ {agent_name}: no frontmatter found")
|
||||
return False
|
||||
|
||||
# Update description to mention GNS-2
|
||||
fm_lines = fm_raw.split('\n')
|
||||
new_fm_lines = []
|
||||
for line in fm_lines:
|
||||
if line.startswith('description:'):
|
||||
desc = line.replace('description:', '').strip()
|
||||
new_fm_lines.append(f'description: {desc} (GNS-2 Tier {tier})')
|
||||
else:
|
||||
new_fm_lines.append(line)
|
||||
|
||||
new_fm = '---\n' + '\n'.join(new_fm_lines) + '\n---'
|
||||
|
||||
# Generate GNS-2 section
|
||||
gns_section = generate_gns_protocol(tier)
|
||||
|
||||
# Combine: frontmatter + original body + GNS section
|
||||
# Insert GNS section before <!-- gitea-commenting -->
|
||||
gitea_pattern = r'<gitea-commenting[^/]*/>'
|
||||
|
||||
if re.search(gitea_pattern, body):
|
||||
# Insert before gitea-commenting tag
|
||||
new_body = re.sub(
|
||||
gitea_pattern,
|
||||
f"{gns_section}\n\n\\g<0>",
|
||||
body
|
||||
)
|
||||
else:
|
||||
# Append at end
|
||||
new_body = body + '\n\n' + gns_section
|
||||
|
||||
new_content = new_fm + '\n' + new_body
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print(f"✅ {agent_name} (Tier {tier})")
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("GNS-2 Agent Mass Update")
|
||||
print(f"Target: {AGENTS_DIR}")
|
||||
print(f"Tier 0 (Leaf): {len(TIER_0_AGENTS)}")
|
||||
print(f"Tier 1 (Task): {len(TIER_1_AGENTS)}")
|
||||
print()
|
||||
|
||||
updated = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
for filepath in sorted(glob.glob(os.path.join(AGENTS_DIR, '*.md'))):
|
||||
agent_name = os.path.basename(filepath).replace('.md', '')
|
||||
|
||||
# Skip already updated agents
|
||||
if agent_name in ['capability-analyst', 'agent-architect', 'evaluator']:
|
||||
print(f"⏭️ {agent_name} (already GNS-2)")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
result = update_agent_file(filepath)
|
||||
if result:
|
||||
updated += 1
|
||||
elif 'already' in f'{result}':
|
||||
skipped += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
print(f"Done: {updated} updated, {skipped} skipped, {failed} failed")
|
||||
print(f"Total: {updated + skipped + failed} agents processed")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
74
scripts/validate-gns-agents.py
Normal file
74
scripts/validate-gns-agents.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GNS-2 Agent Protocol Validator
|
||||
Validates that agents follow Gitea-Nervous-System v2.0 protocol.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import yaml
|
||||
import glob
|
||||
|
||||
CHECKPOINT_PATTERN = re.compile(r'## GNS Checkpoint\s*```yaml\s*(.*?)```', re.DOTALL)
|
||||
EVENT_PATTERN = re.compile(r'<!-- GNS_EVENT:\s*(.*?)\s*-->', re.DOTALL)
|
||||
|
||||
def validate_agent_file(path):
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
errors = []
|
||||
agent_name = path.split('/')[-1].replace('.md', '')
|
||||
|
||||
# Check frontmatter
|
||||
if not content.startswith('---'):
|
||||
errors.append('Missing YAML frontmatter')
|
||||
else:
|
||||
parts = content.split('---')
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1])
|
||||
if not fm.get('description'):
|
||||
errors.append('Missing description in frontmatter')
|
||||
if 'mode' not in fm:
|
||||
errors.append('Missing mode in frontmatter')
|
||||
if 'task' not in str(fm.get('permission', {})):
|
||||
errors.append('Missing task permission')
|
||||
except Exception as e:
|
||||
errors.append(f'Invalid YAML frontmatter: {e}')
|
||||
|
||||
# Check GNS protocol sections
|
||||
if 'GNS Checkpoint' not in content:
|
||||
errors.append('Missing GNS Checkpoint section')
|
||||
if 'GNS_EVENT' not in content:
|
||||
errors.append('Missing GNS_EVENT footer example')
|
||||
if 'gns-agent-protocol' not in content.lower() and 'GNS' not in content:
|
||||
errors.append('Agent not updated for GNS-2 protocol')
|
||||
|
||||
return errors
|
||||
|
||||
def main():
|
||||
print("GNS-2 Agent Protocol Validator")
|
||||
print()
|
||||
|
||||
all_valid = True
|
||||
for path in glob.glob('.kilo/agents/*.md'):
|
||||
errors = validate_agent_file(path)
|
||||
agent_name = path.split('/')[-1].replace('.md', '')
|
||||
|
||||
if errors:
|
||||
print(f"❌ {agent_name}: {len(errors)} errors")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
all_valid = False
|
||||
else:
|
||||
print(f"✅ {agent_name}")
|
||||
|
||||
print()
|
||||
if all_valid:
|
||||
print("All agents pass GNS-2 validation")
|
||||
return 0
|
||||
else:
|
||||
print("Some agents need GNS-2 protocol update")
|
||||
return 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
204
scripts/web-test.sh
Normal file
204
scripts/web-test.sh
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Web Testing Quick Start Script
|
||||
#
|
||||
# Usage: ./scripts/web-test.sh <url> [options]
|
||||
#
|
||||
# Project root: Run from project root
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/web-test.sh https://my-app.com
|
||||
# ./scripts/web-test.sh https://my-app.com --auto-fix
|
||||
# ./scripts/web-test.sh https://my-app.com --visual-only
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Get script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default values
|
||||
TARGET_URL=""
|
||||
AUTO_FIX=false
|
||||
VISUAL_ONLY=false
|
||||
CONSOLE_ONLY=false
|
||||
LINKS_ONLY=false
|
||||
THRESHOLD=0.05
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--auto-fix)
|
||||
AUTO_FIX=true
|
||||
shift
|
||||
;;
|
||||
--visual-only)
|
||||
VISUAL_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--console-only)
|
||||
CONSOLE_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--links-only)
|
||||
LINKS_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--threshold)
|
||||
THRESHOLD=$2
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 <url> [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --auto-fix Auto-fix detected issues"
|
||||
echo " --visual-only Run visual tests only"
|
||||
echo " --console-only Run console error detection only"
|
||||
echo " --links-only Run link checking only"
|
||||
echo " --threshold N Visual diff threshold (default: 0.05)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$TARGET_URL" ]]; then
|
||||
TARGET_URL=$1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate URL
|
||||
if [[ -z "$TARGET_URL" ]]; then
|
||||
echo -e "${RED}Error: URL is required${NC}"
|
||||
echo "Usage: $0 <url> [options]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Banner
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BLUE} Web Application Testing Suite${NC}"
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "Target URL: ${YELLOW}${TARGET_URL}${NC}"
|
||||
echo -e "Auto Fix: ${YELLOW}${AUTO_FIX}${NC}"
|
||||
echo -e "Threshold: ${YELLOW}${THRESHOLD}${NC}"
|
||||
echo ""
|
||||
|
||||
# Check Docker
|
||||
echo -e "${BLUE}Checking Docker...${NC}"
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}Error: Docker is not running${NC}"
|
||||
echo "Please start Docker and try again"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Docker is running${NC}"
|
||||
|
||||
# Check if Playwright MCP is running
|
||||
echo -e "${BLUE}Checking Playwright MCP...${NC}"
|
||||
if curl -s http://localhost:8931/mcp -X POST -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | grep -q "tools"; then
|
||||
echo -e "${GREEN}✓ Playwright MCP is running${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Starting Playwright MCP container...${NC}"
|
||||
cd "${PROJECT_ROOT}"
|
||||
docker compose -f docker/docker-compose.web-testing.yml up -d
|
||||
|
||||
# Wait for MCP to be ready
|
||||
echo -n "Waiting for MCP to be ready"
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:8931/mcp -X POST -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | grep -q "tools"; then
|
||||
echo -e " ${GREEN}✓${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! curl -s http://localhost:8931/mcp -X POST -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | grep -q "tools"; then
|
||||
echo -e "${RED}Error: Playwright MCP failed to start${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install dependencies if needed
|
||||
cd "${PROJECT_ROOT}/tests"
|
||||
if [[ ! -d "node_modules" ]]; then
|
||||
echo -e "${BLUE}Installing dependencies...${NC}"
|
||||
npm install --silent
|
||||
fi
|
||||
|
||||
# Export environment
|
||||
export TARGET_URL
|
||||
export PIXELMATCH_THRESHOLD=$THRESHOLD
|
||||
export PLAYWRIGHT_MCP_URL="http://localhost:8931/mcp"
|
||||
export MCP_PORT=8931
|
||||
export REPORTS_DIR="${PROJECT_ROOT}/tests/reports"
|
||||
|
||||
# Run tests
|
||||
echo ""
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BLUE} Running Tests${NC}"
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ "$VISUAL_ONLY" == true ]]; then
|
||||
echo -e "${BLUE}Visual Regression Testing Only${NC}"
|
||||
node scripts/compare-screenshots.js
|
||||
elif [[ "$CONSOLE_ONLY" == true ]]; then
|
||||
echo -e "${BLUE}Console Error Detection Only${NC}"
|
||||
node scripts/console-error-monitor.js
|
||||
elif [[ "$LINKS_ONLY" == true ]]; then
|
||||
echo -e "${BLUE}Link Checking Only${NC}"
|
||||
node scripts/link-checker.js
|
||||
else
|
||||
echo -e "${BLUE}Running All Tests${NC}"
|
||||
node run-all-tests.js
|
||||
fi
|
||||
|
||||
# Check results
|
||||
TEST_RESULT=$?
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BLUE} Test Results${NC}"
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ $TEST_RESULT -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓ All tests passed!${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Tests failed${NC}"
|
||||
|
||||
# Auto-fix if requested
|
||||
if [[ "$AUTO_FIX" == true ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}Auto-fixing detected issues...${NC}"
|
||||
echo ""
|
||||
|
||||
# This would trigger Kilo Code agents
|
||||
# In production, this would call Task tool with the-fixer
|
||||
|
||||
echo -e "${YELLOW}Note: Auto-fix requires Kilo Code integration${NC}"
|
||||
echo -e "${YELLOW}Run: /web-test-fix ${TARGET_URL}${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}Reports generated:${NC}"
|
||||
echo " - ${PROJECT_ROOT}/tests/reports/web-test-report.html"
|
||||
echo " - ${PROJECT_ROOT}/tests/reports/web-test-report.json"
|
||||
echo ""
|
||||
echo -e "${BLUE}To view report:${NC}"
|
||||
echo " open ${PROJECT_ROOT}/tests/reports/web-test-report.html"
|
||||
echo ""
|
||||
|
||||
exit $TEST_RESULT
|
||||
Reference in New Issue
Block a user