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:
NW
2026-05-18 17:53:59 +01:00
parent b680c5aeca
commit 863a67db8e
56 changed files with 8590 additions and 0 deletions

192
scripts/agent-stats.ts Normal file
View 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
View 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
View 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
View 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 };

View 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()

View 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
View 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