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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
.kilo/node_modules/

418
AGENTS.md Normal file
View File

@@ -0,0 +1,418 @@
# Kilo Code Agents Reference
This file configures AI agent behavior for the project - a self-improving code pipeline with Gitea logging.
## Pipeline Workflow
The main workflow is `/pipeline` - use it to process issues through all agents automatically.
```
User: /pipeline 42
Agent: Runs full pipeline for issue #42 with Gitea logging
```
## Commands (Slash Commands)
| Command | Description | Usage |
|---------|-------------|-------|
| `/pipeline <issue>` | Run full agent pipeline for issue | `/pipeline 42` |
| `/nextjs` | Next.js 14+ full-stack app pipeline | `/nextjs my-app` |
| `/vue` | Vue/Nuxt 3 full-stack app pipeline | `/vue my-app` |
| `/laravel` | Laravel full-stack app pipeline | `/laravel my-app` |
| `/wordpress` | WordPress plugin/site pipeline | `/wordpress my-plugin` |
| `/feature` | Feature development pipeline | `/feature` |
| `/commerce` | E-commerce site pipeline | `/commerce` |
| `/status <issue>` | Check pipeline status for issue | `/status 42` |
| `/evolve` | Run evolution cycle with fitness scoring | `/evolve --issue 42` |
| `/evaluate <issue>` | Generate performance report | `/evaluate 42` |
| `/plan` | Creates detailed task plans | `/plan feature X` |
| `/ask` | Answers codebase questions | `/ask how does auth work` |
| `/debug` | Analyzes and fixes bugs | `/debug error in login` |
| `/code` | Quick code generation | `/code add validation` |
| `/research [topic]` | Run research and self-improvement | `/research multi-agent` |
| `/evolution log` | Log agent model change | `/evolution log planner "reason"` |
| `/evolution report` | Generate evolution report | `/evolution report` |
| `/index-project` | Index codebase into .architect/ for agent orientation | `/index-project` |
| `/web-test <url>` | Visual regression testing in Docker | `/web-test https://bbox.wtf` |
| `/e2e-test <url>` | E2E browser automation tests | `/e2e-test https://my-app.com` |
## Pipeline Agents (Subagents)
These agents are invoked automatically by `/pipeline` or manually via `@mention`:
### Core Development
| Agent | Role | When Invoked |
|-------|------|--------------|
| `@RequirementRefiner` | Converts vague ideas and bug reports into strict User Stories with acceptance criteria checklists | Issue status: new |
| `@HistoryMiner` | Analyzes git history to find duplicates and past solutions, preventing regression and duplicate work | Status: planned |
| `@SystemAnalyst` | Designs technical specifications, data schemas, and API contracts before implementation | Status: researching |
| `@SdetEngineer` | Writes tests following TDD methodology | Status: designed |
| `@LeadDeveloper` | Primary code writer for backend and core logic | Status: testing |
| `@FrontendDeveloper` | Handles UI implementation with multimodal capabilities | When UI work needed |
| `@BackendDeveloper` | Backend specialist for Node | When backend needed |
| `@GoDeveloper` | Go backend specialist for Gin, Echo, APIs, and database integration | When Go backend needed |
| `@DevopsEngineer` | DevOps specialist for Docker, Kubernetes, CI/CD pipeline automation, and infrastructure management | When deployment/infra needed |
### Quality Assurance
| Agent | Role | When Invoked |
|-------|------|--------------|
| `@CodeSkeptic` | Adversarial code reviewer | Status: implementing |
| `@TheFixer` | Iteratively fixes bugs based on specific error reports and test failures | When review fails |
| `@PerformanceEngineer` | Reviews code for performance issues | After code-skeptic |
| `@SecurityAuditor` | Scans for security vulnerabilities, OWASP Top 10, dependency CVEs, and hardcoded secrets | After performance |
| `@VisualTester` | Visual regression testing agent that compares screenshots and detects UI differences using pixelmatch and image diff | When UI changes |
### DevOps & Infrastructure
| Agent | Role | When Invoked |
|-------|------|--------------|
| `@devops-engineer` | Docker/Swarm/K8s deployment | When deployment needed |
| `@security-auditor` | Container security scan | After deployment config |
### Cognitive Enhancement
| Agent | Role | When Invoked |
|-------|------|--------------|
| `@Planner` | Advanced task planner using Chain of Thought, Tree of Thoughts, and Plan-Execute-Reflect | Complex tasks |
| `@Reflector` | Self-reflection agent using Reflexion pattern - learns from mistakes | After each agent |
| `@MemoryManager` | Manages agent memory systems - short-term (context), long-term (vector store), and episodic (experiences) | Context management |
### Meta & Process
| Agent | Role | When Invoked |
|-------|------|--------------|
| `@Orchestrator` | Main dispatcher | Manages all agent routing |
| `@ReleaseManager` | Manages git operations, semantic versioning, branching, and deployments | Status: releasing |
| `@Evaluator` | Scores agent effectiveness after task completion for continuous improvement | Status: evaluated |
| `@PromptOptimizer` | Improves agent system prompts based on performance failures | When score < 7 |
| `@ProductOwner` | Manages issue checklists, status labels, tracks progress and coordinates with human users | Manages issues |
| `@AgentArchitect` | Creates, modifies, and reviews new agents, workflows, and skills based on capability gap analysis | When gaps identified |
| `@CapabilityAnalyst` | Analyzes task requirements against available agents, workflows, and skills | When starting new task |
| `@WorkflowArchitect` | Creates and maintains workflow definitions with complete architecture, Gitea integration, and quality gates | New workflow needed |
| `@MarkdownValidator` | Validates and corrects Markdown descriptions for Gitea issues | Before issue creation |
### Security & Incident Response
| Agent | Role | When Invoked |
|-------|------|--------------|
| `@IncidentResponder` | Server incident response, live forensics, malware removal, hardening, SSH-based cleanup | Incident, compromise, breach |
### Status Labels
Pipeline uses Gitea labels to track progress:
- `status: new``status: planned``status: researching` → ...
- Agents add/remove labels automatically
### Performance Logging
Each agent logs to Gitea issue comments:
```markdown
## ✅ lead-developer completed
**Score**: 8/10
**Duration**: 1.2h
**Files**: src/auth.ts, src/user.ts
### Notes
- Clean implementation
- Follows existing patterns
- Tests passing
```
### Efficiency Tracking
Scores saved to `.kilo/logs/efficiency_score.json`:
```json
{
"version": "1.0",
"history": [
{
"issue": 42,
"date": "2024-01-02T10:00:00Z",
"agents": {
"lead-developer": 8,
"code-skeptic": 7,
"the-fixer": 9
},
"iterations": 2,
"duration_hours": 1.5
}
]
}
```
### Fitness Tracking
Fitness scores saved to `.kilo/logs/fitness-history.jsonl`:
```jsonl
{"ts":"2026-04-06T00:00:00Z","issue":42,"workflow":"feature","fitness":0.82,"tokens":38400,"time_ms":245000,"tests_passed":45,"tests_total":47}
{"ts":"2026-04-06T01:30:00Z","issue":43,"workflow":"bugfix","fitness":0.91,"tokens":12000,"time_ms":85000,"tests_passed":47,"tests_total":47}
```
## Manual Agent Invocation
```typescript
// Use Task tool to invoke subagent
Task tool with:
subagent_type: "lead-developer"
prompt: "Implement authentication for issue #42"
```
Or via `@mention`:
```
@lead-developer implement authentication flow
```
## Environment Variables
Gitea integration uses centralized authentication (see `.kilo/shared/gitea-auth.md` and `.kilo/gitea.jsonc`):
| Variable | Required | Description |
|----------|----------|-------------|
| `GITEA_API_URL` | No | API base URL (default: `https://git.softuniq.eu/api/v1`) |
| `GITEA_TOKEN` | Preferred | Pre-existing API token |
| `GITEA_USER` | Fallback | Username for Basic Auth token creation |
| `GITEA_PASS` | Fallback | Password for Basic Auth token creation |
| `GITEA_TARGET_REPO` | No | Override target project (auto-detected otherwise) |
Auth resolution: `GITEA_TOKEN``GITEA_USER+GITEA_PASS``ValueError`. **NEVER hardcode credentials.**
## Self-Improvement Cycle
1. **Pipeline runs** for each issue
2. **Evaluator scores** each agent (1-10) - subjective
3. **Pipeline Judge measures** fitness objectively (0.0-1.0)
4. **Low fitness (<0.70)** triggers prompt-optimizer
5. **Prompt optimizer** analyzes failures and improves prompts
6. **Re-run workflow** with improved prompts
7. **Compare fitness** before/after - commit if improved
8. **Log results** to `.kilo/logs/fitness-history.jsonl`
### Evaluator vs Pipeline Judge
| Aspect | Evaluator | Pipeline Judge |
|--------|-----------|----------------|
| Type | Subjective | Objective |
| Score | 1-10 (opinion) | 0.0-1.0 (metrics) |
| Metrics | Observations | Tests, tokens, time |
| Trigger | After workflow | After evaluator |
| Action | Logs to Gitea | Triggers optimization |
### Fitness Score Components
```
fitness = (test_pass_rate × 0.50) + (quality_gates_rate × 0.25) + (efficiency_score × 0.25)
where:
test_pass_rate = passed_tests / total_tests
quality_gates_rate = passed_gates / total_gates (build, lint, types, tests, coverage)
efficiency_score = 1.0 - clamp(normalized_cost, 0, 1)
```
## Architecture Files
| File | Purpose |
|------|---------|
| `AGENTS.md` | This file - main config |
| `.kilo/agents/*.md` | Agent definitions with prompts |
| `.kilo/commands/*.md` | Workflow commands |
| `.kilo/rules/*.md` | Custom rules loaded globally |
| `.kilo/skills/` | Skill modules |
| `.kilo/shared/gitea-auth.md` | Centralized Gitea auth (env vars, no hardcoded creds) |
| `.kilo/gitea.jsonc` | Gitea auth structure (env var mapping) |
| `.kilo/shared/gitea-api.md` | Centralized Gitea API client |
| `.kilo/shared/gitea-commenting.md` | Comment format for Gitea |
| `.kilo/shared/self-evolution.md` | Self-evolution protocol |
| `.kilo/rules/architect-first-contact.md` | First-contact project indexing rules |
| `.kilo/skills/project-mapping/SKILL.md` | Project mapping skill (`.architect/` system) |
| `.architect/` | Project codebase map (auto-indexed, see below) |
| `src/kilocode/` | TypeScript API for programmatic use |
## Skills Reference
### Containerization Skills
| Skill | Purpose | Location |
|-------|---------|----------|
| `docker-compose` | Multi-container orchestration | `.kilo/skills/docker-compose/` |
| `docker-swarm` | Production cluster deployment | `.kilo/skills/docker-swarm/` |
| `docker-security` | Container security hardening | `.kilo/skills/docker-security/` |
| `docker-monitoring` | Container monitoring/logging | `.kilo/skills/docker-monitoring/` |
### Node.js Skills
| Skill | Purpose | Location |
|-------|---------|----------|
| `nodejs-express-patterns` | Express routing, middleware | `.kilo/skills/nodejs-express-patterns/` |
| `nodejs-auth-jwt` | JWT authentication | `.kilo/skills/nodejs-auth-jwt/` |
| `nodejs-security-owasp` | OWASP security | `.kilo/skills/nodejs-security-owasp/` |
### Database Skills
| Skill | Purpose | Location |
|-------|---------|----------|
| `postgresql-patterns` | PostgreSQL patterns | `.kilo/skills/postgresql-patterns/` |
| `sqlite-patterns` | SQLite patterns | `.kilo/skills/sqlite-patterns/` |
| `clickhouse-patterns` | ClickHouse patterns | `.kilo/skills/clickhouse-patterns/` |
### Go Skills
| Skill | Purpose | Location |
|-------|---------|----------|
| `go-modules` | Go modules management | `.kilo/skills/go-modules/` |
| `go-concurrency` | Goroutines and channels | `.kilo/skills/go-concurrency/` |
| `go-testing` | Go testing patterns | `.kilo/skills/go-testing/` |
| `go-security` | Go security patterns | `.kilo/skills/go-security/` |
### Process Skills
| Skill | Purpose | Location |
|-------|---------|----------|
| `planning-patterns` | CoT/ToT planning | `.kilo/skills/planning-patterns/` |
| `memory-systems` | Memory management | `.kilo/skills/memory-systems/` |
| `tool-use` | Tool usage patterns | `.kilo/skills/tool-use/` |
| `research-cycle` | Self-improvement cycle | `.kilo/skills/research-cycle/` |
## Using the TypeScript API
```typescript
import {
PipelineRunner,
GiteaClient,
decideRouting
} from './src/kilocode/index.js'
const runner = await createPipelineRunner({
giteaToken: process.env.GITEA_TOKEN
})
await runner.run({ issueNumber: 42 })
```
## Agent Evolution Dashboard
Track agent model changes, performance, and recommendations in real-time.
### Access
```bash
# Sync agent data
bun run sync:evolution
# Open dashboard
bun run evolution:dashboard
bun run evolution:open
# or visit http://localhost:3001
```
### Dashboard Tabs
| Tab | Description |
|-----|-------------|
| **Overview** | Stats, recent changes, pending recommendations |
| **All Agents** | Filterable agent cards with history |
| **Timeline** | Full evolution history |
| **Recommendations** | Priority-based model suggestions |
| **Model Matrix** | Agent × Model mapping with fit scores |
### Data Sources
| Source | What it tracks |
|--------|----------------|
| `.kilo/agents/*.md` | Model, description, capabilities |
| `.kilo/kilo.jsonc` | Model assignments |
| `.kilo/capability-index.yaml` | Capability routing |
| Git History | Model and prompt changes |
| Gitea Comments | Performance scores |
### Evolution Data Structure
```json
{
"agents": {
"lead-developer": {
"current": { "model": "qwen3-coder:480b", "fit_score": 92 },
"history": [{ "type": "model_change", "from": "deepseek", "to": "qwen3" }],
"performance_log": [{ "issue": 42, "score": 8, "success": true }]
}
}
}
```
### Recommendations Priority
| Priority | When | Example |
|----------|------|---------|
| **Critical** | Fit score < 70 | Immediate model change required |
| **High** | Model unavailable | Switch to fallback |
| **Medium** | Better model available | Consider upgrade |
| **Low** | Optimization possible | Optional improvement |
## Agent Execution Monitoring
Every agent invocation is logged to `.kilo/logs/agent-executions.jsonl` for project-level monitoring.
### Log Format
```jsonl
{"ts":"2026-04-18T14:00:00Z","agent":"php-developer","issue":42,"project":"UniqueSoft/my-shop","task":"Create Product model","subtask_type":"model_creation","duration_ms":45000,"tokens_used":8500,"status":"success","files":["app/Models/Product.php"],"score":8,"next_agent":"code-skeptic"}
```
### Monitoring Commands
```bash
# Agent stats report
bun run scripts/agent-stats.ts
# Stats for last 7 days
bun run scripts/agent-stats.ts --last 7
# Stats for specific project
bun run scripts/agent-stats.ts --project UniqueSoft/my-shop
```
### Required Logging Fields
| Field | Description |
|-------|-------------|
| `agent` | Agent name |
| `issue` | Gitea issue number |
| `project` | Target project repo (NOT hardcoded APAW) |
| `task` | Atomic task description |
| `duration_ms` | Execution time |
| `tokens_used` | Token estimate |
| `status` | success/fail/pass/blocked |
## Critical Rules
### Target Project (NOT APAW)
**Issues MUST be created in the target project repository, NOT in APAW.** APAW is the agent framework, not the default project.
```bash
# Auto-detect from git remote
TARGET_REPO=$(git remote get-url origin | sed 's:/*$::' | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|')
```
### Atomic Tasks (1 action = 1 task)
Every agent invocation solves exactly ONE atomic task:
- ❌ "Implement the entire e-commerce backend"
- ✅ "Create Product model with migration"
- ✅ "Add POST /api/products endpoint"
### Modular Code
- Maximum 100 lines per file
- Maximum 30 lines per function
- Features organized as independent modules
- Cross-module communication via events/interfaces only
### Token Budgets
| Task Size | Max Tokens | Example |
|----------|-----------|---------|
| Tiny | 2,000 | Fix typo, add config |
| Small | 5,000 | Create model + migration |
| Medium | 10,000 | Create API endpoint + test |
| Large | 20,000 | Create service with 3 methods |
## Code Style
- Use TypeScript for new files
- Follow existing patterns
- Write tests before code (TDD)
- Keep functions under 50 lines
- Use early returns
- No comments unless explicitly requested

309
STRUCTURE.md Normal file
View File

@@ -0,0 +1,309 @@
# Project Structure
This document describes the organized structure of the APAW project.
## Root Directory
```
APAW/
├── .architect/ # Project codebase map (auto-indexed)
│ ├── README.md # Navigation index
│ ├── project.json # Machine-readable project metadata
│ ├── state.json # Index freshness state (hashes, timestamps)
│ ├── architecture/ # Architecture overview and dependency graph
│ ├── entities/ # Domain entities, fields, relationships
│ ├── db-schema/ # Database tables, columns, indexes
│ ├── api-surface/ # API endpoints, methods, auth
│ ├── conventions/ # Naming, patterns, forbidden practices
│ ├── maps/ # Programmatic file/module graphs (.gitignored)
│ └── tech-stack/ # Languages, frameworks, databases, tools
├── .kilo/ # Kilo Code configuration
│ ├── agents/ # 30 agent definitions (YAML frontmatter)
│ │ ├── orchestrator.md # Main dispatcher
│ │ ├── php-developer.md # PHP/Laravel/Symfony/WordPress
│ │ ├── python-developer.md # Python/Django/FastAPI
│ │ ├── lead-developer.md # Primary code writer
│ │ ├── code-skeptic.md # Adversarial review
│ │ └── ... (25 more)
│ ├── commands/ # Slash commands
│ │ ├── pipeline.md # Full agent pipeline
│ │ ├── laravel.md # Laravel web app pipeline
│ │ ├── nextjs.md # Next.js app pipeline
│ │ ├── vue.md # Vue/Nuxt app pipeline
│ │ ├── wordpress.md # WordPress development pipeline
│ │ ├── feature.md # Feature development
│ │ ├── commerce.md # E-commerce site
│ │ └── ... (15 more)
│ ├── rules/ # Global rules (loaded for all agents)
│ │ ├── global.md # Base rules
│ │ ├── atomic-tasks.md # 1 action = 1 task principle
│ │ ├── modular-code.md # Modules, libraries, microservices
│ │ ├── token-optimization.md # Token budget rules
│ │ ├── gitea-centric-workflow.md # Gitea as center of work
│ │ └── ... (10 more)
│ ├── skills/ # Agent skills
│ │ ├── php-laravel-patterns/ # Laravel patterns
│ │ ├── php-symfony-patterns/ # Symfony patterns
│ │ ├── php-wordpress-patterns/ # WordPress patterns
│ │ ├── php-security/ # OWASP, CSRF, XSS
│ │ ├── php-testing/ # PHPUnit, Pest
│ │ ├── php-modular-architecture/ # Module separation
│ │ ├── agent-logging/ # Execution logging
│ │ ├── gitea-workflow/ # Gitea integration (auto-detect project)
│ │ ├── gitea-commenting/ # Comment format (auto-detect project)
│ │ └── ... (30+ more)
│ ├── shared/ # Shared modules
│ │ ├── gitea-api.md # Centralized API client (auto-detect repo)
│ │ ├── gitea-auth.md # Centralized auth (env vars, no hardcoded creds)
│ │ ├── gitea-commenting.md # Comment format
│ │ └── self-evolution.md # Evolution protocol
│ ├── logs/ # Execution logs
│ │ ├── agent-executions.jsonl # Every agent invocation
│ │ ├── fitness-history.jsonl # Fitness scores
│ │ └── efficiency_score.json # Efficiency history
│ ├── capability-index.yaml # Agent capabilities & routing
│ ├── gitea.jsonc # Gitea auth structure (env var mapping, NO secrets)
│ ├── kilo.jsonc # Primary agent config
│ ├── KILO_SPEC.md # Specification
│ └── EVOLUTION_LOG.md # Evolution timeline
├── agent-evolution/ # Evolution Dashboard
│ ├── index.standalone.html # Standalone dashboard
│ ├── scripts/ # Sync & build scripts
│ ├── data/ # Agent version history
│ └── docker-compose.yml # Docker launch
├── scripts/ # Utility scripts
│ └── agent-stats.ts # Agent execution statistics
├── docker/ # Docker configurations
│ ├── Dockerfile.playwright # Playwright MCP container
│ ├── Dockerfile.architect-indexer # Project indexer container
│ ├── docker-compose.yml # Base config
│ ├── docker-compose.architect.yml # Architect indexer service
│ └── docker-compose.web-testing.yml
├── AGENTS.md # Agent reference (main config)
├── STRUCTURE.md # This document
└── README.md # Project overview
```
## Key Configuration Files
### capability-index.yaml
Maps agent capabilities for orchestrator routing:
### `.architect/` Directory (Project Brain)
Auto-indexed codebase map that all agents read before starting work:
| File | Purpose |
|------|---------|
| `.architect/README.md` | Navigation index (auto-updated) |
| `.architect/project.json` | Machine-readable project metadata |
| `.architect/state.json` | Index freshness (hashes, timestamps) |
| `.architect/architecture/overview.md` | Architecture pattern, layers |
| `.architect/architecture/dependency-graph.md` | Module dependency graph |
| `.architect/entities/entities.md` | Domain entities, relations |
| `.architect/db-schema/schema.md` | Tables, columns, indexes, FKs |
| `.architect/api-surface/endpoints.md` | API routes, methods, auth |
| `.architect/conventions/conventions.md` | Coding standards, patterns |
| `.architect/tech-stack/stack.md` | Languages, frameworks, versions |
| `.architect/maps/file-graph.json` | File imports/exports graph |
| `.architect/maps/module-graph.json` | Module dependencies graph |
See `.kilo/skills/project-mapping/SKILL.md` for full documentation.
### capability-index.yaml
Maps agent capabilities for orchestrator routing:
```yaml
agents:
php-developer:
capabilities:
- php_web_development
- laravel_development
- symfony_development
- wordpress_development
model: ollama-cloud/qwen3-coder:480b
mode: subagent
delegates_to:
- code-skeptic
- security-auditor
capability_routing:
php_web_development: php-developer
laravel_development: php-developer
```
### kilo.jsonc
Primary agents configuration:
```jsonc
{
"model": "ollama-cloud/glm-5.1",
"default_agent": "orchestrator",
"agent": {
"orchestrator": { "model": "ollama-cloud/glm-5.1", "variant": "thinking" },
"code": { "model": "ollama-cloud/qwen3-coder:480b" },
"ask": { "model": "ollama-cloud/glm-5.1", "variant": "instant" },
"plan": { "model": "ollama-cloud/nemotron-3-super" },
"debug": { "model": "ollama-cloud/glm-5.1", "variant": "thinking" }
}
}
```
## Rules System
Rules in `.kilo/rules/` are loaded globally for all agents:
### Critical Rules
| Rule | Purpose |
|------|---------|
| `atomic-tasks.md` | 1 action = 1 task, max 100 lines/file, task sizing |
| `modular-code.md` | Modules, services, repositories, events |
| `token-optimimization.md` | Token budgets, no scope creep |
| `gitea-centric-workflow.md` | Issues before work, progress tracking, research |
| `global.md` | Base coding rules |
### Language-Specific Rules
| Rule | Purpose |
|------|---------|
| `php.md` | PSR-12, TDD, security |
| `nodejs.md` | Express, JWT, async |
| `go.md` | Error wrapping, context, table-driven tests |
| `flutter.md` | Riverpod, Clean Architecture |
| `docker.md` | Multi-stage, security |
## Skills System
Skills are capability modules agents reference:
### PHP Development
| Skill | Lines | Purpose |
|-------|-------|---------|
| `php-laravel-patterns` | 403 | Routing, Eloquent, Services, Repositories, Auth, Queues |
| `php-symfony-patterns` | 233 | Controllers, Doctrine, Messenger, Voters |
| `php-wordpress-patterns` | 276 | Plugins, CPT, REST API, Security |
| `php-security` | 147 | OWASP Top 10, CSRF, XSS, SQL injection |
| `php-testing` | 242 | PHPUnit, Pest, Dusk browser tests |
| `php-modular-architecture` | 242 | Module separation, interfaces, events |
### Frontend Frameworks
| Skill | Lines | Purpose |
|-------|-------|---------|
| `nextjs-patterns` | 290 | Next.js 14+ App Router, Server Components, Server Actions, Auth.js |
| `vue-nuxt-patterns` | 270 | Vue 3 / Nuxt 3 Composition API, Pinia, Nitro, SSR |
| `react-patterns` | 240 | React 18+ hooks, Context, TanStack Query, React Hook Form |
### Python Development
| Skill | Lines | Purpose |
|-------|-------|---------|
| `python-django-patterns` | 200 | Django models, DRF, services, repositories |
| `python-fastapi-patterns` | 230 | FastAPI async, Pydantic, SQLAlchemy, dependencies |
### Infrastructure
| Skill | Purpose |
|-------|---------|
| `gitea-workflow` | Issue creation, quality gates, progress tracking |
| `gitea-commenting` | Comment format with auto-project detection |
| `agent-logging` | Execution logging to agent-executions.jsonl |
| `docker-compose` | Docker Compose patterns |
| `docker-swarm` | Docker Swarm orchestration |
## Execution Logging
Every agent invocation is logged to `.kilo/logs/agent-executions.jsonl`:
```jsonl
{"ts":"2026-04-18T14:00:00Z","agent":"php-developer","issue":42,"project":"UniqueSoft/my-shop","task":"Create Product model","subtask_type":"model_creation","duration_ms":45000,"tokens_used":8500,"status":"success","files":["app/Models/Product.php"],"score":8,"next_agent":"code-skeptic"}
```
### Required Fields
| Field | Description |
|-------|-------------|
| `agent` | Agent name |
| `issue` | Gitea issue number |
| `project` | Target project repo (auto-detected, NOT hardcoded) |
| `task` | Atomic task description |
| `duration_ms` | Execution time |
| `tokens_used` | Token estimate |
| `status` | success/fail/pass/blocked |
| `score` | Self-assessment 1-10 |
| `next_agent` | Next agent in pipeline |
### Statistics
```bash
bun run agent:stats # Last 30 days
bun run agent:stats:week # Last 7 days
bun run agent:stats:project # Filter by project
```
## Gitea Integration
### Centralized Authentication
All Gitea API calls use `get_gitea_token()` from `.kilo/shared/gitea-auth.md`. Configuration structure in `.kilo/gitea.jsonc` maps env var names — no actual credentials in code.
### Critical: Target Project Detection
Issues MUST be created in the target project, NOT in APAW:
```python
def get_target_repo():
"""Auto-detect from git remote - NEVER hardcode"""
result = subprocess.run(['git', 'remote', 'get-url', 'origin'], capture_output=True, text=True)
match = re.search(r'[:/]([^/]+/[^/]+?)(?:\.git)?$', result.stdout.strip())
if match:
return match.group(1)
return os.environ.get('GITEA_TARGET_REPO', 'UniqueSoft/APAW')
```
All API calls use `get_target_repo()` instead of hardcoded repo.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `GITEA_API_URL` | `https://git.softuniq.eu/api/v1` | Gitea API endpoint |
| `GITEA_TOKEN` | - | Gitea API token (PREFERRED) |
| `GITEA_USER` | - | Gitea username (fallback for Basic Auth) |
| `GITEA_PASS` | - | Gitea password (fallback for Basic Auth) |
| `GITEA_TARGET_REPO` | auto-detect | Override target project |
| `TARGET_URL` | `http://localhost:3000` | URL for testing |
| `PIXELMATCH_THRESHOLD` | `0.05` | Visual diff tolerance |
Auth resolution: `GITEA_TOKEN``GITEA_USER+GITEA_PASS``ValueError`. See `.kilo/shared/gitea-auth.md`.
## Quick Reference
```bash
# Pipeline for issue
/pipeline 42
# Laravel app
/laravel project_name
# WordPress plugin
/wordpress plugin_name
# Agent statistics
bun run agent:stats
# Evolution dashboard
bun run sync:evolution
bun run evolution:open
# Docker containers
docker compose -f docker/docker-compose.web-testing.yml up -d
# Web tests
./scripts/web-test.sh https://your-app.com
```

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
E2E Test Example: Login Flow with Screenshot Upload to Gitea
This test demonstrates:
1. Browser automation with Playwright
2. Screenshot capture on error
3. Upload screenshots to Gitea issues
"""
import json
import urllib.request
import urllib.error
import base64
import os
import sys
from datetime import datetime
# Configuration
GITEA_URL = "https://git.softuniq.eu/api/v1"
GITEA_USER = "NW"
GITEA_PASSWORD = "eshkink0t" # Note: zero not 'o'
REPO_OWNER = "UniqueSoft"
REPO_NAME = "APAW"
def get_gitea_token():
"""Get Gitea API token using basic auth"""
credentials = base64.b64encode(f"{GITEA_USER}:{GITEA_PASSWORD}".encode()).decode()
req = urllib.request.Request(
f"{GITEA_URL}/users/{GITEA_USER}/tokens",
data=json.dumps({"name": f"screenshot-{os.getpid()}", "scopes": ["all"]}).encode(),
headers={'Content-Type': 'application/json', 'Authorization': f'Basic {credentials}'},
method='POST'
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())['sha1']
def upload_screenshot_to_gitea(issue_number, screenshot_path, description="Error screenshot"):
"""
Upload a screenshot to Gitea and post comment with reference.
"""
token = get_gitea_token()
with open(screenshot_path, 'rb') as f:
file_content = f.read()
filename = os.path.basename(screenshot_path)
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
body = f'--{boundary}\r\n'.encode()
body += f'Content-Disposition: form-data; name="attachment"; filename="{filename}"\r\n'.encode()
body += b'Content-Type: image/png\r\n\r\n'
body += file_content
body += f'\r\n--{boundary}--\r\n'.encode()
req = urllib.request.Request(
f"{GITEA_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue_number}/assets",
data=body,
headers={
'Content-Type': f'multipart/form-data; boundary={boundary}',
'Authorization': f'token {token}'
},
method='POST'
)
with urllib.request.urlopen(req) as r:
result = json.loads(r.read())
uuid = result['uuid']
# Post comment with screenshot reference
comment_body = f"""## 📸 {description}
![{description}](/attachments/{uuid})
**File**: `{filename}`
**Size**: {os.path.getsize(screenshot_path)} bytes
**Uploaded**: {datetime.now().isoformat()}
"""
req = urllib.request.Request(
f"{GITEA_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue_number}/comments",
data=json.dumps({"body": comment_body}).encode(),
headers={'Content-Type': 'application/json', 'Authorization': f'token {token}'},
method='POST'
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
def create_test_screenshot(error_message, test_name):
"""Create a test screenshot (SVG placeholder)"""
screenshot_path = f".test/screenshots/current/{test_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
svg_content = f"""<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
<rect width="800" height="600" fill="#f0f0f0"/>
<rect x="50" y="50" width="700" height="100" fill="#ff6b6b"/>
<text x="400" y="100" text-anchor="middle" fill="white" font-size="24" font-weight="bold">ERROR: {test_name}</text>
<rect x="50" y="170" width="700" height="380" fill="white" stroke="#ddd"/>
<text x="70" y="200" fill="#333" font-size="14" font-family="monospace">{error_message[:80]}</text>
<rect x="50" y="560" width="700" height="30" fill="#e9ecef"/>
<text x="400" y="580" text-anchor="middle" fill="#666" font-size="12">Screenshot by browser-automation agent</text>
</svg>"""
with open(screenshot_path, 'w') as f:
f.write(svg_content)
return screenshot_path
def run_login_test():
"""Test login flow with error screenshot"""
test_name = "login_flow"
issue_number = 12
print(f"Running test: {test_name}")
try:
# Simulate test
# browser_navigate("https://example.com/login")
# browser_type("input[name=email]", "test@example.com")
# browser_click("button[type=submit]")
# Simulate error
raise Exception("Login button not found - element '#login-btn' does not exist")
except Exception as e:
print(f"Test FAILED: {e}")
# Create and upload screenshot
screenshot_path = create_test_screenshot(str(e), test_name)
print(f"Screenshot created: {screenshot_path}")
result = upload_screenshot_to_gitea(
issue_number=issue_number,
screenshot_path=screenshot_path,
description=f"Test Failure: {test_name}"
)
print(f"Screenshot uploaded to Gitea")
return False
return True
def main():
print("=" * 60)
print("E2E Browser Tests with Screenshot Upload")
print("=" * 60)
os.makedirs(".test/screenshots/current", exist_ok=True)
run_login_test()
print("\n✅ Test complete - check Gitea Issue #12 for screenshot")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
<rect width="800" height="600" fill="#f0f0f0"/>
<rect x="50" y="50" width="700" height="100" fill="#ff6b6b"/>
<text x="400" y="100" text-anchor="middle" fill="white" font-size="24" font-weight="bold">ERROR: login_flow</text>
<rect x="50" y="170" width="700" height="380" fill="white" stroke="#ddd"/>
<text x="70" y="200" fill="#333" font-size="14" font-family="monospace">Login button not found - element '#login-btn' does not exist</text>
<rect x="50" y="560" width="700" height="30" fill="#e9ecef"/>
<text x="400" y="580" text-anchor="middle" fill="#666" font-size="12">Screenshot by browser-automation agent</text>
</svg>

After

Width:  |  Height:  |  Size: 704 B

141
archive/AGENT_AUDIT.md Normal file
View File

@@ -0,0 +1,141 @@
# Agent, Workflow, and Skill Audit Report
## Audit Date: 2026-04-04
## Model Availability
Available model prefixes:
- `ollama-cloud/` - Primary cloud models
- `openrouter/` - Router models
- `qwen/` - Qwen models
- `groq/` - Groq models
**NOT Available:**
- `anthropic/` - Claude models (❌ removed)
- `openai/` - OpenAI models directly (use via openrouter, or specific ollama-cloud/gpt-oss)
## Agents Audit
| Agent | Model | Status | Issues |
|-------|-------|--------|--------|
| orchestrator | ollama-cloud/glm-5 | ✅ OK | - |
| requirement-refiner | ollama-cloud/kimi-k2-thinking | ✅ OK | - |
| history-miner | ollama-cloud/gpt-oss:20b | ✅ OK | - |
| system-analyst | openrouter/qwen/qwen3.6-plus:free | ✅ OK | - |
| product-owner | openrouter/qwen/qwen3.6-plus:free | ✅ OK | - |
| lead-developer | ollama-cloud/qwen3-coder:480b | ✅ OK | - |
| frontend-developer | ollama-cloud/kimi-k2.5 | ✅ OK | - |
| sdet-engineer | ollama-cloud/qwen3-coder:480b | ✅ OK | - |
| code-skeptic | ollama-cloud/minimax-m2.5 | ✅ OK | - |
| the-fixer | ollama-cloud/minimax-m2.5 | ✅ OK | - |
| performance-engineer | ollama-cloud/nemotron-3-super | ✅ OK | - |
| security-auditor | ollama-cloud/kimi-k2.5 | ✅ OK | - |
| release-manager | ollama-cloud/qwen3-coder:480b | ✅ OK | - |
| evaluator | ollama-cloud/gpt-oss:120b | ✅ OK | - |
| prompt-optimizer | openrouter/qwen/qwen3.6-plus:free | ✅ OK | - |
| **capability-analyst** | ~~anthropic/claude-sonnet-4~~ | ⚠️ FIXED | Changed to ollama-cloud/gpt-oss:120b |
| **agent-architect** | ~~anthropic/claude-sonnet-4~~ | ⚠️ FIXED | Changed to ollama-cloud/gpt-oss:120b |
| markdown-validator | qwen/qwen3.6-plus:free | ✅ OK | - |
## Commands/Workflows Audit
| Command | Model | Status | Issues |
|---------|-------|--------|--------|
| pipeline | - | ✅ OK | Uses subagent models |
| status | qwen/qwen3.6-plus:free | ✅ OK | - |
| evaluate | ollama-cloud/gpt-oss:120b | ✅ OK | - |
| plan | openrouter/qwen/qwen3-coder:free | ✅ OK | - |
| ask | groq/qwen3-32b | ✅ OK | - |
| debug | ollama-cloud/gpt-oss:20b | ✅ OK | - |
| code | openrouter/qwen/qwen3-coder:free | ✅ OK | - |
| review | openrouter/minimax/minimax-m2.5:free | ✅ OK | - |
| feature | openrouter/qwen/qwen3-coder:free | ✅ OK | - |
| hotfix | openrouter/minimax/minimax-m2.5:free | ✅ OK | - |
| **review-watcher** | ~~openai/compound~~ | ⚠️ FIXED | Changed to ollama-cloud/glm-5 |
## Skills Audit
| Skill | Status | Notes |
|-------|--------|-------|
| gitea | ✅ OK | TypeScript module |
| scoped-labels | ✅ OK | Documentation only |
| fix-workflow | ✅ OK | Documentation only |
## Issues Fixed
### 1. Unavailable Models (2 agents)
**Before:**
```yaml
capability-analyst: anthropic/claude-sonnet-4-20250514
agent-architect: anthropic/claude-sonnet-4-20250514
```
**After:**
```yaml
capability-analyst: ollama-cloud/gpt-oss:120b
agent-architect: ollama-cloud/gpt-oss:120b
```
### 2. Invalid Model for Command (1 command)
**Before:**
```yaml
review-watcher: openai/compound
```
**After:**
```yaml
review-watcher: ollama-cloud/glm-5
```
### 3. Duplicate Model Definitions (1 agent)
agent-architect.md had 3 model definitions, fixed to single correct one.
## Model Profile Recommendations
### Analysis & Strategy
- `ollama-cloud/gpt-oss:120b` - Complex reasoning, analysis
- `ollama-cloud/glm-5` - Routing, orchestration, simple tasks
### Code Generation
- `ollama-cloud/qwen3-coder:480b` - Primary code generation
- `openrouter/qwen/qwen3-coder:free` - Free alternative
### Code Review
- `ollama-cloud/minimax-m2.5` - Critical analysis
- `ollama-cloud/nemotron-3-super` - Performance review
### Security & Testing
- `ollama-cloud/kimi-k2.5` - Security audit, frontend
- `ollama-cloud/kimi-k2-thinking` - Requirements analysis
### Light Tasks
- `openrouter/qwen/qwen3.6-plus:free` - Documentation, planning
- `qwen/qwen3.6-plus:free` - Quick tasks
- `groq/qwen3-32b` - Fast queries
## Remaining Consistency Issues
### Model Prefix Inconsistency
Some models use different prefixes for the same provider:
- `qwen/qwen3.6-plus:free` vs `openrouter/qwen/qwen3.6-plus:free`
**Recommendation:** Standardize to one prefix pattern.
### Suggested Standardization
| Current | Standardize To |
|---------|----------------|
| `qwen/qwen3.6-plus:free` | `openrouter/qwen/qwen3.6-plus:free` |
## Summary
- **Total Agents:** 18
- **Total Commands:** 11
- **Total Skills:** 3
- **Issues Found:** 4
- **Issues Fixed:** 4
- **Status:** ✅ All models now use available endpoints

40
archive/ARCHIVE_README.md Normal file
View File

@@ -0,0 +1,40 @@
# Archive
This directory contains deprecated or unused files that have been moved from the root for cleanup.
## Files and Directories
### Docker & Infrastructure
| File | Description |
|------|-------------|
| `docker-compose.yml` | Docker Compose configuration |
| `Dockerfile.playwright` | Playwright Docker image |
| `run-playwright-tests.sh` | Playwright test runner |
### Scripts
| File | Description |
|------|-------------|
| `scripts/` | Legacy test and deployment scripts |
### Documentation (Old)
| File | Description |
|------|-------------|
| `IMPROVEMENT_PROPOSAL.md` | Original analysis document (superseded by .kilo/) |
| `BROWSER_VISIBILITY.md` | Old documentation |
| `README.Docker.md` | Docker setup guide (superseded) |
| `AGENT_AUDIT.md` | Agent audit document |
| `GITEA_INTEGRATION.md` | Gitea integration docs |
### Test Artifacts
| File | Description |
|------|-------------|
| `.test/` | Legacy test screenshots and E2E tests |
### Docs
| File | Description |
|------|-------------|
| `docs/` | Deprecated documentation files |
---
**Note:** The active system is now in `.kilo/` directory. All agents, skills, rules, and workflows are managed there.

View File

@@ -0,0 +1,99 @@
# Browser Visibility Configuration
## By Default: Browser is VISIBLE (headed mode)
This allows you to observe browser actions in real-time during testing.
## Configuration Options
### Option 1: Visible Browser (Default for Development)
```bash
# Docker: Browser window visible
docker-compose up playwright-mcp
# Or directly:
npx @playwright/mcp@latest --browser chromium --no-sandbox
# (without --headless flag = visible window)
```
### Option 2: Hidden Browser (For CI/CD)
Set environment variable:
```bash
export PLAYWRIGHT_MCP_HEADLESS=true
```
Or use flag:
```bash
npx @playwright/mcp@latest --headless --browser chromium --no-sandbox
```
## Docker Setup for Visible Browser
### Linux
```bash
# Allow Docker to access X11
xhost +local:docker
# Run with display
docker-compose up playwright-mcp
```
### macOS
```bash
# Install XQuartz for X11
brew install --cask xquartz
# Start XQuartz
open -a XQuartz
# Allow connections
xhost +local:
# Run with display
docker-compose up playwright-mcp
```
### Windows (WSL2)
```bash
# Install VcXsrv or use WSLg (Windows 11)
# Export display
export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0
# Run
docker-compose up playwright-mcp
```
## Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `PLAYWRIGHT_MCP_HEADLESS` | `false` | `true` = hidden, `false` = visible |
| `PLAYWRIGHT_MCP_BROWSER` | `chromium` | Browser to use |
| `DISPLAY` | `:0` | X11 display for headed mode |
## Benefits of Headed Mode
1. **Observe Actions** - Watch browser in real-time
2. **Debug Tests** - See what's happening visually
3. **Learn** - Understand how automation works
4. **Verify** - Confirm correct behavior visually
## When to Use Headless
For CI/CD pipelines or production testing where you don't need to see the browser:
```bash
# Set in CI environment
export PLAYWRIGHT_MCP_HEADLESS=true
# Or in docker-compose.override.yml
services:
playwright-mcp:
environment:
- PLAYWRIGHT_MCP_HEADLESS=true
```

View File

@@ -0,0 +1,239 @@
# Gitea API Integration
Интеграция с Gitea API 1.21+ для автмоатического управления issues, milestones и логирования работы агентов.
## Установка
```bash
# Установите зависимости
bun install
# Или с npm
npm install
```
## Настройка
### Вариант 1: Токен через окружение
```bash
# Установите переменные окружения
export GITEA_API_URL="https://git.softuniq.eu/api/v1"
export GITEA_TOKEN=аш_токен_здесь"
```
### Вариант 2: Получение токена из логина/пароля
```bash
# Запустите скрипт для создания токена
./scripts/create-gitea-token.sh your_username your_password
# Скрипт выведет:
# export GITEA_TOKEN=abc123...
```
### Вариант 3: Программно через API
```typescript
import { GiteaClient } from './src/kilocode/agent-manager/gitea-client.js'
// Создать токен из логина/пароля
const { token } = await GiteaClient.createToken(
'your_username',
'your_password',
'Pipeline Token',
['all'] // или ['read:issue', 'write:issue', 'read:repository', 'write:repository']
)
// Использовать токен
const client = new GiteaClient({ token })
```
## Использование
### Создание Issues с Milestone
```typescript
import { GiteaClient } from './src/kilocode/agent-manager/gitea-client.js'
const client = new GiteaClient({
apiUrl: 'https://git.softuniq.eu/api/v1',
token: process.env.GITEA_TOKEN
})
client.setRepository('UniqueSoft', 'APAW')
// Создать milestone
const milestone = await client.createMilestone({
title: 'Sprint 1',
description: 'First sprint',
due_on: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString()
})
// Создать issue с milestone
const issue = await client.createIssue({
title: 'Implement authentication',
body: `## Чеклист
- [ ] Дизайн API
- [ ] Реализация
- [ ] Тесты`,
labels: ['status: new'],
milestone: milestone.id
})
// Добавить комментарий
await client.createComment(issue.number, {
body: `## ✅ Прогресс
### Выполнено
- ✅ API спроектирован
- ✅ Начата реализация
### В процессе
- 🔄 Написание тестов`
})
// Изменить статус
await client.setStatus(issue.number, 'implementing')
```
### Логирование производительности агентов
```typescript
import { logAgentPerformance, logPipelineStep } from './src/kilocode/agent-manager/gitea-client.js'
// Логировать шаг пайплайна
await logPipelineStep(client, issueNumber, '@lead-developer', 'started', 'Implementing authentication')
// Логировать результат работы агента
await logAgentPerformance(client, issueNumber, 'lead-developer', 8, 'Clean implementation, good test coverage')
```
### Работа с Labels
```typescript
// Получить все labels репозитория
const labels = await client.getRepoLabels()
// Добавить labels к issue
await client.addLabels(issueNumber, [1, 2, 3]) // по ID
await client.addLabels(issueNumber, ['bug', 'priority:high']) // по имени
// Заменить все labels
await client.replaceLabels(issueNumber, [1, 2])
// Удалить label
await client.removeLabel(issueNumber, 1)
// Установить статус (удаляет старые status: labels)
await client.setStatus(issueNumber, 'implementing')
```
## API Scopes
Gitea использует гранулярные scopes вместо старых `repo`, `issue`:
| Scope | Описание |
|-------|----------|
| `all` | Полный доступ |
| `read:issue` | Чтение issues |
| `write:issue` | Создание/изменение issues |
| `read:repository` | Чтение репозитория |
| `write:repository` | Изменение репозитория |
| `read:milestone` | Чтение milestones |
| `write:milestone` | Создание/изменение milestones |
## Скрипты тестирования
### Создание токена
```bash
./scripts/create-gitea-token.sh username password
```
### Полный тест (токен + milestone + issues + comments)
```bash
./scripts/full-gitea-test.sh username password
```
### Тест с существующим токеном
```bash
export GITEA_TOKEN=your_token
./scripts/test-gitea.sh
```
## Структура ответов API
### Milestone
```json
{
"id": 42,
"title": "Pipeline Integration Test",
"description": "...",
"state": "open",
"open_issues": 3,
"closed_issues": 0,
"created_at": "2026-04-04T00:27:11Z",
"due_on": "2026-04-11T00:27:03Z"
}
```
### Issue
```json
{
"number": 1,
"title": "Setup Gitea Client",
"body": "## Чеклист\n- [x] Задача 1\n- [ ] Задача 2",
"state": "open",
"labels": [{ "id": 1, "name": "status: new", "color": "0052cc" }],
"milestone": { "id": 42, "title": "..." },
"comments": 3
}
```
### Label
```json
{
"id": 1,
"name": "status: new",
"color": "0052cc",
"description": "New issue",
"exclusive": false,
"is_archived": false
}
```
## Интеграция с агентами
Агенты могут использовать GiteaClient для:
1. **Создание tasks**: `client.createIssue()`
2. **Обновление статуса**: `client.setStatus()`
3. **Логирование прогресса**: `client.createComment()`
4. **Работа с milestone**: `client.createMilestone()`
5. **Парсинг чеклистов**: Извлечение `- [x]` из issue body
### Pipeline Integration
```typescript
// В .kilo/commands/pipeline.md
import { GiteaClient, PipelineRunner } from './src/kilocode/index.js'
const runner = await createPipelineRunner({
giteaToken: process.env.GITEA_TOKEN
})
// Запустить пайплайн для issue
await runner.run({ issueNumber: 42 })
// Оценить производительность
await runner.logEvaluation(42, [
{ agent: 'lead-developer', score: 8, notes: 'Clean code' },
{ agent: 'code-skeptic', score: 7, notes: 'Found 2 issues' }
], 2, 1.5)
```

View File

@@ -0,0 +1,425 @@
# Multi-Agent System Improvement Proposal
## Executive Summary
Based on research from Anthropic's "Building Effective Agents" and Kilo.ai documentation, this proposal outlines improvements to the APAW multi-agent architecture for better development outcomes.
**Current State:** 22 agents, 18 commands, 12 skills
**Issues:** Mode confusion, serial execution, overlapping capabilities
**Goal:** Optimize for efficiency, maintainability, and quality
---
## Analysis Findings
### 1. Agent Inventory
| Agent | Mode | Role | Issues |
|-------|------|------|--------|
| orchestrator | all | Dispatcher | ✅ Correct |
| capability-analyst | subagent | Gap analysis | ✅ Correct |
| history-miner | subagent | Git search | ✅ Correct |
| requirement-refiner | subagent | User stories | ✅ Correct |
| system-analyst | subagent | Architecture | ✅ Correct |
| sdet-engineer | subagent | Test writing | ✅ Correct |
| lead-developer | all | Code writing | ⚠️ Should be subagent |
| frontend-developer | subagent | UI implementation | ✅ Correct |
| backend-developer | subagent | Node/Express/APIs | ✅ Correct |
| workflow-architect | subagent | Create workflows | ✅ Correct |
| code-skeptic | all | Adversarial review | ⚠️ Should be subagent |
| the-fixer | subagent | Bug fixes | ✅ Correct |
| performance-engineer | subagent | Performance review | ✅ Correct |
| security-auditor | subagent | Security audit | ✅ Correct |
| release-manager | all | Git operations | ⚠️ Should be subagent |
| evaluator | all | Scoring | ⚠️ Should be subagent |
| prompt-optimizer | subagent | Optimize prompts | ✅ Correct |
| product-owner | subagent | Issue management | ✅ Correct |
| visual-tester | subagent | Visual regression | ✅ Correct |
| browser-automation | subagent | E2E testing | ✅ Correct |
| markdown-validator | subagent | Markdown validation | ✅ Correct |
| agent-architect | subagent | Create agents | ✅ Correct |
### 2. Issue Summary
| Issue | Severity | Impact |
|-------|----------|--------|
| Mode confusion (all vs subagent) | Medium | Context pollution |
| Serial execution of independent tasks | High | Slower execution |
| No parallelization pattern | High | Latency overhead |
| Overlapping agent roles | Low | Maint overhead |
| Quality gates not enforced | Medium | Quality variance |
---
## Proposed Improvements
### Improvement 1: Normalize Agent Modes
**Problem:** Many agents use `mode: all` but are conceptually subagents that should run in isolated contexts.
**Solution:** Change all specialized agents to `mode: subagent`:
```yaml
# Before
lead-developer:
mode: all
# After
lead-developer:
mode: subagent
```
**Files to Update:**
- `.kilo/agents/lead-developer.md`
- `.kilo/agents/code-skeptic.md`
- `.kilo/agents/release-manager.md`
- `.kilo/agents/evaluator.md`
**Rationale:** Subagent mode provides:
- Isolated context
- Clear input/output contracts
- Better token efficiency
- Prevents context pollution
---
### Improvement 2: Implement Parallelization Pattern
**Problem:** Security and performance reviews run serially but are independent.
**Solution:** Use orchestrator-workers pattern for parallel execution:
```python
async def execute_parallel_reviews():
"""Run security and performance reviews in parallel"""
tasks = [
Task(subagent_type="security-auditor", prompt="..."),
Task(subagent_type="performance-engineer", prompt="...")
]
results = await asyncio.gather(*tasks)
# Collect all issues
all_issues = [
*results[0].security_issues,
*results[1].performance_issues
]
if all_issues:
return Task(subagent_type="the-fixer", issues=all_issues)
```
**New Workflow Step:**
```markdown
## Step 6: Parallel Review
**Agents**: `@security-auditor`, `@performance-engineer` (parallel)
1. Launch both agents simultaneously
2. Wait for both results
3. Aggregate findings
4. If issues found → send to `@the-fixer`
5. If all pass → proceed to release
```
**Rationale:** Anthropic's research shows parallelization reduces latency for independent tasks by ~50%.
---
### Improvement 3: Evaluator-Optimizer Pattern
**Problem:** Code review loop is informal - `code-skeptic``the-fixer` lacks structured iteration.
**Solution:** Formalize as evaluator-optimizer pattern:
```yaml
# New agent definition
code-skeptic:
role: evaluator
outputs:
- verdict: APPROVED | REQUEST_CHANGES
- issues: List[Issue]
- severity: critical | high | medium | low
the-fixer:
role: optimizer
inputs:
- issues: List[Issue]
- code: CodeContext
outputs:
- changes: List[Change]
- resolution_notes: List[str]
# Iteration loop
max_iterations: 3
convergence_criteria: all_issues_resolved OR max_iterations_reached
```
**Implementation:**
```python
def review_loop(issue_number, code_context):
"""Evaluator-Optimizer pattern for code review"""
for iteration in range(max_iterations=3):
# Evaluator reviews
review = task(subagent_type="code-skeptic", code=code_context)
if review.verdict == "APPROVED":
return review
# Optimizer fixes
fix = task(
subagent_type="the-fixer",
issues=review.issues,
code=code_context
)
code_context = apply_fixes(code_context, fix.changes)
iteration += 1
# Escalate if not resolved
post_comment(issue_number, "⚠️ Max iterations reached, manual review needed")
```
**Rationale:** Structured iteration prevents infinite loops and ensures convergence.
---
### Improvement 4: Quality Gate Enforcement
**Problem:** Workflow defines quality gates but agents don't enforce them.
**Solution:** Add gate validation to each agent:
```yaml
# Add to each agent definition
gates:
preconditions:
- files_exist: true
- tests_pass: true
postconditions:
- build_succeeds: true
- coverage_met: true
- no_critical_issues: true
```
**Implementation in Workflow:**
```python
def validate_gate(agent_name, gate_name, artifacts):
"""Validate quality gate before proceeding"""
gates = {
"requirements": ["user_stories_defined", "acceptance_criteria_complete"],
"architecture": ["schema_valid", "endpoints_documented"],
"implementation": ["build_success", "no_type_errors"],
"testing": ["coverage >= 80", "all_tests_pass"],
"review": ["no_critical_issues", "no_security_vulnerabilities"],
"docker": ["build_success", "health_check_pass"]
}
gate_checks = gates[gate_name]
results = run_checks(gate_checks, artifacts)
if not results.all_passed:
raise GateError(f"Gate {gate_name} failed: {results.failed}")
return results
```
---
### Improvement 5: Agent Capability Consolidation
**Problem:** Some agents have overlapping capabilities.
**Solution:** Merge and clarify responsibilities:
| Merge From | Merge To | Rationale |
|------------|----------|-----------|
| browser-automation | sdet-engineer | E2E testing is SDET domain |
| markdown-validator | requirement-refiner | Validation is refiner's job |
**New SDET Engineer Capabilities:**
```yaml
sdet-engineer:
capabilities:
- unit_tests
- integration_tests
- e2e_tests:
tool: playwright
browser: chromium, firefox, webkit
- visual_regression:
tool: pixelmatch
threshold: 0.1
```
**Rationale:** Reduces agent count while maintaining coverage. Browser automation is a capability of SDET, not a separate agent.
---
### Improvement 6: Add Capability Index
**Problem:** No central registry of what each agent can do.
**Solution:** Create capability index for orchestrator:
```yaml
# .kilo/capability-index.yaml
agents:
lead-developer:
capabilities:
- code_writing
- refactoring
- bug_fixing
receives:
- tests
- specifications
produces:
- code
- documentation
code-skeptic:
capabilities:
- code_review
- security_review
- style_review
receives:
- code
produces:
- review_comments
- approval_status
forbidden:
- suggest_implementations
```
**Usage in Orchestrator:**
```python
def route_task(task_type: str) -> str:
"""Route task to appropriate agent based on capability"""
capability_map = {
"code_writing": "lead-developer",
"code_review": "code-skeptic",
"test_writing": "sdet-engineer",
"architecture": "system-analyst",
"security": "security-auditor",
"performance": "performance-engineer"
}
return capability_map.get(task_type, "orchestrator")
```
---
### Improvement 7: Workflow State Machine Enforcement
**Problem:** Workflow state machine is documented but not enforced.
**Solution:** Add explicit state transitions:
```python
# State machine definition
from enum import Enum
from typing import Dict, List
class WorkflowState(Enum):
NEW = "new"
PLANNED = "planned"
RESEARCHING = "researching"
DESIGNED = "designed"
TESTING = "testing"
IMPLEMENTING = "implementing"
REVIEWING = "reviewing"
FIXING = "fixing"
PERF_CHECK = "perf-check"
SECURITY_CHECK = "security-check"
RELEASING = "releasing"
EVALUATED = "evaluated"
COMPLETED = "completed"
# Valid transitions
TRANSITIONS = {
WorkflowState.NEW: [WorkflowState.PLANNED],
WorkflowState.PLANNED: [WorkflowState.RESEARCHING],
WorkflowState.RESEARCHING: [WorkflowState.DESIGNED],
WorkflowState.DESIGNED: [WorkflowState.TESTING],
WorkflowState.TESTING: [WorkflowState.IMPLEMENTING],
WorkflowState.IMPLEMENTING: [WorkflowState.REVIEWING],
WorkflowState.REVIEWING: [WorkflowState.FIXING, WorkflowState.PERF_CHECK],
WorkflowState.FIXING: [WorkflowState.REVIEWING],
WorkflowState.PERF_CHECK: [WorkflowState.SECURITY_CHECK],
WorkflowState.SECURITY_CHECK: [WorkflowState.RELEASING],
WorkflowState.RELEASING: [WorkflowState.EVALUATED],
WorkflowState.EVALUATED: [WorkflowState.COMPLETED],
}
def transition(current: WorkflowState, next_state: WorkflowState) -> bool:
"""Validate state transition"""
valid_next = TRANSITIONS.get(current, [])
if next_state not in valid_next:
raise InvalidTransition(f"Cannot go from {current} to {next_state}")
return True
```
---
## Implementation Priority
| Priority | Improvement | Effort | Impact |
|----------|-------------|--------|--------|
| P0 | Implement Parallelization | Medium | High |
| P0 | Quality Gate Enforcement | Medium | High |
| P1 | Normalize Agent Modes | Low | Medium |
| P1 | Evaluator-Optimizer Pattern | Low | High |
| P2 | Agent Consolidation | Medium | Low |
| P2 | Capability Index | Low | Medium |
| P3 | State Machine Enforcement | Medium | Medium |
---
## Files to Modify
### Must Modify
1. `.kilo/agents/lead-developer.md` - Change mode to `subagent`
2. `.kilo/agents/code-skeptic.md` - Change mode to `subagent`
3. `.kilo/agents/release-manager.md` - Change mode to `subagent`
4. `.kilo/agents/evaluator.md` - Change mode to `subagent`
5. `.kilo/commands/workflow.md` - Add parallel execution
6. `.kilo/agents/orchestrator.md` - Add evaluator-optimizer pattern
### Must Create
1. `.kilo/capability-index.yaml` - Agent capabilities registry
2. `.kilo/skills/quality-gates/SKILL.md` - Gate validation skill
---
## Expected Outcomes
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Workflow duration | ~3 hours | ~2 hours | 33% faster |
| Review iterations | 2-5 | 1-3 | 40% fewer |
| Agent context pollution | High | Low | Isolated |
| Quality gate failures | Manual | Automated | Consistent |
---
## Next Steps
1. **Apply this proposal as issues** - Create Gitea issues for each improvement
2. **Run `/pipeline` for each** - Use existing pipeline to implement
3. **Measure improvements** - Use evaluator to track effectiveness
4. **Iterate** - Use prompt-optimizer to refine
---
*Generated by @capability-analyst based on Anthropic's "Building Effective Agents" research*

153
archive/README.Docker.md Normal file
View File

@@ -0,0 +1,153 @@
# Docker Testing Environment
Quick guide for running browser automation tests in Docker.
## Quick Start
```bash
# Build the image
docker-compose build playwright-mcp
# Start MCP server
docker-compose up -d playwright-mcp
# Check logs
docker-compose logs -f playwright-mcp
```
## Available Modes
### 1. Headless MCP Server (Default)
Best for CI/CD and automated testing:
```bash
# Start server
docker-compose up playwright-mcp
# Connect from Kilo Code
# Configure: "url": "http://localhost:8931/mcp"
```
### 2. Headed Mode (Visual Debugging)
Best for development and debugging:
```bash
# Start with display
docker-compose --profile debug up playwright-headed
# Requires X11 forwarding or VNC
```
### 3. Test Runner Mode
Best for running E2E tests:
```bash
# Run all tests
docker-compose --profile test up test-runner
# Run specific test
docker-compose run --rm playwright-test npx playwright test e2e/homepage.spec.ts
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `PLAYWRIGHT_MCP_BROWSER` | chromium | Browser to use (chromium, firefox, webkit) |
| `PLAYWRIGHT_MCP_HEADLESS` | true | Run without visible window |
| `PLAYWRIGHT_MCP_PORT` | 8931 | Port for MCP server |
| `PLAYWRIGHT_MCP_HOST` | 0.0.0.0 | Host to bind |
| `PLAYWRIGHT_MCP_NO_SANDBOX` | true | Disable sandbox for Docker |
## Useful Commands
```bash
# Pull official image
docker pull mcr.microsoft.com/playwright:v1.58.2-noble
# Run interactive shell
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.58.2-noble /bin/bash
# Run MCP server directly
docker run -d -i --rm --init --ipc=host \
-p 8931:8931 \
--name playwright-mcp \
mcr.microsoft.com/playwright/mcp \
cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0
# Check MCP server health
curl http://localhost:8931/health
# View browser versions
docker run --rm mcr.microsoft.com/playwright:v1.58.2-noble npx playwright --version
```
## CI/CD Integration
```yaml
# .github/workflows/e2e-tests.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.2-noble
options: --ipc=host
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run E2E tests
run: npx playwright test
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshots
path: .test/screenshots/
```
## Troubleshooting
### Chromium Failed to Launch
```bash
# Add --no-sandbox and --disable-dev-shm-usage
docker run --rm --cap-add=SYS_ADMIN mcr.microsoft.com/playwright:v1.58.2-noble
```
### Out of Memory
```bash
# Increase shared memory
docker run --shm-size=2g ...
```
### Slow Performance
```bash
# Use headless mode
# --headless flag is default in Dockerfile
```
## Performance Metrics
| Setup | Startup Time | Memory | Recommended For |
|-------|--------------|--------|------------------|
| Local headless | 1-2s | 150-250MB | Development |
| Local headed | 2-4s | 250-400MB | Debugging |
| Docker headless | 2-5s | 300-500MB | CI/CD |
| Docker headed | 3-6s | 400-600MB | Visual debugging |
## Related Files
- `Dockerfile.playwright` - Custom image with MCP
- `docker-compose.yml` - Service definitions
- `.kilo/skills/playwright/SKILL.md` - Playwright usage guide
- `.kilo/agents/browser-automation.md` - Browser agent

View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Cleanup script to remove duplicate files in packages/opencode
# The agent-manager code is now integrated into src/kilocode/
# Run with: sudo ./cleanup-packages.sh
echo "Removing duplicate files from packages/opencode..."
# Remove the old location (files are now in src/kilocode/agent-manager/)
rm -rf /opt/Projects/APAW/packages/opencode/src/kilocode/
# Remove empty directories
rmdir /opt/Projects/APAW/packages/opencode/src/ 2>/dev/null || true
rmdir /opt/Projects/APAW/packages/opencode/ 2>/dev/null || true
rmdir /opt/Projects/APAW/packages/ 2>/dev/null || true
echo "Cleanup complete!"
echo ""
echo "Agent manager is now located at:"
echo " src/kilocode/agent-manager/ - Core modules"
echo " src/kilocode/index.ts - Entry point"
echo ""
echo "Usage:"
echo " import { PipelineRunner, GiteaClient } from './src/kilocode/index.js'"

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Fix permissions for root-owned files
# Run with: sudo ./fix-permissions.sh
echo "Fixing permissions..."
# Fix ownership for all project directories
chown -R swp:swp /opt/Projects/APAW/packages/ 2>/dev/null || true
chown -R swp:swp /opt/Projects/APAW/src/ 2>/dev/null || true
chown -R swp:swp /opt/Projects/APAW/.kilo/ 2>/dev/null || true
chown swp:swp /opt/Projects/APAW/*.sh 2>/dev/null || true
chown swp:swp /opt/Projects/APAW/package.json 2>/dev/null || true
chown swp:swp /opt/Projects/APAW/tsconfig.json 2>/dev/null || true
# Make scripts executable
chmod +x /opt/Projects/APAW/*.sh 2>/dev/null || true
echo "Permissions fixed!"

49
archive/install-apaw.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# APAW — Claude Code Agent Pipeline Installer
#
# Usage:
# ./install-apaw.sh # install in current directory
# ./install-apaw.sh /path/to/project # install in target project
#
# After install, use /project:pipeline <task> in Claude Code
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="${1:-.}"
if [ ! -d "$TARGET" ]; then
echo "Error: target directory '$TARGET' does not exist"
exit 1
fi
echo "Installing APAW Claude Code pipeline into: $TARGET"
mkdir -p "$TARGET/.claude/commands"
mkdir -p "$TARGET/.claude/rules"
mkdir -p "$TARGET/.claude/logs"
cp "$SCRIPT_DIR/.claude/commands/"*.md "$TARGET/.claude/commands/"
cp "$SCRIPT_DIR/.claude/rules/global.md" "$TARGET/.claude/rules/"
if [ ! -f "$TARGET/.claude/logs/efficiency_score.json" ]; then
echo '[]' > "$TARGET/.claude/logs/efficiency_score.json"
fi
echo ""
echo "Done. 14 agent commands installed."
echo ""
echo "Quick start in Claude Code:"
echo " /project:pipeline <describe your task>"
echo ""
echo "Or step by step:"
echo " /project:refine <vague idea> — clarify requirements"
echo " /project:mine <topic> — check git history for duplicates"
echo " /project:analyze <feature> — design system (Opus)"
echo " /project:tests <feature> — write failing tests (TDD red)"
echo " /project:implement <feature> — write code (TDD green, Opus)"
echo " /project:skeptic <feature> — adversarial code review"
echo " /project:perf <feature> — performance check"
echo " /project:security <feature> — OWASP audit (Opus)"
echo " /project:release <version> — tag and publish"
echo " /project:evaluate <task> — score agents (Haiku)"

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Run Playwright MCP tests directly without docker-compose
echo "=== Starting Playwright MCP Server ==="
# Create directories
mkdir -p .test/screenshots/{baseline,current,diff} .test/reports
# Build and run container
echo "Building Docker image..."
docker build -t playwright-mcp -f Dockerfile.playwright . 2>&1 | tail -5
echo ""
echo "Running Playwright MCP (headed mode - browser will be visible)..."
echo ""
# Run with DISPLAY for headed mode
docker run --rm -it \
--ipc=host \
--shm-size=2g \
-v $(pwd):/app \
-e DISPLAY=$DISPLAY \
-p 8931:8931 \
playwright-mcp
echo ""
echo "Playwright MCP stopped."

View File

@@ -0,0 +1,66 @@
#!/bin/bash
# Create Gitea API token using Basic Auth
# Usage: ./scripts/create-gitea-token.sh <username> <password>
API_URL="https://git.softuniq.eu/api/v1"
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <username> <password>"
echo ""
echo "Example: $0 NW mypassword"
echo ""
echo "This will create a new API token for the user."
exit 1
fi
USERNAME="$1"
PASSWORD="$2"
TOKEN_NAME="Pipeline Test $(date +%Y%m%d%H%M%S)"
echo "=== Gitea Token Creation ==="
echo ""
echo "👤 Username: $USERNAME"
echo "🔑 Token name: $TOKEN_NAME"
echo ""
# Create token using Basic Auth
echo "🔐 Creating token..."
RESPONSE=$(curl -s -X POST \
-u "$USERNAME:$PASSWORD" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$TOKEN_NAME\",\"scopes\":[\"all\"]}" \
"$API_URL/users/$USERNAME/tokens")
# Check for error
if echo "$RESPONSE" | grep -q '"message"'; then
ERROR=$(echo "$RESPONSE" | sed 's/.*"message":"\([^"]*\)".*/\1/')
echo "❌ Failed to create token: $ERROR"
echo ""
echo "Response: $RESPONSE"
exit 1
fi
# Extract token
TOKEN=$(echo "$RESPONSE" | sed 's/.*"sha1":"\([^"]*\)".*/\1/' | head -1)
TOKEN_ID=$(echo "$RESPONSE" | sed 's/.*"id":\([0-9]*\).*/\1/' | head -1)
if [ -n "$TOKEN" ] && [ ${#TOKEN} -gt 10 ]; then
echo "✅ Token created successfully!"
echo ""
echo "📋 Token details:"
echo " ID: $TOKEN_ID"
echo " Name: $TOKEN_NAME"
echo " Token: $TOKEN"
echo ""
echo "💡 Save this token for future use:"
echo ""
echo " export GITEA_TOKEN=$TOKEN"
echo ""
echo " # Add to ~/.bashrc for persistence:"
echo " echo 'export GITEA_TOKEN=$TOKEN' >> ~/.bashrc"
echo ""
else
echo "❌ Failed to extract token from response"
echo "Response: $RESPONSE"
exit 1
fi

View File

@@ -0,0 +1,221 @@
#!/bin/bash
# Full Gitea API test: create token, create milestone, create issues with comments
# Usage: ./scripts/full-gitea-test.sh <username> <password>
echo "=== Gitea API Full Test ==="
echo ""
# Check arguments
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <username> <password>"
echo ""
echo "Example: $0 NW mypassword"
exit 1
fi
USERNAME="$1"
PASSWORD="$2"
API_URL="https://git.softuniq.eu/api/v1"
OWNER="UniqueSoft"
REPO="APAW"
echo "👤 Username: $USERNAME"
echo "📦 Repository: $OWNER/$REPO"
echo ""
# Step 1: Create token
echo "🔑 Step 1: Creating API token..."
TOKEN_NAME="Pipeline Test $(date +%Y%m%d%H%M%S)"
TOKEN_RESPONSE=$(curl -s -X POST \
-u "$USERNAME:$PASSWORD" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$TOKEN_NAME\",\"scopes\":[\"all\"]}" \
"$API_URL/users/$USERNAME/tokens")
# Check for error
if echo "$TOKEN_RESPONSE" | grep -q '"message"'; then
ERROR=$(echo "$TOKEN_RESPONSE" | sed 's/.*"message":"\([^"]*\)".*/\1/')
echo "❌ Failed to create token: $ERROR"
echo "Response: $TOKEN_RESPONSE"
exit 1
fi
GITEA_TOKEN=$(echo "$TOKEN_RESPONSE" | sed 's/.*"sha1":"\([^"]*\)".*/\1/' | head -1)
if [ ${#GITEA_TOKEN} -lt 10 ]; then
echo "❌ Failed to extract token"
echo "Response: $TOKEN_RESPONSE"
exit 1
fi
echo "✅ Token created: ${GITEA_TOKEN:0:8}..."
echo ""
# Step 2: Verify authentication
echo "🔐 Step 2: Verifying authentication..."
WHOAMI=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API_URL/user")
LOGIN=$(echo "$WHOAMI" | sed 's/.*"login":"\([^"]*\)".*/\1/')
if [ -n "$LOGIN" ]; then
echo "✅ Authenticated as: $LOGIN"
else
echo "❌ Authentication failed"
echo "Response: $WHOAMI"
exit 1
fi
echo ""
# Step 3: Create milestone
echo "🎯 Step 3: Creating milestone..."
DUE_DATE=$(date -d "+7 days" -Iseconds 2>/dev/null || date -v+7d -Iseconds 2>/dev/null || date -Iseconds)
MILESTONE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Pipeline Integration Test\",
\"description\": \"Testing agent-manager integration with Gitea API. Created by automated test script.\",
\"due_on\": \"$DUE_DATE\"
}" \
"$API_URL/repos/$OWNER/$REPO/milestones")
MILESTONE_ID=$(echo "$MILESTONE_RESPONSE" | sed 's/.*"id":\([0-9]*\).*/\1/' | head -1)
if [ -n "$MILESTONE_ID" ] && [ "$MILESTONE_ID" -eq "$MILESTONE_ID" ] 2>/dev/null; then
echo "✅ Milestone created: ID=$MILESTONE_ID"
else
echo "❌ Failed to create milestone"
echo "Response: $MILESTONE_RESPONSE"
exit 1
fi
echo ""
# Step 4: Create issues
echo "📝 Step 4: Creating issues..."
create_issue() {
local TITLE="$1"
local BODY="$2"
# Escape quotes in body
local ESCAPED_BODY=$(echo "$BODY" | sed 's/"/\\"/g' | tr '\n' ' ')
local RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"$TITLE\",
\"body\": \"$ESCAPED_BODY\",
\"milestone\": $MILESTONE_ID
}" \
"$API_URL/repos/$OWNER/$REPO/issues")
local NUM=$(echo "$RESPONSE" | sed 's/.*"number":\([0-9]*\).*/\1/' | head -1)
echo "$NUM"
}
# Issue 1
ISSUE1_BODY='## Описание
Настроить Gitea API клиент для работы с репозиторием.
## Чеклист
- [x] Создать GiteaClient класс
- [x] Добавить методы для Issues API
- [x] Добавить методы для Labels API
- [x] Добавить методы для Milestones API
- [ ] Написать документацию
## Примечания
Базовая функциональность готова, нужно дописать JSDoc комментарии.'
ISSUE1_NUM=$(create_issue "Setup Gitea Client (Test)" "$ISSUE1_BODY")
if [ -n "$ISSUE1_NUM" ] && [ "$ISSUE1_NUM" -eq "$ISSUE1_NUM" ] 2>/dev/null; then
echo "✅ Issue #$ISSUE1_NUM: Setup Gitea Client"
fi
# Issue 2
ISSUE2_BODY='## Описание
Реализовать оркестратор пайплайна для управления workflow.
## Чеклист
- [x] Создать класс PipelineRunner
- [x] Добавить маршрутизацию по статусам
- [x] Интегрировать логирование в Gitea
- [x] Добавить подсчёт эффективности
- [ ] Добавить обработку ошибок
- [ ] Добавить retry механизм
## Примечания
Основная логика готова, нужно улучшить обработку edge cases.'
ISSUE2_NUM=$(create_issue "Implement Pipeline Runner (Test)" "$ISSUE2_BODY")
if [ -n "$ISSUE2_NUM" ] && [ "$ISSUE2_NUM" -eq "$ISSUE2_NUM" ] 2>/dev/null; then
echo "✅ Issue #$ISSUE2_NUM: Implement Pipeline Runner"
fi
# Issue 3
ISSUE3_BODY='## Описание
Протестировать интеграцию с Gitea API.
## Чеклист
- [x] Unit тесты для GiteaClient
- [x] Integration тесты для PipelineRunner
- [ ] E2E тесты с реальным API
- [ ] Performance тесты
## Примечания
Требуется настройка CI/CD для автоматического запуска тестов.'
ISSUE3_NUM=$(create_issue "Test Integration (Test)" "$ISSUE3_BODY")
if [ -n "$ISSUE3_NUM" ] && [ "$ISSUE3_NUM" -eq "$ISSUE3_NUM" ] 2>/dev/null; then
echo "✅ Issue #$ISSUE3_NUM: Test Integration"
fi
echo ""
# Step 5: Add comments
echo "💬 Step 5: Adding comments..."
if [ -n "$ISSUE1_NUM" ] && [ "$ISSUE1_NUM" -eq "$ISSUE1_NUM" ] 2>/dev/null; then
# Progress comment
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "## ✅ Прогресс\n\nКод готов и протестирован.\n\n### Выполнено\n- ✅ Базовая структура классов\n- ✅ Интеграция с Gitea API\n- ✅ Обработка ошибок\n\n### В процессе\n- 🔄 Документация\n- 🔄 Тесты"}' \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE1_NUM/comments" > /dev/null
# Technical details comment
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "## 📋 Технические детали\n\n### Используемые API endpoints\n\n```\nPOST /repos/{owner}/{repo}/milestones - Создать milestone\nPOST /repos/{owner}/{repo}/issues - Создать issue\nPOST /repos/{owner}/{repo}/issues/{n}/comments - Добавить комментарий\nGET /repos/{owner}/{repo}/labels - Получить labels\n```\n\n### Структура данных\n\n```typescript\ninterface Milestone {\n id: number\n title: string\n state: \"open\" | \"closed\"\n open_issues: number\n closed_issues: number\n}\n```"}' \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE1_NUM/comments" > /dev/null
# Results comment
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "## ✅ Результаты тестирования\n\n| Компонент | Статус |\n|-----------|--------|\n| GiteaClient | ✅ Работает |\n| PipelineRunner | ✅ Работает |\n| Label API | ✅ Работает |\n| Milestone API | ✅ Работает |\n| Comment API | ✅ Работает |\n\n### Метрики\n\n- **Время создания milestone**: ~150ms\n- **Время создания issue**: ~120ms \n- **Время добавления comment**: ~80ms\n\n### Выводы\n\nИнтеграция с Gitea API 1.21+ работает корректно. Все основные функции протестированы.\n\n✅ Тест успешно завершён!"}' \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE1_NUM/comments" > /dev/null
echo "✅ Added 3 comments to issue #$ISSUE1_NUM"
fi
echo ""
# Step 6: Summary
echo "=========================================="
echo "📊 Test Results Summary"
echo "=========================================="
echo "✅ Token created (saved in GITEA_TOKEN)"
echo "✅ Milestone: ID=$MILESTONE_ID"
echo ""
echo "✅ Issues created:"
if [ -n "$ISSUE1_NUM" ]; then echo " - #$ISSUE1_NUM: Setup Gitea Client"; fi
if [ -n "$ISSUE2_NUM" ]; then echo " - #$ISSUE2_NUM: Implement Pipeline Runner"; fi
if [ -n "$ISSUE3_NUM" ]; then echo " - #$ISSUE3_NUM: Test Integration"; fi
echo ""
echo "💬 Comments: 3 added to first issue"
echo ""
echo "🔗 View milestone:"
echo " https://git.softuniq.eu/$OWNER/$REPO/milestone/$MILESTONE_ID"
echo ""
echo "🔑 Token for future use:"
echo " export GITEA_TOKEN=$GITEA_TOKEN"
echo ""

View File

@@ -0,0 +1,148 @@
#!/bin/bash
# Initialize standard scoped labels for Gitea
# Usage: ./scripts/init-scoped-labels.sh
echo "=== Gitea Scoped Labels Initialization ==="
echo ""
# Check for token
if [ -z "$GITEA_TOKEN" ]; then
echo "❌ GITEA_TOKEN not set!"
echo ""
echo "Run: ./scripts/create-gitea-token.sh <username> <password>"
echo "Or: export GITEA_TOKEN=your_token"
exit 1
fi
API_URL="https://git.softuniq.eu/api/v1"
# Detect repository
REMOTE=$(git remote get-url origin 2>/dev/null | head -1)
OWNER=$(echo "$REMOTE" | sed 's/.*[:/]\([^/]*\)\/.*/\1/')
REPO=$(echo "$REMOTE" | sed 's/.*[:/][^/]*\/\([^/.]*\).*/\1/')
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
echo "❌ Could not detect repository"
echo "Set OWNER and REPO manually:"
echo " export OWNER=UniqueSoft"
echo " export REPO=APAW"
exit 1
fi
echo "📦 Repository: $OWNER/$REPO"
echo ""
# Function to create label
create_label() {
local NAME="$1"
local COLOR="$2"
local DESC="$3"
local EXCLUSIVE="$4"
# Check if label exists
EXISTING=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$OWNER/$REPO/labels?name=$NAME" | grep -o "\"name\":\"$NAME\"")
if [ -n "$EXISTING" ]; then
echo " ⏭️ $NAME (already exists)"
return
fi
# Create label
RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$NAME\",\"color\":\"$COLOR\",\"description\":\"$DESC\",\"exclusive\":$EXCLUSIVE}" \
"$API_URL/repos/$OWNER/$REPO/labels")
if echo "$RESPONSE" | grep -q '"id"'; then
echo "$NAME"
else
echo "$NAME - $(echo $RESPONSE | grep -o '"message":"[^"]*"')"
fi
}
# Status labels
echo "🏷️ Creating status labels..."
create_label "status::new" "0052cc" "New issue, not started" true
create_label "status::planned" "1d76db" "Planned for sprint" true
create_label "status::in-progress" "fbca04" "Work in progress" true
create_label "status::review" "d93f0b" "Under review" true
create_label "status::testing" "d4c5f9" "In testing" true
create_label "status::done" "0e8a16" "Completed" true
create_label "status::blocked" "b60205" "Blocked" true
create_label "status::cancelled" "5319e7" "Cancelled" true
echo ""
# Priority labels
echo "🏷️ Creating priority labels..."
create_label "priority::critical" "b60205" "Critical priority" true
create_label "priority::high" "d93f0b" "High priority" true
create_label "priority::medium" "fbca04" "Medium priority" true
create_label "priority::low" "0e8a16" "Low priority" true
echo ""
# Type labels
echo "🏷️ Creating type labels..."
create_label "type::bug" "d73a4a" "Something is broken" true
create_label "type::feature" "0e8a16" "New feature" true
create_label "type::enhancement" "a2eeef" "Improvement" true
create_label "type::documentation" "0075ca" "Documentation" true
create_label "type::refactor" "7057ff" "Code refactoring" true
create_label "type::test" "d4c5f9" "Testing" true
create_label "type::chore" "cfd3d7" "Maintenance task" true
echo ""
# Size labels
echo "🏷️ Creating size labels..."
create_label "size::xs" "cfd3d7" "Extra small (<1 hour)" true
create_label "size::s" "c2e0c6" "Small (1-2 hours)" true
create_label "size::m" "fbca04" "Medium (2-4 hours)" true
create_label "size::l" "d93f0b" "Large (4-8 hours)" true
create_label "size::xl" "b60205" "Extra large (>8 hours)" true
echo ""
# Component labels (non-exclusive)
echo "🏷️ Creating component labels..."
create_label "component::api" "1d76db" "API related" false
create_label "component::ui" "bfdadc" "UI related" false
create_label "component::database" "c5def5" "Database related" false
create_label "component::auth" "d4c5f9" "Authentication related" false
create_label "component::pipeline" "7057ff" "Pipeline related" false
create_label "component::agent" "5319e7" "Agent related" false
echo ""
# Pipeline status labels (alternative status format)
echo "🏷️ Creating pipeline status labels..."
create_label "pipeline::new" "0052cc" "New pipeline task" true
create_label "pipeline::researching" "1d76db" "Research phase" true
create_label "pipeline::designed" "bfd4f2" "Design complete" true
create_label "pipeline::testing" "fbca04" "Tests in progress" true
create_label "pipeline::implementing" "d93f0b" "Implementation" true
create_label "pipeline::reviewing" "d4c5f9" "Code review" true
create_label "pipeline::fixing" "e99695" "Fixing issues" true
create_label "pipeline::releasing" "c2e0c6" "Release preparation" true
create_label "pipeline::evaluated" "fef2c0" "Evaluation complete" true
create_label "pipeline::completed" "0e8a16" "Pipeline complete" true
echo ""
echo "=========================================="
echo "✅ Scoped labels initialized!"
echo ""
echo "📋 Usage examples:"
echo ""
echo "Set status (exclusive - removes other status:: labels):"
echo " client.addLabels(issueNumber, ['status::in-progress', 'priority::high'])"
echo ""
echo "Set priority:"
echo " client.addLabels(issueNumber, ['priority::critical'])"
echo ""
echo "Set type and size:"
echo " client.addLabels(issueNumber, ['type::feature', 'size::m'])"
echo ""
echo "Add component (multiple allowed):"
echo " client.addLabels(issueNumber, ['component::api', 'component::auth'])"
echo ""
echo "🔗 View all labels:"
echo " https://git.softuniq.eu/$OWNER/$REPO/labels"
echo ""

View File

@@ -0,0 +1,203 @@
#!/bin/bash
# Review Watcher Script
# Watches for completion comments and triggers automatic review
# Usage: ./scripts/review-watcher.sh [issue_number]
echo "=== Review Watcher ==="
echo ""
# Check for token
if [ -z "$GITEA_TOKEN" ]; then
echo "❌ GITEA_TOKEN not set!"
echo "Run: export GITEA_TOKEN=your_token"
exit 1
fi
API_URL="https://git.softuniq.eu/api/v1"
# Detect repository
REMOTE=$(git remote get-url origin 2>/dev/null | head -1)
OWNER=$(echo "$REMOTE" | sed 's/.*[:/]\([^/]*\)\/.*/\1/')
REPO=$(echo "$REMOTE" | sed 's/.*[:/][^/]*\/\([^/.]*\).*/\1/')
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
echo "❌ Could not detect repository"
exit 1
fi
echo "📦 Repository: $OWNER/$REPO"
echo ""
# Function to check issue for completion markers
check_issue() {
local ISSUE_NUM=$1
echo "🔎 Checking issue #$ISSUE_NUM..."
# Get issue details
ISSUE=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE_NUM")
ISSUE_BODY=$(echo "$ISSUE" | grep -o '"body":"[^"]*"' | sed 's/"body":"//; s/"$//')
ISSUE_TITLE=$(echo "$ISSUE" | grep -o '"title":"[^"]*"' | sed 's/"title":"//; s/"$//')
# Get comments
COMMENTS=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE_NUM/comments")
# Check for completion markers
COMPLETION_MARKERS="done completed ready выполнено готово сделано ✓ ✅"
FOUND_MARKER=""
for marker in $COMPLETION_MARKERS; do
if echo "$ISSUE_BODY $COMMENTS" | grep -qi "$marker"; then
FOUND_MARKER="$marker"
break
fi
done
# Check for checklist completion
CHECKLIST_COMPLETE=$(echo "$ISSUE_BODY" | grep -c '\- \[x\]' || echo 0)
CHECKLIST_TOTAL=$(echo "$ISSUE_BODY" | grep -c '\- \[' || echo 0)
if [ "$CHECKLIST_TOTAL" -gt 0 ] && [ "$CHECKLIST_COMPLETE" -eq "$CHECKLIST_TOTAL" ]; then
echo "✅ All checklist items complete ($CHECKLIST_COMPLETE/$CHECKLIST_TOTAL)"
FOUND_MARKER="checklist-complete"
fi
if [ -n "$FOUND_MARKER" ]; then
echo "✅ Completion marker found: $FOUND_MARKER"
echo ""
echo "📋 Issue: $ISSUE_TITLE"
echo " Body length: ${#ISSUE_BODY} chars"
echo " Comments: $(echo "$COMMENTS" | grep -c '"id"' || echo 0)"
echo ""
return 0
else
echo "⏳ No completion markers found"
return 1
fi
}
# Function to create fix task
create_fix_task() {
local PARENT_NUM=$1
local FIX_TITLE=$2
local FIX_BODY=$3
local PRIORITY=$4
echo "Creating fix task: $FIX_TITLE"
RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Fix: $FIX_TITLE (from #$PARENT_NUM)\",
\"body\": \"## Parent Issue\n#$PARENT_NUM\n\n## Problem\n$FIX_BODY\n\n## Priority\n$PRIORITY\",
\"labels\": [\"type::bug\", \"$PRIORITY\", \"status::new\"]
}" \
"$API_URL/repos/$OWNER/$REPO/issues")
FIX_NUM=$(echo "$RESPONSE" | grep -o '"number":[0-9]*' | head -1 | cut -d: -f2)
if [ -n "$FIX_NUM" ]; then
echo "✅ Created fix task #$FIX_NUM"
# Comment on parent
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"body\":\"Created fix task: #$FIX_NUM\n\n**Priority:** $PRIORITY\n**Action:** Fix required before merge\"}" \
"$API_URL/repos/$OWNER/$REPO/issues/$PARENT_NUM/comments" > /dev/null
return $FIX_NUM
else
echo "❌ Failed to create fix task"
return 0
fi
}
# Function to run validation
run_validation() {
local ISSUE_NUM=$1
echo ""
echo "🔍 Running validation..."
echo ""
# Get issue files (from body or comments)
FILES=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE_NUM" | \
grep -oE 'src/[a-zA-Z0-9_/.-]+\.(ts|js|tsx|jsx|go|py)' | \
head -10)
if [ -n "$FILES" ]; then
echo "Files to validate:"
echo "$FILES" | while read f; do echo " - $f"; done
echo ""
fi
# Placeholder for actual validation
# In real implementation, this would call validation agents
echo "Running checks:"
echo " ✅ Markdown validation"
echo " ✅ Syntax check"
echo " ⚠️ Security scan (2 issues found)"
echo " ⚠️ Performance check (1 issue found)"
echo ""
# Simulated issues for demo
echo "Creating fix tasks for found issues..."
create_fix_task "$ISSUE_NUM" \
"Add rate limiting to auth endpoints" \
"auth.ts lacks rate limiting, vulnerable to brute force attacks" \
"priority::high"
create_fix_task "$ISSUE_NUM" \
"Remove debug console.log statements" \
"Production code contains console.log in jwt.ts line 12" \
"priority::medium"
}
# Main logic
if [ -n "$1" ]; then
# Check specific issue
check_issue "$1"
if [ $? -eq 0 ]; then
run_validation "$1"
fi
else
# Check all open issues with status::review label
echo "Searching for issues ready for review..."
echo ""
ISSUES=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$OWNER/$REPO/issues?state=open&labels=status::review" | \
grep -o '"number":[0-9]*' | cut -d: -f2)
if [ -z "$ISSUES" ]; then
echo "No issues found with status::review label"
echo ""
echo "To check a specific issue: $0 <issue_number>"
exit 0
fi
for ISSUE_NUM in $ISSUES; do
echo "────────────────────────────────────"
check_issue "$ISSUE_NUM"
if [ $? -eq 0 ]; then
run_validation "$ISSUE_NUM"
fi
echo ""
done
fi
echo "=========================================="
echo "✅ Review watcher complete"
echo ""
echo "To manually trigger review:"
echo " $0 <issue_number>"
echo ""
echo "To watch continuously (webhook mode):"
echo " while true; do $0; sleep 300; done"
echo ""

View File

@@ -0,0 +1,168 @@
#!/bin/bash
# Autonomous Pipeline Test Runner
# Runs complete system test for all agents, workflows, and skills
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ APAW Autonomous Pipeline - System Test Runner ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# Configuration
GITEA_URL="https://git.softuniq.eu"
API_URL="https://git.softuniq.eu/api/v1"
OWNER="UniqueSoft"
REPO="APAW"
# Check for token
if [ -z "$GITEA_TOKEN" ]; then
echo "❌ GITEA_TOKEN not set!"
echo " Run: export GITEA_TOKEN=your_token"
exit 1
fi
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Test counters
TESTS_PASSED=0
TESTS_FAILED=0
TESTS_TOTAL=0
# Function to run test
run_test() {
local test_name="$1"
local test_command="$2"
TESTS_TOTAL=$((TESTS_TOTAL + 1))
echo -e "${BLUE}▶ Testing: ${test_name}${NC}"
if eval "$test_command" > /dev/null 2>&1; then
TESTS_PASSED=$((TESTS_PASSED + 1))
echo -e " ${GREEN}✅ PASSED${NC}"
return 0
else
TESTS_FAILED=$((TESTS_FAILED + 1))
echo -e " ${RED}❌ FAILED${NC}"
return 1
fi
}
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 1: Agent Testing ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# Test each agent file exists
echo "Testing agent configurations..."
for agent in orchestrator requirement-refiner history-miner system-analyst product-owner lead-developer frontend-developer sdet-engineer code-skeptic the-fixer performance-engineer security-auditor release-manager evaluator prompt-optimizer capability-analyst agent-architect markdown-validator; do
run_test "Agent file: $agent" "[ -f .kilo/agents/${agent}.md ]"
done
echo ""
echo "Testing agent models..."
# Check that no unavailable models are used (anthropic, openai direct)
run_test "No unavailable models" "! grep -r 'model:.*anthropic' .kilo/agents/ 2>/dev/null"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 2: Commands Testing ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
for cmd in pipeline status evaluate plan ask debug code review review-watcher feature hotfix; do
run_test "Command: /$cmd" "[ -f .kilo/commands/${cmd}.md ]"
done
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 3: Skills Testing ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
for skill in gitea scoped-labels fix-workflow; do
run_test "Skill: $skill" "[ -f .kilo/skills/${skill}/SKILL.md ]"
done
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 4: TypeScript Modules Testing ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
for module in index workflow router evaluator git-ops gitea-client pipeline-runner prompt-loader types; do
run_test "Module: ${module}.ts" "[ -f src/kilocode/agent-manager/${module}.ts ]"
done
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 5: Gitea Integration ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# Test Gitea API connectivity
echo "Testing Gitea API..."
run_test "API connectivity" "curl -s '$API_URL/repos/$OWNER/$REPO' -H 'Authorization: token $GITEA_TOKEN' | grep -q '\"name\"'"
# Test scoped labels
echo ""
echo "Testing scoped labels..."
run_test "Status labels" "curl -s '$API_URL/repos/$OWNER/$REPO/labels' -H 'Authorization: token $GITEA_TOKEN' | grep -q 'status::new'"
run_test "Priority labels" "curl -s '$API_URL/repos/$OWNER/$REPO/labels' -H 'Authorization: token $GITEA_TOKEN' | grep -q 'priority::critical'"
run_test "Type labels" "curl -s '$API_URL/repos/$OWNER/$REPO/labels' -H 'Authorization: token $GITEA_TOKEN' | grep -q 'type::feature'"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 6: Pipeline Flow Test ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# Test workflow state machine
echo "Testing workflow transitions..."
run_test "Workflow graph" "[ -f src/kilocode/agent-manager/workflow.ts ]"
run_test "Router" "[ -f src/kilocode/agent-manager/router.ts ]"
run_test "Pipeline runner" "[ -f src/kilocode/agent-manager/pipeline-runner.ts ]"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PHASE 7: Autonomous Components ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
run_test "Capability analyst" "[ -f .kilo/agents/capability-analyst.md ]"
run_test "Agent architect" "[ -f .kilo/agents/agent-architect.md ]"
run_test "Review watcher" "[ -f .kilo/commands/review-watcher.md ]"
run_test "Fix workflow" "[ -f .kilo/skills/fix-workflow/SKILL.md ]"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TEST RESULTS SUMMARY ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Total Tests: ${TESTS_TOTAL}"
echo -e "Passed: ${GREEN}${TESTS_PASSED}${NC}"
echo -e "Failed: ${RED}${TESTS_FAILED}${NC}"
echo ""
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}✅ ALL TESTS PASSED${NC}"
echo ""
echo "System is ready for autonomous operation!"
echo ""
echo "Next steps:"
echo " 1. Run: ./scripts/run-pipeline-test.sh (or /pipeline 5 in KiloCode)"
echo " 2. Test individual agents: @agent-name <task>"
echo " 3. View milestone: https://git.softuniq.eu/${OWNER}/${REPO}/milestone/43"
echo ""
exit 0
else
echo -e "${RED}❌ SOME TESTS FAILED${NC}"
echo ""
echo "Please fix the issues above before running pipeline."
exit 1
fi

View File

@@ -0,0 +1,203 @@
#!/bin/bash
# Test Gitea API Integration
# Run this script after setting GITEA_TOKEN environment variable
echo "=== Gitea API Test Script ==="
echo ""
# Check if GITEA_TOKEN is set
if [ -z "$GITEA_TOKEN" ]; then
echo "❌ GITEA_TOKEN not set!"
echo ""
echo "To get your token:"
echo "1. Run: ./scripts/create-gitea-token.sh <username> <password>"
echo "2. Or create manually at: https://git.softuniq.eu/user/settings/applications"
echo "3. Export it: export GITEA_TOKEN=your_token_here"
echo ""
echo "Note: Use 'all' scope for full access"
echo ""
exit 1
fi
API_URL="https://git.softuniq.eu/api/v1"
OWNER="UniqueSoft"
REPO="APAW"
echo "📦 Testing API: $API_URL"
echo "📁 Repository: $OWNER/$REPO"
echo ""
# 1. Test authentication
echo "🔐 Testing authentication..."
WHOAMI=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API_URL/user")
if echo "$WHOAMI" | grep -q '"login"'; then
LOGIN=$(echo "$WHOAMI" | sed 's/.*"login":"\([^"]*\)".*/\1/')
echo "✅ Authenticated as: $LOGIN"
else
echo "❌ Authentication failed"
echo "Response: $WHOAMI"
exit 1
fi
echo ""
# 2. Create Milestone
echo "🎯 Creating milestone..."
DUE_DATE=$(date -d "+7 days" -Iseconds 2>/dev/null || date -v+7d -Iseconds 2>/dev/null || date -Iseconds)
MILESTONE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Pipeline Integration Test\",
\"description\": \"Testing agent-manager integration with Gitea API\",
\"due_on\": \"$DUE_DATE\"
}" \
"$API_URL/repos/$OWNER/$REPO/milestones")
MILESTONE_ID=$(echo "$MILESTONE_RESPONSE" | sed 's/.*"id":\([0-9]*\).*/\1/' | head -1)
if [ -n "$MILESTONE_ID" ] && [ "$MILESTONE_ID" != "" ]; then
echo "✅ Milestone created: ID=$MILESTONE_ID"
else
echo "❌ Failed to create milestone"
echo "Response: $MILESTONE_RESPONSE"
exit 1
fi
echo ""
# 3. Create Issues with checklists
echo "📝 Creating issues..."
# Issue 1
echo " Creating issue 1..."
ISSUE1_BODY='## Описание
Настроить Gitea API клиент для работы с репозиторием.
## Чеклист
- [x] Создать GiteaClient класс
- [x] Добавить методы для Issues API
- [x] Добавить методы для Labels API
- [x] Добавить методы для Milestones API
- [ ] Написать документацию
## Примечания
Базовая функциональность готова, нужно дописать JSDoc комментарии.'
ISSUE1_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Setup Gitea Client\",
\"body\": $(echo "$ISSUE1_BODY" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | tr -d '\n' | sed 's/\\n$//'),
\"milestone\": $MILESTONE_ID
}" \
"$API_URL/repos/$OWNER/$REPO/issues")
ISSUE1_NUM=$(echo "$ISSUE1_RESPONSE" | sed 's/.*"number":\([0-9]*\).*/\1/' | head -1)
if [ -n "$ISSUE1_NUM" ]; then
echo "✅ Issue #$ISSUE1_NUM: Setup Gitea Client"
else
echo "❌ Failed to create issue 1"
echo "Response: $ISSUE1_RESPONSE"
fi
# Issue 2
echo " Creating issue 2..."
ISSUE2_BODY='## Описание
Реализовать оркестратор пайплайна для управления workflow.
## Чеклист
- [x] Создать класс PipelineRunner
- [x] Добавить маршрутизацию по статусам
- [x] Интегрировать логирование в Gitea
- [x] Добавить подсчёт эффективности
- [ ] Добавить обработку ошибок
- [ ] Добавить retry механизм
## Примечания
Основная логика готова, нужно улучшить обработку edge cases.'
ISSUE2_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Implement Pipeline Runner\",
\"body\": $(echo "$ISSUE2_BODY" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | tr -d '\n' | sed 's/\\n$//'),
\"milestone\": $MILESTONE_ID
}" \
"$API_URL/repos/$OWNER/$REPO/issues")
ISSUE2_NUM=$(echo "$ISSUE2_RESPONSE" | sed 's/.*"number":\([0-9]*\).*/\1/' | head -1)
if [ -n "$ISSUE2_NUM" ]; then
echo "✅ Issue #$ISSUE2_NUM: Implement Pipeline Runner"
fi
# Issue 3
echo " Creating issue 3..."
ISSUE3_BODY='## Описание
Протестировать интеграцию с Gitea API.
## Чеклист
- [x] Unit тесты для GiteaClient
- [x] Integration тесты для PipelineRunner
- [ ] E2E тесты с реальным API
- [ ] Performance тесты
## Примечания
Требуется настройка CI/CD для автоматического запуска тестов.'
ISSUE3_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Test Integration\",
\"body\": $(echo "$ISSUE3_BODY" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | tr -d '\n' | sed 's/\\n$//'),
\"milestone\": $MILESTONE_ID
}" \
"$API_URL/repos/$OWNER/$REPO/issues")
ISSUE3_NUM=$(echo "$ISSUE3_RESPONSE" | sed 's/.*"number":\([0-9]*\).*/\1/' | head -1)
if [ -n "$ISSUE3_NUM" ]; then
echo "✅ Issue #$ISSUE3_NUM: Test Integration"
fi
echo ""
# 4. Add comments
if [ -n "$ISSUE1_NUM" ]; then
echo "💬 Adding comments to issue #$ISSUE1_NUM..."
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "## ✅ Выполнено\n\n- Базовая структура классов\n- Интеграция с Gitea API\n- Обработка ошибок\n\n## 🔄 В процессе\n\n- Документация\n- Тестирование"}' \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE1_NUM/comments" > /dev/null
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "## 📋 Технические детали\n\n### Используемые API endpoints\n\n```\nPOST /repos/{owner}/{repo}/milestones\nPOST /repos/{owner}/{repo}/issues\nPOST /repos/{owner}/{repo}/issues/{n}/comments\nGET /repos/{owner}/{repo}/labels\n```\n\n### Структура данных\n\n```typescript\ninterface Milestone {\n id: number\n title: string\n state: \"open\" | \"closed\"\n}\n```"}' \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE1_NUM/comments" > /dev/null
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "## ✅ Результаты тестирования\n\n| Компонент | Статус |\n|-----------|--------|\n| GiteaClient | ✅ Работает |\n| PipelineRunner | ✅ Работает |\n| Label API | ✅ Работает |\n| Milestone API | ✅ Работает |\n| Comment API | ✅ Работает |\n\n### Метрики\n\n- **Время создания milestone**: ~200ms\n- **Время создания issue**: ~150ms\n- **Время добавления comment**: ~100ms\n\n### Выводы\n\nИнтеграция с Gitea API 1.21+ работает корректно."}' \
"$API_URL/repos/$OWNER/$REPO/issues/$ISSUE1_NUM/comments" > /dev/null
echo "✅ Comments added"
fi
echo ""
# 5. Summary
echo "=========================================="
echo "📊 Test Results"
echo "=========================================="
echo "✅ Milestone created: ID=$MILESTONE_ID"
echo "✅ Issues created: 3"
if [ -n "$ISSUE1_NUM" ]; then echo " - #$ISSUE1_NUM: Setup Gitea Client"; fi
if [ -n "$ISSUE2_NUM" ]; then echo " - #$ISSUE2_NUM: Implement Pipeline Runner"; fi
if [ -n "$ISSUE3_NUM" ]; then echo " - #$ISSUE3_NUM: Test Integration"; fi
echo "✅ Comments added: 3 per issue (to first issue)"
echo ""
echo "🔗 View at:"
echo " https://git.softuniq.eu/$OWNER/$REPO/milestone/$MILESTONE_ID"
echo ""

View File

@@ -0,0 +1,63 @@
# Architect Indexer Dockerfile
# Scans target project codebase and generates .architect/ directory
#
# Usage:
# docker compose -f docker/docker-compose.architect.yml build
# docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer
# docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer --mode incremental
# ── Build stage ────────────────────────────────────────────────────
FROM oven/bun:1-alpine AS builder
WORKDIR /build
# Copy dependency files first (cache layer)
COPY package.json ./
# bun.lock might not exist; handle gracefully
COPY bun.lock* ./
# Install dependencies
RUN bun install 2>/dev/null || npm install 2>/dev/null || true
# Copy source for build
COPY tsconfig.json ./
COPY src/ ./src/
# Build TypeScript
RUN bun run build 2>/dev/null || npx tsc 2>/dev/null || true
# ── Production stage ────────────────────────────────────────────────
FROM node:20-alpine AS production
WORKDIR /app
# Security: non-root user
RUN addgroup -S apaw && adduser -S apaw -G apaw
# Copy built artifacts
COPY --from=builder /build/dist/ ./dist/
COPY --from=builder /build/node_modules/ ./node_modules/
COPY --from=builder /build/package.json ./
# Copy .kilo configs for agent context
COPY .kilo/agents/architect-indexer.md ./agents/architect-indexer.md
COPY .kilo/skills/project-mapping/ ./skills/project-mapping/
COPY .kilo/rules/ ./rules/
# Ensure .architect template directory exists
COPY .architect/ /template/.architect/
# Default project mount point
ENV PROJECT_ROOT=/project
ENV NODE_ENV=production
ENV TZ=UTC
# Healthcheck: verify .architect/ was generated
HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=2 \
CMD test -d /project/.architect || exit 1
USER apaw
# Run the indexer script (compiled from TypeScript)
ENTRYPOINT ["node", "dist/kilocode/scripts/run-architect-indexer.js"]
CMD ["--target", "/project"]

View File

@@ -0,0 +1,33 @@
# Playwright MCP Docker Image for E2E Testing
# Based on official Microsoft Playwright image
FROM mcr.microsoft.com/playwright:v1.58.2-noble
# Set working directory
WORKDIR /app
# Install dependencies
RUN npm install -g @playwright/mcp@latest
# Create directories for tests
RUN mkdir -p .test/screenshots/{baseline,current,diff} .test/reports
# Set environment variables
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV PLAYWRIGHT_MCP_BROWSER=chromium
# HEADLESS=false by default - browser is VISIBLE for observation
# Set PLAYWRIGHT_MCP_HEADLESS=true for CI/CD
ENV PLAYWRIGHT_MCP_HEADLESS=false
# Expose port for MCP server
EXPOSE 8931
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8931/health || exit 1
# Default command - MCP server (HEADED by default - browser visible)
# Browser window will be visible for observation
CMD ["node", "/usr/local/lib/node_modules/@playwright/mcp/cli.js", \
"--browser", "chromium", \
"--no-sandbox", "--port", "8931", "--host", "0.0.0.0"]

View File

@@ -0,0 +1,48 @@
# Architect Indexer Service
# Scans project codebase and generates/updates .architect/ directory
#
# Usage:
# Full index (first run):
# docker compose -f docker/docker-compose.architect.yml run architect-indexer
#
# Incremental update:
# docker compose -f docker/docker-compose.architect.yml run architect-indexer --mode incremental
#
# Index a specific project path:
# docker compose -f docker/docker-compose.architect.yml run architect-indexer --target /project
#
# Re-build image:
# docker compose -f docker/docker-compose.architect.yml build
services:
architect-indexer:
build:
context: ..
dockerfile: docker/Dockerfile.architect-indexer
container_name: apaw-architect-indexer
volumes:
# Mount target project for scanning and .architect/ output
- ..:/project:rw
# Exclude node_modules from mount (use container's own)
- /project/node_modules
# Exclude .kilo internal deps from scan
- /project/.kilo/node_modules
environment:
- PROJECT_ROOT=/project
- NODE_ENV=production
- TZ=UTC
# Gitea integration (optional, for posting indexing comments)
- GITEA_API_URL=${GITEA_API_URL:-https://git.softuniq.eu/api/v1}
- GITEA_TOKEN=${GITEA_TOKEN:-}
- GITEA_ISSUE=${GITEA_ISSUE:-}
working_dir: /project
networks:
- architect-network
restart: "no"
labels:
- "com.apaw.service=architect-indexer"
- "com.apaw.description=Project codebase indexer - generates .architect/ directory"
networks:
architect-network:
driver: bridge

View File

@@ -0,0 +1,129 @@
# Web Testing Infrastructure
# Covers: Visual Regression, Link Checking, Form Testing, Console Errors
#
# Usage:
# Local app testing (bridge network):
# docker compose -f docker/docker-compose.web-testing.yml up visual-tester
#
# External site testing (host network for DNS):
# docker compose --profile external -f docker/docker-compose.web-testing.yml up visual-tester
#
# Override target URL:
# TARGET_URL=https://example.com docker compose --profile external -f docker/docker-compose.web-testing.yml up visual-tester
#
# Gitea integration:
# GITEA_ISSUE=42 docker compose --profile external -f docker/docker-compose.web-testing.yml up visual-tester
services:
# ─── Screenshot Capture: Create Baselines ─────────────────────────
screenshot-baseline:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-screenshot-baseline
working_dir: /app
volumes:
- ../tests:/app/tests
environment:
- TARGET_URL=${TARGET_URL:-http://host.docker.internal:3000}
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
- DNS_RESOLUTION_ORDER=hostname-first
command: >
sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null;
node scripts/capture-screenshots.js baseline"
extra_hosts:
- "host.docker.internal:host-gateway"
shm_size: '2gb'
ipc: host
network_mode: ${NETWORK_MODE:-bridge}
# ─── Screenshot Capture: Create Current ──────────────────────────
screenshot-current:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-screenshot-current
working_dir: /app
volumes:
- ../tests:/app/tests
environment:
- TARGET_URL=${TARGET_URL:-http://host.docker.internal:3000}
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
- DNS_RESOLUTION_ORDER=hostname-first
command: >
sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null;
node scripts/capture-screenshots.js current"
extra_hosts:
- "host.docker.internal:host-gateway"
shm_size: '2gb'
ipc: host
network_mode: ${NETWORK_MODE:-bridge}
# ─── Visual Regression: Compare Screenshots ──────────────────────
visual-compare:
image: node:20-alpine
container_name: apaw-visual-compare
working_dir: /app
volumes:
- ../tests:/app/tests
environment:
- PIXELMATCH_THRESHOLD=0.05
- BASELINE_DIR=/app/tests/visual/baseline
- CURRENT_DIR=/app/tests/visual/current
- DIFF_DIR=/app/tests/visual/diff
- REPORTS_DIR=/app/tests/reports
command: >
sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null;
node scripts/compare-screenshots.js"
# ─── Full Visual Test Pipeline ──────────────────────────────────
# Captures current screenshots and compares against baselines
visual-tester:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-visual-tester
working_dir: /app
volumes:
- ../tests:/app/tests
environment:
- TARGET_URL=${TARGET_URL:-http://host.docker.internal:3000}
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
- PIXELMATCH_THRESHOLD=${PIXELMATCH_THRESHOLD:-0.05}
- PAGES=${PAGES:-/,/admin/login}
- BASELINE_DIR=/app/tests/visual/baseline
- CURRENT_DIR=/app/tests/visual/current
- DIFF_DIR=/app/tests/visual/diff
- REPORTS_DIR=/app/tests/reports
- GITEA_ISSUE=${GITEA_ISSUE:-}
- GITEA_TOKEN=${GITEA_TOKEN:-}
- GITEA_USER=${GITEA_USER:-}
- GITEA_PASSWORD=${GITEA_PASSWORD:-}
- DNS_RESOLUTION_ORDER=hostname-first
command: >
sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null;
node scripts/visual-test-pipeline.js"
extra_hosts:
- "host.docker.internal:host-gateway"
shm_size: '2gb'
ipc: host
network_mode: ${NETWORK_MODE:-bridge}
# ─── Console Error Monitor ──────────────────────────────────────
console-monitor:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-console-monitor
working_dir: /app
volumes:
- ../tests:/app/tests
environment:
- TARGET_URL=${TARGET_URL:-http://host.docker.internal:3000}
- REPORTS_DIR=/app/tests/reports
- GITEA_ISSUE=${GITEA_ISSUE:-}
- GITEA_TOKEN=${GITEA_TOKEN:-}
- GITEA_USER=${GITEA_USER:-}
- GITEA_PASSWORD=${GITEA_PASSWORD:-}
- DNS_RESOLUTION_ORDER=hostname-first
command: >
sh -c "cd /app/tests && npm install --ignore-scripts 2>/dev/null;
node scripts/console-error-monitor-standalone.js"
extra_hosts:
- "host.docker.internal:host-gateway"
shm_size: '2gb'
ipc: host
network_mode: ${NETWORK_MODE:-bridge}

54
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
version: '3.8'
services:
playwright-mcp:
build:
context: .
dockerfile: Dockerfile.playwright
container_name: playwright-mcp
ports:
- "8931:8931"
volumes:
- ./:/app
- /app/node_modules
environment:
- PLAYWRIGHT_MCP_BROWSER=chromium
- PLAYWRIGHT_MCP_HEADLESS=false
- PLAYWRIGHT_MCP_NO_SANDBOX=true
- PLAYWRIGHT_MCP_PORT=8931
- PLAYWRIGHT_MCP_HOST=0.0.0.0
- DISPLAY=${DISPLAY:-:0}
restart: unless-stopped
shm_size: '2gb'
ipc: host
security_opt:
- seccomp:unconfined
# For visual debugging (headed mode)
playwright-headed:
image: mcr.microsoft.com/playwright:v1.58.2-noble
container_name: playwright-headed
ports:
- "8932:8931"
volumes:
- ./:/app
environment:
- DISPLAY=$DISPLAY
command: >
npx @playwright/mcp@latest
--browser chromium
--port 8931
--host 0.0.0.0
profiles:
- debug
# For running tests locally
test-runner:
image: mcr.microsoft.com/playwright:v1.58.2-noble
container_name: playwright-test
volumes:
- ./:/app
working_dir: /app
command: npx playwright test
profiles:
- test

320
install.sh Executable file
View File

@@ -0,0 +1,320 @@
#!/usr/bin/env bash
# Kilo + APAW One-Command Installer for Linux
# Usage: curl -fsSL https://git.softuniq.eu/UniqueSoft/APAW/raw/branch/dev/install.sh | bash
# OR: ./install.sh
set -euo pipefail
REPO_URL="https://git.softuniq.eu/UniqueSoft/APAW"
INSTALL_DIR="${APAW_DIR:-$HOME/APAW}"
VSCODE_EXTENSION="kilocode.kilo-code"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; }
ok() { printf "${GREEN}[OK]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
err() { printf "${RED}[ERR]${NC} %s\n" "$*" >&2; }
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "$ID"
else
echo "unknown"
fi
}
install_vscode() {
if command -v code &>/dev/null || command -v codium &>/dev/null; then
ok "VS Code / VSCodium already installed"
return 0
fi
local dist
dist=$(detect_distro)
info "Installing VS Code for distro: $dist"
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a
case "$dist" in
ubuntu|debian|pop|mint|elementary|zorin)
sudo apt-get update -qq
sudo apt-get install -y -qq wget gpg apt-transport-https
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/packages.microsoft.gpg
sudo install -D -o root -g root -m 644 /tmp/packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
sudo apt-get update -qq
sudo apt-get install -y -qq code
;;
fedora|rhel|centos|rocky|almalinux)
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
sudo sh -c 'echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/vscode.repo'
sudo dnf install -y code
;;
arch|manjaro|endeavouros)
if command -v yay &>/dev/null; then
yay -S --noconfirm visual-studio-code-bin
elif command -v paru &>/dev/null; then
paru -S --noconfirm visual-studio-code-bin
else
sudo pacman -Sy --noconfirm git base-devel
git clone --depth=1 https://aur.archlinux.org/visual-studio-code-bin.git /tmp/vscode-aur
(cd /tmp/vscode-aur && makepkg -si --noconfirm)
fi
;;
alpine)
sudo apk add --no-cache curl
curl -L -o /tmp/vscode.tar.gz "https://code.visualstudio.com/sha/download?build=stable&os=linux-x64"
sudo mkdir -p /usr/share/vscode
sudo tar -xzf /tmp/vscode.tar.gz -C /usr/share/vscode --strip-components=1
sudo ln -sf /usr/share/vscode/bin/code /usr/local/bin/code
;;
*)
warn "Unknown distro, trying snap install..."
if command -v snap &>/dev/null; then
sudo snap install code --classic
else
err "Cannot auto-install VS Code on '$dist'. Please install it manually, then re-run this script."
exit 1
fi
;;
esac
ok "VS Code installed"
}
install_node_bun() {
if command -v bun &>/dev/null; then
ok "Bun already installed: $(bun --version)"
return 0
fi
if command -v node &>/dev/null && command -v npm &>/dev/null; then
ok "Node.js already installed: $(node --version)"
else
info "Installing Node.js via NodeSource..."
local dist
dist=$(detect_distro)
case "$dist" in
ubuntu|debian|pop|mint)
sudo apt-get install -y -qq curl ca-certificates gnupg
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
sudo apt-get install -y -qq nodejs
;;
fedora|rhel|rocky|almalinux)
sudo dnf install -y nodejs npm
;;
arch|manjaro)
sudo pacman -Sy --noconfirm nodejs npm
;;
alpine)
sudo apk add --no-cache nodejs npm
;;
*)
warn "Installing Node via n..."
curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | sudo bash -s lts
;;
esac
fi
info "Installing Bun..."
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
# Make bun available immediately and persistently
if ! grep -q '.bun/bin' ~/.bashrc 2>/dev/null; then
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc
fi
if [ -f ~/.profile ] && ! grep -q '.bun/bin' ~/.profile 2>/dev/null; then
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.profile
fi
if [ -d /etc/profile.d ] && [ "$EUID" -eq 0 ]; then
echo 'export PATH="/root/.bun/bin:$PATH"' > /etc/profile.d/bun.sh
chmod 644 /etc/profile.d/bun.sh
fi
ok "Bun installed: $(bun --version)"
}
install_docker() {
if command -v docker &>/dev/null && command -v docker-compose &>/dev/null; then
ok "Docker + Docker Compose already installed"
return 0
fi
info "Installing Docker..."
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER" 2>/dev/null || true
ok "Docker installed (re-login may be needed for group permissions)"
}
install_git() {
if command -v git &>/dev/null; then
ok "Git already installed: $(git --version)"
return 0
fi
local dist
dist=$(detect_distro)
info "Installing Git..."
case "$dist" in
ubuntu|debian|pop|mint|elementary)
sudo apt-get update -qq && sudo apt-get install -y -qq git
;;
fedora|rhel|centos|rocky|almalinux)
sudo dnf install -y git
;;
arch|manjaro)
sudo pacman -Sy --noconfirm git
;;
alpine)
sudo apk add --no-cache git
;;
*)
err "Cannot auto-install git on '$dist'. Please install manually."
exit 1
;;
esac
ok "Git installed"
}
install_vscode_extension() {
local bin=""
if command -v code &>/dev/null; then
bin="code"
elif command -v codium &>/dev/null; then
bin="codium"
else
warn "VS Code binary not found, skipping extension install"
return 1
fi
info "Installing Kilo Code extension ($VSCODE_EXTENSION)..."
# 1. Install for current user (no custom --user-data-dir so it lands in standard location)
"$bin" --install-extension "$VSCODE_EXTENSION" --force || {
warn "Marketplace failed, trying OpenVSX..."
"$bin" --install-extension "https://open-vsx.org/extension/kilocode/kilo-code" --force || warn "Extension install failed for current user."
}
# 2. If root, also install for every regular user with a home directory
if [ "$EUID" -eq 0 ]; then
local ext_dir="/usr/share/code/resources/app/extensions"
[ -d "$ext_dir" ] || ext_dir="/usr/share/vscode/resources/app/extensions"
[ -d "$ext_dir" ] || ext_dir=""
for user_home in /home/*; do
[ -d "$user_home" ] || continue
local user_name
user_name=$(basename "$user_home")
# Only real users with uid >= 1000
local user_uid
user_uid=$(id -u "$user_name" 2>/dev/null || echo 0)
[ "$user_uid" -ge 1000 ] || continue
info "Installing Kilo Code extension for user: $user_name ..."
su - "$user_name" -c "$bin --install-extension $VSCODE_EXTENSION --force" || {
warn "Marketplace failed for $user_name, trying OpenVSX..."
su - "$user_name" -c "$bin --install-extension https://open-vsx.org/extension/kilocode/kilo-code --force" || warn "Extension install failed for $user_name."
}
done
# 3. Try system-wide install (copy into VS Code's bundled extensions)
if [ -n "$ext_dir" ] && [ -d "$ext_dir" ]; then
local current_user_ext
current_user_ext=$(ls -d "$HOME/.vscode/extensions/kilocode.kilo-code-"* 2>/dev/null | head -1)
if [ -n "$current_user_ext" ] && [ ! -d "$ext_dir/kilocode.kilo-code" ]; then
info "Copying extension to system-wide directory..."
cp -r "$current_user_ext" "$ext_dir/" && ok "System-wide extension installed" || warn "System-wide copy failed (permissions)."
fi
fi
fi
ok "Kilo Code extension installed"
}
clone_apaw() {
if [ -d "$INSTALL_DIR/.git" ]; then
info "APAW repo already exists at $INSTALL_DIR, pulling latest..."
(cd "$INSTALL_DIR" && git pull --ff-only)
else
info "Cloning APAW into $INSTALL_DIR..."
git clone "$REPO_URL" "$INSTALL_DIR"
fi
ok "APAW repo ready at $INSTALL_DIR"
}
setup_apaw() {
info "Setting up APAW dependencies..."
cd "$INSTALL_DIR"
bun install || npm install
if [ ! -f .env ]; then
cp .env.example .env 2>/dev/null || true
fi
ok "APAW dependencies installed"
}
print_summary() {
local dist
dist=$(detect_distro)
echo ""
echo "========================================"
echo " Kilo + APAW Installation Complete"
echo "========================================"
local vscode_ver="N/A"
if command -v code &>/dev/null; then
vscode_ver=$(code --version 2>/dev/null | head -1 || echo "VS Code")
elif command -v codium &>/dev/null; then
vscode_ver=$(codium --version 2>/dev/null | head -1 || echo "VSCodium")
fi
ok "VS Code: $vscode_ver"
ok "Kilo Ext: $VSCODE_EXTENSION"
if [ "$EUID" -eq 0 ]; then
ok "Kilo Users: root + all regular users (/home/*)"
fi
ok "Node: $(node --version 2>/dev/null || echo 'N/A')"
ok "Bun: $(bun --version 2>/dev/null || echo 'N/A')"
ok "Docker: $(docker --version 2>/dev/null || echo 'N/A')"
ok "Git: $(git --version 2>/dev/null || echo 'N/A')"
ok "APAW Path: $INSTALL_DIR"
echo ""
info "Next steps:"
echo " cd $INSTALL_DIR"
echo " code ."
echo ""
if [ "$EUID" -eq 0 ]; then
echo " For root GUI sessions, if sandbox errors:"
echo " code --no-sandbox ."
echo ""
fi
if [ "$dist" != "unknown" ] && ! id -nG "$USER" | grep -qw docker; then
warn "Docker group change requires re-login. Run: newgrp docker"
fi
}
main() {
echo "========================================"
echo " Kilo + APAW Linux Installer"
echo "========================================"
echo ""
install_git
install_node_bun
install_docker
install_vscode
install_vscode_extension
clone_apaw
setup_apaw
print_summary
}
main "$@"

391
kilo-meta.json Normal file
View File

@@ -0,0 +1,391 @@
{
"$schema": "https://app.kilo.ai/config.json",
"metaVersion": "1.0.0",
"lastSync": "2026-04-27T11:07:02.592Z",
"agents": {
"requirement-refiner": {
"file": ".kilo/agents/requirement-refiner.md",
"description": "Converts vague ideas and bug reports into strict User Stories with acceptance criteria checklists",
"model": "ollama-cloud/kimi-k2-thinking",
"mode": "all",
"color": "#4F46E5",
"category": "core"
},
"history-miner": {
"file": ".kilo/agents/history-miner.md",
"description": "Analyzes git history to find duplicates and past solutions, preventing regression and duplicate work",
"model": "ollama-cloud/nemotron-3-super",
"mode": "subagent",
"category": "core"
},
"system-analyst": {
"file": ".kilo/agents/system-analyst.md",
"description": "Designs technical specifications, data schemas, and API contracts before implementation",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"category": "core"
},
"sdet-engineer": {
"file": ".kilo/agents/sdet-engineer.md",
"description": "Writes tests following TDD methodology. Tests MUST fail initially (Red phase)",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "all",
"color": "#8B5CF6",
"category": "core"
},
"lead-developer": {
"file": ".kilo/agents/lead-developer.md",
"description": "Primary code writer for backend and core logic. Writes implementation to pass tests",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"color": "#DC2626",
"category": "core"
},
"frontend-developer": {
"file": ".kilo/agents/frontend-developer.md",
"description": "Handles UI implementation with multimodal capabilities. Accepts visual references like screenshots and mockups",
"model": "ollama-cloud/minimax-m2.5",
"mode": "all",
"color": "#0EA5E9",
"category": "core"
},
"backend-developer": {
"file": ".kilo/agents/backend-developer.md",
"description": "Backend specialist for Node.js, Express, APIs, and database integration",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"color": "#10B981",
"category": "core"
},
"go-developer": {
"file": ".kilo/agents/go-developer.md",
"description": "Go backend specialist for Gin, Echo, APIs, and database integration",
"model": "ollama-cloud/deepseek-v4-pro-max",
"mode": "subagent",
"color": "#00ADD8",
"category": "core"
},
"devops-engineer": {
"file": ".kilo/agents/devops-engineer.md",
"description": "DevOps specialist for Docker, Kubernetes, CI/CD pipeline automation, and infrastructure management",
"model": "ollama-cloud/kimi-k2.6",
"mode": "subagent",
"color": "#FF6B35",
"category": "core"
},
"code-skeptic": {
"file": ".kilo/agents/code-skeptic.md",
"description": "Adversarial code reviewer. Finds problems and issues. Does NOT suggest implementations",
"model": "ollama-cloud/minimax-m2.5",
"mode": "subagent",
"color": "#E11D48",
"category": "quality"
},
"the-fixer": {
"file": ".kilo/agents/the-fixer.md",
"description": "Iteratively fixes bugs based on specific error reports and test failures",
"model": "ollama-cloud/kimi-k2.6",
"mode": "all",
"color": "#F59E0B",
"category": "quality"
},
"performance-engineer": {
"file": ".kilo/agents/performance-engineer.md",
"description": "Reviews code for performance issues. Focuses on efficiency, N+1 queries, memory leaks, and algorithmic complexity",
"model": "ollama-cloud/deepseek-v4-pro-max",
"mode": "all",
"color": "#0D9488",
"category": "quality"
},
"security-auditor": {
"file": ".kilo/agents/security-auditor.md",
"description": "Scans for security vulnerabilities, OWASP Top 10, dependency CVEs, and hardcoded secrets",
"model": "ollama-cloud/deepseek-v4-pro-max",
"mode": "subagent",
"color": "#DC2626",
"category": "quality"
},
"visual-tester": {
"file": ".kilo/agents/visual-tester.md",
"description": "Visual regression testing agent that compares screenshots and detects UI differences using pixelmatch and image diff",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"category": "quality"
},
"orchestrator": {
"file": ".kilo/agents/orchestrator.md",
"description": "Main dispatcher. Routes tasks between agents based on Issue status and manages the workflow state machine",
"model": "ollama-cloud/kimi-k2.6",
"mode": "all",
"color": "#7C3AED",
"category": "meta"
},
"release-manager": {
"file": ".kilo/agents/release-manager.md",
"description": "Manages git operations, semantic versioning, branching, and deployments. Ensures clean history",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"category": "meta"
},
"evaluator": {
"file": ".kilo/agents/evaluator.md",
"description": "Scores agent effectiveness after task completion for continuous improvement",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"color": "#047857",
"category": "meta"
},
"prompt-optimizer": {
"file": ".kilo/agents/prompt-optimizer.md",
"description": "Improves agent system prompts based on performance failures. Meta-learner for prompt optimization",
"model": "ollama-cloud/qwen3.6-plus",
"mode": "subagent",
"category": "meta"
},
"product-owner": {
"file": ".kilo/agents/product-owner.md",
"description": "Manages issue checklists, status labels, tracks progress and coordinates with human users",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"category": "meta"
},
"agent-architect": {
"file": ".kilo/agents/agent-architect.md",
"description": "Creates, modifies, and reviews new agents, workflows, and skills based on capability gap analysis",
"model": "ollama-cloud/kimi-k2.6",
"mode": "subagent",
"category": "meta"
},
"capability-analyst": {
"file": ".kilo/agents/capability-analyst.md",
"description": "Analyzes task requirements against available agents, workflows, and skills. Identifies gaps and recommends new components.",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"category": "meta"
},
"workflow-architect": {
"file": ".kilo/agents/workflow-architect.md",
"description": "Creates and maintains workflow definitions with complete architecture, Gitea integration, and quality gates",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"category": "meta"
},
"markdown-validator": {
"file": ".kilo/agents/markdown-validator.md",
"description": "Validates and corrects Markdown descriptions for Gitea issues",
"model": "ollama-cloud/deepseek-v4-pro-max",
"mode": "subagent",
"category": "meta"
},
"browser-automation": {
"file": ".kilo/agents/browser-automation.md",
"description": "Browser automation agent using Playwright MCP for E2E testing, form filling, navigation, and web interaction",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"category": "testing"
},
"planner": {
"file": ".kilo/agents/planner.md",
"description": "Advanced task planner using Chain of Thought, Tree of Thoughts, and Plan-Execute-Reflect",
"model": "ollama-cloud/deepseek-v4-pro-max",
"mode": "subagent",
"color": "#F59E0B",
"category": "cognitive"
},
"reflector": {
"file": ".kilo/agents/reflector.md",
"description": "Self-reflection agent using Reflexion pattern - learns from mistakes",
"model": "ollama-cloud/deepseek-v4-pro-max",
"mode": "subagent",
"color": "#10B981",
"category": "cognitive"
},
"memory-manager": {
"file": ".kilo/agents/memory-manager.md",
"description": "Manages agent memory systems - short-term (context), long-term (vector store), and episodic (experiences)",
"model": "ollama-cloud/qwen3.6-plus",
"mode": "subagent",
"color": "#8B5CF6",
"category": "cognitive"
},
"architect-indexer": {
"file": ".kilo/agents/architect-indexer.md",
"description": "Indexes and maps project codebase architecture into .architect/ directory",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"color": "#10B981",
"category": "core"
},
"flutter-developer": {
"file": ".kilo/agents/flutter-developer.md",
"description": "Flutter mobile specialist for cross-platform apps, state management, and UI components",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"color": "#02569B",
"category": "core"
},
"php-developer": {
"file": ".kilo/agents/php-developer.md",
"description": "PHP specialist for Laravel, Symfony, WordPress, and modular architecture",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"color": "#8B5CF6",
"category": "core"
},
"pipeline-judge": {
"file": ".kilo/agents/pipeline-judge.md",
"description": "Automated pipeline judge. Evaluates workflow execution by running tests, measuring token cost and wall-clock time. Produces objective fitness scores. Never writes code - only measures and scores.",
"model": "ollama-cloud/glm-5.1",
"mode": "subagent",
"color": "#DC2626",
"category": "meta"
},
"python-developer": {
"file": ".kilo/agents/python-developer.md",
"description": "Python specialist for Django, FastAPI, data processing, and ML pipelines",
"model": "ollama-cloud/qwen3-coder:480b",
"mode": "subagent",
"color": "#3776AB",
"category": "core"
},
"incident-responder": {
"file": ".kilo/agents/incident-responder.md",
"description": "Server incident response and system hardening specialist. Handles live forensics, malware removal, persistence hunting, SSH-based server cleanup, and post-incident hardening. Works with any OS and panel.",
"model": "ollama-cloud/kimi-k2.6",
"mode": "subagent",
"color": "#B91C1C",
"category": "core"
}
},
"commands": {
"pipeline": {
"file": ".kilo/commands/pipeline.md",
"description": "Run full agent pipeline for issue with Gitea logging"
},
"status": {
"file": ".kilo/commands/status.md",
"description": "Check pipeline status for issue",
"model": "qwen/qwen3.6-plus:free"
},
"evaluate": {
"file": ".kilo/commands/evaluate.md",
"description": "Generate performance report",
"model": "ollama-cloud/gpt-oss:120b"
},
"plan": {
"file": ".kilo/commands/plan.md",
"description": "Creates detailed task plans",
"model": "openrouter/qwen/qwen3-coder:free"
},
"ask": {
"file": ".kilo/commands/ask.md",
"description": "Answers codebase questions",
"model": "openai/qwen3-32b"
},
"debug": {
"file": ".kilo/commands/debug.md",
"description": "Analyzes and fixes bugs",
"model": "ollama-cloud/gpt-oss:20b"
},
"code": {
"file": ".kilo/commands/code.md",
"description": "Quick code generation",
"model": "openrouter/qwen/qwen3-coder:free"
},
"research": {
"file": ".kilo/commands/research.md",
"description": "Run research and self-improvement",
"model": "ollama-cloud/glm-5"
},
"feature": {
"file": ".kilo/commands/feature.md",
"description": "Full feature development pipeline",
"model": "openrouter/qwen/qwen3-coder:free"
},
"hotfix": {
"file": ".kilo/commands/hotfix.md",
"description": "Hotfix workflow",
"model": "openrouter/minimax/minimax-m2.5:free"
},
"review": {
"file": ".kilo/commands/review.md",
"description": "Code review workflow",
"model": "openrouter/minimax/minimax-m2.5:free"
},
"review-watcher": {
"file": ".kilo/commands/review-watcher.md",
"description": "Auto-validate review results",
"model": "ollama-cloud/glm-5"
},
"e2e-test": {
"file": ".kilo/commands/e2e-test.md",
"description": "Run E2E tests with browser automation"
},
"workflow": {
"file": ".kilo/commands/workflow.md",
"description": "Run complete workflow with quality gates",
"model": "ollama-cloud/glm-5"
},
"landing-page": {
"file": ".kilo/commands/landing-page.md",
"description": "Create landing page CMS from HTML mockups",
"model": "ollama-cloud/kimi-k2.5"
},
"commerce": {
"file": ".kilo/commands/commerce.md",
"description": "Create e-commerce site with products, cart, payments",
"model": "qwen/qwen3-coder:free"
},
"blog": {
"file": ".kilo/commands/blog.md",
"description": "Create blog/CMS with posts, comments, SEO",
"model": "qwen/qwen3-coder:free"
},
"booking": {
"file": ".kilo/commands/booking.md",
"description": "Create booking system for services/appointments",
"model": "qwen/qwen3-coder:free"
}
},
"syncTargets": [
{
"file": ".kilo/agents/*.md",
"type": "agent-frontmatter",
"fields": [
"model",
"mode",
"description",
"color"
]
},
{
"file": ".kilo/KILO_SPEC.md",
"section": "### Pipeline Agents",
"type": "markdown-table"
},
{
"file": ".kilo/KILO_SPEC.md",
"section": "### Workflow Commands",
"type": "markdown-table"
},
{
"file": "AGENTS.md",
"section": "Pipeline Agents",
"type": "category-tables"
},
{
"file": ".kilo/agents/orchestrator.md",
"section": "Task Tool Invocation",
"type": "subagent-mapping"
}
],
"validation": {
"checkOn": [
"evolutionary-mode",
"pre-commit",
"manual-sync"
],
"failOnError": true,
"reportFile": ".kilo/logs/sync-violations.json"
}
}

523
kilo.jsonc Normal file
View File

@@ -0,0 +1,523 @@
{
"$schema": "https://app.kilo.ai/config.json",
"instructions": [
".kilo/rules/global.md",
".kilo/rules/agent-patterns.md",
".kilo/rules/docker.md",
".kilo/rules/go.md",
".kilo/rules/history-miner.md",
".kilo/rules/lead-developer.md",
".kilo/rules/nodejs.md",
".kilo/rules/prompt-engineering.md",
".kilo/rules/release-manager.md",
".kilo/rules/sdet-engineer.md",
".kilo/rules/code-skeptic.md",
".kilo/rules/evolutionary-sync.md"
],
"skills": {
"paths": [
".kilo/skills"
]
},
"agent": {
"requirement-refiner": {
"description": "Converts vague ideas and bug reports into strict User Stories with acceptance criteria checklists",
"mode": "all",
"model": "ollama-cloud/kimi-k2-thinking",
"color": "#4F46E5",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"history-miner": "allow",
"system-analyst": "allow",
"subagent": "deny"
}
}
},
"history-miner": {
"description": "Analyzes git history to find duplicates and past solutions, preventing regression and duplicate work",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"system-analyst": {
"description": "Designs technical specifications, data schemas, and API contracts before implementation",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"sdet-engineer": {
"description": "Writes tests following TDD methodology. Tests MUST fail initially (Red phase)",
"mode": "all",
"model": "ollama-cloud/qwen3-coder:480b",
"color": "#8B5CF6",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"lead-developer": "allow",
"subagent": "deny"
}
}
},
"lead-developer": {
"description": "Primary code writer for backend and core logic. Writes implementation to pass tests",
"mode": "subagent",
"model": "ollama-cloud/qwen3-coder:480b",
"color": "#DC2626",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"subagent": "deny"
}
}
},
"frontend-developer": {
"description": "Handles UI implementation with multimodal capabilities. Accepts visual references like screenshots and mockups",
"mode": "all",
"model": "ollama-cloud/minimax-m2.5",
"color": "#0EA5E9",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"subagent": "deny"
}
}
},
"backend-developer": {
"description": "Backend specialist for Node.js, Express, APIs, and database integration",
"mode": "subagent",
"model": "ollama-cloud/minimax-m2.5",
"color": "#10B981",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"subagent": "deny"
}
}
},
"go-developer": {
"description": "Go backend specialist for Gin, Echo, APIs, and database integration",
"mode": "subagent",
"model": "ollama-cloud/minimax-m2.5",
"color": "#00ADD8",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"subagent": "deny"
}
}
},
"devops-engineer": {
"description": "DevOps specialist for Docker, Kubernetes, CI/CD pipeline automation, and infrastructure management",
"mode": "subagent",
"model": "ollama-cloud/minimax-m2.5",
"color": "#FF6B35",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"security-auditor": "allow",
"subagent": "deny"
}
}
},
"code-skeptic": {
"description": "Adversarial code reviewer. Finds problems and issues. Does NOT suggest implementations",
"mode": "subagent",
"model": "ollama-cloud/deepseek-v4-pro-max",
"color": "#E11D48",
"permission": {
"read": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"the-fixer": "allow",
"performance-engineer": "allow",
"subagent": "deny"
}
}
},
"the-fixer": {
"description": "Iteratively fixes bugs based on specific error reports and test failures",
"mode": "all",
"model": "ollama-cloud/kimi-k2.6",
"color": "#F59E0B",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"orchestrator": "allow",
"subagent": "deny"
}
}
},
"performance-engineer": {
"description": "Reviews code for performance issues. Focuses on efficiency, N+1 queries, memory leaks, and algorithmic complexity",
"mode": "all",
"model": "ollama-cloud/kimi-k2.6",
"color": "#0D9488",
"permission": {
"read": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"the-fixer": "allow",
"security-auditor": "allow",
"subagent": "deny"
}
}
},
"security-auditor": {
"description": "Scans for security vulnerabilities, OWASP Top 10, dependency CVEs, and hardcoded secrets",
"mode": "subagent",
"model": "ollama-cloud/kimi-k2.6",
"color": "#DC2626",
"permission": {
"read": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"the-fixer": "allow",
"release-manager": "allow",
"subagent": "deny"
}
}
},
"visual-tester": {
"description": "Visual regression testing agent that compares screenshots and detects UI differences using pixelmatch and image diff",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"read": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"orchestrator": {
"description": "Main dispatcher. Routes tasks between agents based on Issue status and manages the workflow state machine",
"mode": "all",
"model": "ollama-cloud/kimi-k2.6",
"color": "#7C3AED",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "ask",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"history-miner": "allow",
"system-analyst": "allow",
"sdet-engineer": "allow",
"lead-developer": "allow",
"code-skeptic": "allow",
"the-fixer": "allow",
"performance-engineer": "allow",
"security-auditor": "allow",
"release-manager": "allow",
"evaluator": "allow",
"prompt-optimizer": "allow",
"product-owner": "allow",
"requirement-refiner": "allow",
"frontend-developer": "allow",
"browser-automation": "allow",
"visual-tester": "allow",
"planner": "allow",
"reflector": "allow",
"memory-manager": "allow",
"devops-engineer": "allow",
"subagent": "deny"
}
}
},
"release-manager": {
"description": "Manages git operations, semantic versioning, branching, and deployments. Ensures clean history",
"mode": "subagent",
"model": "ollama-cloud/qwen3.6-plus",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "ask",
"glob": "allow",
"grep": "allow",
"webfetch": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"evaluator": {
"description": "Scores agent effectiveness after task completion for continuous improvement",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"color": "#047857",
"permission": {
"read": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"prompt-optimizer": "allow",
"product-owner": "allow",
"subagent": "deny"
}
}
},
"prompt-optimizer": {
"description": "Improves agent system prompts based on performance failures. Meta-learner for prompt optimization",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"product-owner": {
"description": "Manages issue checklists, status labels, tracks progress and coordinates with human users",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"webfetch": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"agent-architect": {
"description": "Creates, modifies, and reviews new agents, workflows, and skills based on capability gap analysis",
"mode": "subagent",
"model": "ollama-cloud/kimi-k2.6",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"capability-analyst": {
"description": "Analyzes task requirements against available agents, workflows, and skills. Identifies gaps and recommends new components.",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"read": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"workflow-architect": {
"description": "Creates and maintains workflow definitions with complete architecture, Gitea integration, and quality gates",
"mode": "subagent",
"model": "ollama-cloud/glm-5.1",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"markdown-validator": {
"description": "Validates and corrects Markdown descriptions for Gitea issues",
"mode": "subagent",
"model": "ollama-cloud/deepseek-v4-pro-max",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"browser-automation": {
"description": "Browser automation agent using Playwright MCP for E2E testing, form filling, navigation, and web interaction",
"mode": "subagent",
"model": "ollama-cloud/qwen3-coder:480b",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"planner": {
"description": "Advanced task planner using Chain of Thought, Tree of Thoughts, and Plan-Execute-Reflect",
"mode": "subagent",
"model": "ollama-cloud/deepseek-v4-pro-max",
"color": "#F59E0B",
"permission": {
"read": "allow",
"write": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"reflector": {
"description": "Self-reflection agent using Reflexion pattern - learns from mistakes",
"mode": "subagent",
"model": "ollama-cloud/deepseek-v4-pro-max",
"color": "#10B981",
"permission": {
"read": "allow",
"grep": "allow",
"glob": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"memory-manager": {
"description": "Manages agent memory systems - short-term (context), long-term (vector store), and episodic (experiences)",
"mode": "subagent",
"model": "ollama-cloud/qwen3.6-plus",
"color": "#8B5CF6",
"permission": {
"read": "allow",
"write": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"subagent": "deny"
}
}
},
"incident-responder": {
"description": "Server incident response and system hardening specialist. Handles live forensics, malware removal, persistence hunting, SSH-based server cleanup, and post-incident hardening. Works with any OS and panel.",
"mode": "subagent",
"model": "ollama-cloud/kimi-k2.6",
"color": "#B91C1C",
"permission": {
"read": "allow",
"edit": "allow",
"write": "allow",
"bash": "allow",
"glob": "allow",
"grep": "allow",
"task": {
"*": "deny",
"code-skeptic": "allow",
"orchestrator": "allow",
"subagent": "deny"
}
}
}
}
}

81
package-lock.json generated Normal file
View File

@@ -0,0 +1,81 @@
{
"name": "apaw",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "apaw",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"@types/bun": "^1.1.6",
"@types/node": "^20.10.0",
"typescript": "^5.4.5"
}
},
"node_modules/@types/bun": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz",
"integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"bun-types": "1.3.14"
}
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/bun-types": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz",
"integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

60
package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "apaw",
"version": "1.0.0",
"description": "Self-improving code pipeline with agent management and Gitea logging",
"type": "module",
"main": "./dist/kilocode/index.js",
"types": "./dist/kilocode/index.d.ts",
"exports": {
".": {
"import": "./dist/kilocode/index.js",
"types": "./dist/kilocode/index.d.ts"
},
"./agent-manager": {
"import": "./dist/kilocode/agent-manager/index.js",
"types": "./dist/kilocode/agent-manager/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit",
"test": "bun test",
"sync:evolution": "bun run agent-evolution/scripts/sync-agent-history.ts && node agent-evolution/scripts/build-standalone.cjs",
"evolution:build": "node agent-evolution/scripts/build-standalone.cjs",
"evolution:open": "start agent-evolution/index.standalone.html",
"evolution:dashboard": "bunx serve agent-evolution -l 3001",
"evolution:run": "docker run -d --name apaw-evolution-dashboard -p 3001:3001 -v \"$(pwd)/agent-evolution/data:/app/data:ro\" apaw-evolution:latest",
"evolution:stop": "docker stop apaw-evolution-dashboard && docker rm apaw-evolution-dashboard",
"evolution:start": "bash agent-evolution/docker-run.sh run",
"evolution:dev": "docker-compose -f docker-compose.evolution.yml up -d",
"evolution:logs": "docker logs -f apaw-evolution-dashboard",
"agent:stats": "bun run scripts/agent-stats.ts",
"agent:stats:week": "bun run scripts/agent-stats.ts --last 7",
"agent:stats:project": "bun run scripts/agent-stats.ts --project",
"arch:index": "docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer",
"arch:index:full": "docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer --mode full",
"arch:index:incremental": "docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer --mode incremental",
"arch:build": "docker compose -f docker/docker-compose.architect.yml build",
"arch:status": "docker compose -f docker/docker-compose.architect.yml ps"
},
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"@types/bun": "^1.1.6",
"@types/node": "^20.10.0",
"typescript": "^5.4.5"
},
"keywords": [
"agent",
"pipeline",
"workflow",
"gitea",
"automation",
"self-improving",
"kilocode"
],
"license": "MIT"
}

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

View File

@@ -0,0 +1,6 @@
function testErrorRecovery() {
const x = { property: 42 };
return x.property;
}
module.exports = testErrorRecovery;

12
src/utils/divide.ts Normal file
View File

@@ -0,0 +1,12 @@
export function divide(a: number, b: number): number {
if (typeof a !== 'number' || isNaN(a)) {
throw new Error('Первый аргумент должен быть числом');
}
if (typeof b !== 'number' || isNaN(b)) {
throw new Error('Второй аргумент должен быть числом');
}
if (b === 0) {
throw new Error('Деление на ноль невозможно');
}
return a / b;
}

View File

@@ -0,0 +1,53 @@
// Test file for validation functions
import { describe, it, expect } from 'bun:test'
import { add, subtract, multiply, divide } from './add'
describe('Validation Functions', () => {
describe('add', () => {
it('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5)
})
it('should add negative numbers', () => {
expect(add(-1, -2)).toBe(-3)
})
it('should add zero', () => {
expect(add(5, 0)).toBe(5)
})
it('should handle floating point', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3)
})
})
describe('subtract', () => {
it('should subtract two numbers', () => {
expect(subtract(5, 3)).toBe(2)
})
it('should handle negative result', () => {
expect(subtract(3, 5)).toBe(-2)
})
})
describe('multiply', () => {
it('should multiply two numbers', () => {
expect(multiply(4, 3)).toBe(12)
})
it('should multiply by zero', () => {
expect(multiply(5, 0)).toBe(0)
})
})
describe('divide', () => {
it('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5)
})
it('should throw on division by zero', () => {
expect(() => divide(5, 0)).toThrow('Division by zero')
})
})
})

46
src/validation/add.ts Normal file
View File

@@ -0,0 +1,46 @@
// Test file for autonomous pipeline testing
// This should pass through: requirement-refiner → sdet-engineer → lead-developer → code-skeptic
/**
* Adds two numbers
* @param a - First number
* @param b - Second number
* @returns Sum of a and b
*/
export function add(a: number, b: number): number {
return a + b
}
/**
* Subtracts second number from first
* @param a - First number
* @param b - Second number
* @returns Difference of a and b
*/
export function subtract(a: number, b: number): number {
return a - b
}
/**
* Multiplies two numbers
* @param a - First number
* @param b - Second number
* @returns Product of a and b
*/
export function multiply(a: number, b: number): number {
return a * b
}
/**
* Divides first number by second
* @param a - Dividend
* @param b - Divisor
* @returns Quotient of a and b
* @throws Error if b is zero
*/
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero')
}
return a / b
}

View File

@@ -0,0 +1,48 @@
import { describe, it, expect } from 'bun:test'
import { validateEmail } from './email'
describe('validateEmail', () => {
describe('valid emails', () => {
it('should return true for standard email', () => {
expect(validateEmail('user@example.com')).toBe(true)
})
it('should return true for email with dot in local part', () => {
expect(validateEmail('test.email@domain.org')).toBe(true)
})
it('should return true for email with plus tag', () => {
expect(validateEmail('user+tag@example.com')).toBe(true)
})
it('should return true for email with subdomain', () => {
expect(validateEmail('user@sub.domain.com')).toBe(true)
})
})
describe('invalid emails', () => {
it('should return false for string without @', () => {
expect(validateEmail('invalid')).toBe(false)
})
it('should return false for missing local part', () => {
expect(validateEmail('@example.com')).toBe(false)
})
it('should return false for missing domain', () => {
expect(validateEmail('user@')).toBe(false)
})
it('should return false for domain starting with dot', () => {
expect(validateEmail('user@.com')).toBe(false)
})
it('should return false for empty string', () => {
expect(validateEmail('')).toBe(false)
})
it('should return false for space in local part', () => {
expect(validateEmail('user name@example.com')).toBe(false)
})
})
})

22
src/validation/email.ts Normal file
View File

@@ -0,0 +1,22 @@
export function validateEmail(email: string): boolean {
if (!email) return false;
const parts = email.split('@');
if (parts.length !== 2) return false;
const [localPart, domain] = parts;
// Check if local part exists
if (!localPart) return false;
// Check if domain exists and doesn't start with a dot
if (!domain || domain.startsWith('.')) return false;
// Check for spaces in local part
if (localPart.includes(' ')) return false;
// Basic validation for domain format
if (!domain.includes('.')) return false;
return true;
}

142
tests/README.md Normal file
View File

@@ -0,0 +1,142 @@
# Web Testing Suite
Автоматическое тестирование веб-приложений. Запускается целиком в Docker без зависимостей на хосте.
## Возможности
| Тест | Скрипт | Описание |
|------|--------|----------|
| **Visual Regression** | `visual-test-pipeline.js` | Скриншоты + pixelmatch + извлечение UI-элементов с bbox |
| **Screenshot Capture** | `capture-screenshots.js` | Захват baseline/current скриншотов в 3 viewport |
| **Screenshot Compare** | `compare-screenshots.js` | Сравнение PNG через pixelmatch |
| **Console Errors** | `console-error-monitor-standalone.js` | Ловит JS ошибки, network 4xx/5xx, request failures |
| **Link Checking** | `link-checker.js` | Проверка ссылок на 404/500 |
## Быстрый старт
### Вариант 1: Docker Compose (рекомендуется)
```bash
# Полный визуальный пайплайн (захват + сравнение + элементы + ошибки)
docker compose -f docker/docker-compose.web-testing.yml run --rm \
-e TARGET_URL=https://example.com \
-e PAGES=/ \
visual-tester
# Только захват baseline-скриншотов
docker compose -f docker/docker-compose.web-testing.yml run --rm \
-e TARGET_URL=https://example.com \
screenshot-baseline
# Только захват текущих скриншотов
docker compose -f docker/docker-compose.web-testing.yml run --rm \
-e TARGET_URL=https://example.com \
screenshot-current
# Только сравнение (pixelmatch)
docker compose -f docker/docker-compose.web-testing.yml run --rm visual-compare
# Мониторинг консольных ошибок
docker compose -f docker/docker-compose.web-testing.yml run --rm \
-e TARGET_URL=https://example.com \
console-monitor
```
### Вариант 2: Прямой docker run
```bash
docker run --rm \
--add-host=host.docker.internal:host-gateway \
--shm-size=2g \
-v $(pwd)/tests:/app/tests \
-e TARGET_URL=https://example.com \
-e PAGES=/ \
mcr.microsoft.com/playwright:v1.52.0-noble \
sh -c "cd /app/tests && npm install --ignore-scripts && node scripts/visual-test-pipeline.js"
```
## Структура
```
tests/
├── scripts/
│ ├── visual-test-pipeline.js # Полный пайплайн (захват + сравнение + элементы)
│ ├── capture-screenshots.js # Захват скриншотов (baseline|current)
│ ├── compare-screenshots.js # Сравнение PNG (pixelmatch)
│ ├── console-error-monitor-standalone.js # Мониторинг console/network ошибок
│ ├── console-error-monitor.js # Мониторинг через Playwright MCP
│ └── link-checker.js # Проверка ссылок
├── visual/
│ ├── baseline/ # Эталонные скриншоты (git-tracked)
│ ├── current/ # Текущие скриншоты (gitignored)
│ └── diff/ # Diff-изображения (gitignored)
├── reports/ # JSON-отчёты (gitignored)
│ ├── visual-test-report.json
│ └── console-error-report.json
├── package.json
└── README.md
```
## Переменные окружения
| Переменная | По умолчанию | Описание |
|------------|--------------|----------|
| `TARGET_URL` | `http://host.docker.internal:3000` | URL тестируемого приложения |
| `PAGES` | `/,/admin/login` | Список путей через запятую |
| `PIXELMATCH_THRESHOLD` | `0.05` | Допустимый % отличий (5%) |
| `REPORTS_DIR` | `./reports` | Папка для отчётов |
## Visual Regression — как работает
1. **Захват скриншотов** — Playwright открывает страницу в 3 viewport (mobile 375x667, tablet 768x1024, desktop 1280x720)
2. **Извлечение элементов** — обходит DOM, собирает bbox, tag, className, text, href для каждого видимого элемента
3. **Сравнение** — pixelmatch сравнивает текущие PNG с baseline, генерирует diff-изображение
4. **Отчёт** — JSON с результатами: элементы, console/network ошибки, diff-процент
### Baseline-скриншоты
При первом запуске без baseline скрипт автоматически создаёт их. Для обновления:
```bash
docker compose -f docker/docker-compose.web-testing.yml run --rm \
-e TARGET_URL=https://example.com screenshot-baseline
```
### Обнаруживаемые проблемы
- Наложение элементов (кнопка вне viewportа)
- Сдвиг шрифтов / неверные цвета
- Отсутствующие / лишние элементы
- Микро-кнопки (width < 10px)
- Console JS errors
- Network errors (4xx/5xx)
## Пример отчёта
```json
{
"summary": {
"screenshotsCaptured": 3,
"totalElements": 702,
"comparisonsPassed": 3,
"comparisonsFailed": 0,
"totalConsoleErrors": 0,
"totalNetworkErrors": 25
},
"elements": {
"homepage_desktop": [
{ "tag": "button", "text": "Buy Now", "bbox": {"x":318,"y":349,"width":644,"height":47} }
]
}
}
```
## Docker-образ
Используется `mcr.microsoft.com/playwright:v1.52.0-noble` — предустановленный Playwright с Chromium.
## See Also
- `docker/docker-compose.web-testing.yml` — Docker Compose конфигурация
- `.kilo/agents/visual-tester.md` — Агент визуального тестирования
- `.kilo/commands/e2e-test.md` — E2E workflow

32
tests/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "apaw-web-testing",
"version": "2.0.0",
"description": "Web application testing suite for APAW - Visual regression, link checking, form testing, console error detection",
"main": "scripts/visual-test-pipeline.js",
"scripts": {
"test": "node scripts/visual-test-pipeline.js",
"test:visual": "node scripts/visual-test-pipeline.js",
"test:baseline": "node scripts/capture-screenshots.js baseline",
"test:current": "node scripts/capture-screenshots.js current",
"test:compare": "node scripts/compare-screenshots.js",
"test:console": "node scripts/console-error-monitor-standalone.js",
"test:links": "node scripts/link-checker.js"
},
"keywords": [
"web-testing",
"visual-regression",
"e2e",
"playwright",
"kilo-code"
],
"author": "APAW Team",
"license": "MIT",
"dependencies": {
"pixelmatch": "^5.3.0",
"playwright": "1.52.0",
"pngjs": "^7.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}

485
tests/run-all-tests.js Normal file
View File

@@ -0,0 +1,485 @@
#!/usr/bin/env node
/**
* Web Application Testing - Run All Tests
*
* Comprehensive test suite:
* 1. Visual Regression Testing
* 2. Link Checking
* 3. Form Testing
* 4. Console Error Detection
*
* Generates HTML report with all results
*/
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
// Configuration
const config = {
targetUrl: process.env.TARGET_URL || 'http://localhost:3000',
mcpPort: parseInt(process.env.MCP_PORT || '8931'),
reportsDir: process.env.REPORTS_DIR || './tests/reports',
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
};
/**
* Playwright MCP Client
*/
class PlaywrightMCP {
constructor(port = 8931) {
this.port = port;
this.host = 'localhost';
}
async request(method, params = {}) {
const http = require('http');
return new Promise((resolve, reject) => {
const body = JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: { name: method, arguments: params },
});
const req = http.request({
hostname: this.host,
port: this.port,
path: '/mcp',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => {
req.destroy();
reject(new Error('Timeout'));
});
req.write(body);
req.end();
});
}
async navigate(url) {
return this.request('browser_navigate', { url });
}
async snapshot() {
return this.request('browser_snapshot', {});
}
async screenshot(filename) {
return this.request('browser_take_screenshot', { filename });
}
async consoleMessages(level = 'error') {
return this.request('browser_console_messages', { level, all: true });
}
async networkRequests(filter = '') {
return this.request('browser_network_requests', { filter });
}
async click(ref) {
return this.request('browser_click', { ref });
}
async type(ref, text) {
return this.request('browser_type', { ref, text });
}
}
/**
* Test Runner
*/
class WebTestRunner {
constructor() {
this.mcp = new PlaywrightMCP(config.mcpPort);
this.results = {
visual: { passed: 0, failed: 0, results: [] },
links: { passed: 0, failed: 0, results: [] },
forms: { passed: 0, failed: 0, results: [] },
console: { passed: 0, failed: 0, results: [] },
};
}
/**
* Run all tests
*/
async runAll() {
console.log('═══════════════════════════════════════════════════');
console.log(' Web Application Testing Suite');
console.log('═══════════════════════════════════════════════════\n');
console.log(`Target URL: ${config.targetUrl}`);
console.log(`MCP Port: ${config.mcpPort}`);
console.log(`Reports Dir: ${config.reportsDir}\n`);
// Ensure reports directory exists
if (!fs.existsSync(config.reportsDir)) {
fs.mkdirSync(config.reportsDir, { recursive: true });
}
try {
// 1. Visual Regression
await this.runVisualTests();
// 2. Link Checking
await this.runLinkTests();
// 3. Form Testing
await this.runFormTests();
// 4. Console Errors
await this.runConsoleTests();
// Generate HTML Report
this.generateReport();
} catch (error) {
console.error('\n❌ Test suite error:', error.message);
throw error;
}
return this.results;
}
/**
* Visual Regression Tests
*/
async runVisualTests() {
console.log('\n📸 Visual Regression Testing');
console.log('─────────────────────────────────────');
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
];
try {
for (const viewport of viewports) {
console.log(` Testing ${viewport.name} (${viewport.width}x${viewport.height})...`);
await this.mcp.navigate(config.targetUrl);
await this.mcp.request('browser_resize', { width: viewport.width, height: viewport.height });
const filename = `homepage-${viewport.name}.png`;
const screenshotPath = path.join(config.reportsDir, 'screenshots', filename);
// Ensure screenshots directory exists
if (!fs.existsSync(path.dirname(screenshotPath))) {
fs.mkdirSync(path.dirname(screenshotPath), { recursive: true });
}
await this.mcp.screenshot(screenshotPath);
this.results.visual.results.push({
viewport: viewport.name,
filename,
status: 'info',
message: `Screenshot saved: ${filename}`,
});
console.log(` ✅ Screenshot: ${filename}`);
}
this.results.visual.passed = viewports.length;
} catch (error) {
console.log(` ❌ Visual test error: ${error.message}`);
this.results.visual.failed++;
}
}
/**
* Link Checking Tests
*/
async runLinkTests() {
console.log('\n🔗 Link Checking');
console.log('─────────────────────────────────────');
try {
await this.mcp.navigate(config.targetUrl);
// Get page snapshot to find links
const snapshotResult = await this.mcp.snapshot();
// Parse links from snapshot (simplified)
const linkCount = 10; // Placeholder
console.log(` Found ${linkCount} links to check`);
// TODO: Implement actual link checking
this.results.links.passed = linkCount;
console.log(` ✅ All links OK`);
} catch (error) {
console.log(` ❌ Link test error: ${error.message}`);
this.results.links.failed++;
}
}
/**
* Form Testing
*/
async runFormTests() {
console.log('\n📝 Form Testing');
console.log('─────────────────────────────────────');
try {
await this.mcp.navigate(config.targetUrl);
// Get page snapshot to find forms
const snapshotResult = await this.mcp.snapshot();
console.log(` Checking form functionality...`);
// TODO: Implement actual form testing
this.results.forms.passed = 1;
console.log(` ✅ Forms tested`);
} catch (error) {
console.log(` ❌ Form test error: ${error.message}`);
this.results.forms.failed++;
}
}
/**
* Console Error Detection
*/
async runConsoleTests() {
console.log('\n💻 Console Error Detection');
console.log('─────────────────────────────────────');
try {
await this.mcp.navigate(config.targetUrl);
// Wait for page to fully load
await new Promise(resolve => setTimeout(resolve, 3000));
// Get console messages
const consoleResult = await this.mcp.consoleMessages('error');
// Parse console errors
if (consoleResult.result?.content) {
const errors = consoleResult.result.content;
if (Array.isArray(errors) && errors.length > 0) {
console.log(` ❌ Found ${errors.length} console errors:`);
for (const error of errors) {
console.log(` - ${error.slice(0, 80)}...`);
this.results.console.results.push({
type: 'error',
message: error,
});
}
this.results.console.failed = errors.length;
} else {
console.log(` ✅ No console errors`);
this.results.console.passed = 1;
}
} else {
console.log(` ✅ No console errors`);
this.results.console.passed = 1;
}
} catch (error) {
console.log(` ❌ Console test error: ${error.message}`);
this.results.console.failed++;
}
}
/**
* Generate HTML Report
*/
generateReport() {
console.log('\n📊 Generating Report...');
const totalPassed =
this.results.visual.passed +
this.results.links.passed +
this.results.forms.passed +
this.results.console.passed;
const totalFailed =
this.results.visual.failed +
this.results.links.failed +
this.results.forms.failed +
this.results.console.failed;
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Testing Report - ${new Date().toISOString()}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; border-bottom: 2px solid #333; padding-bottom: 10px; }
h2 { color: #555; margin-top: 30px; }
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 20px 0; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.card h3 { margin: 0 0 10px 0; }
.card .passed { color: #4caf50; font-size: 24px; font-weight: bold; }
.card .failed { color: #f44336; font-size: 24px; font-weight: bold; }
.section { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.pass { color: #4caf50; }
.fail { color: #f44336; }
.info { color: #2196f3; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f9f9f9; }
.timestamp { color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>🧪 Web Testing Report</h1>
<p class="timestamp">Generated: ${new Date().toISOString()}</p>
<p>Target: <code>${config.targetUrl}</code></p>
<div class="summary">
<div class="card">
<h3>📸 Visual</h3>
<div class="passed">${this.results.visual.passed}</div>
<div class="failed">${this.results.visual.failed} failed</div>
</div>
<div class="card">
<h3>🔗 Links</h3>
<div class="passed">${this.results.links.passed}</div>
<div class="failed">${this.results.links.failed} failed</div>
</div>
<div class="card">
<h3>📝 Forms</h3>
<div class="passed">${this.results.forms.passed}</div>
<div class="failed">${this.results.forms.failed} failed</div>
</div>
<div class="card">
<h3>💻 Console</h3>
<div class="passed">${this.results.console.passed}</div>
<div class="failed">${this.results.console.failed} failed</div>
</div>
</div>
<div class="section">
<h2>Visual Regression Results</h2>
<table>
<thead>
<tr>
<th>Viewport</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
${this.results.visual.results.map(r => `
<tr>
<td>${r.viewport}</td>
<td class="${r.status}">${r.status}</td>
<td><a href="screenshots/${r.filename}">${r.message}</a></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
${this.results.console.results.length > 0 ? `
<div class="section">
<h2>Console Errors</h2>
<table>
<thead>
<tr>
<th>Type</th>
<th>Message</th>
</tr>
</thead>
<tbody>
${this.results.console.results.map(r => `
<tr>
<td class="fail">${r.type}</td>
<td><code>${r.message}</code></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : ''}
<div class="section">
<h2>Summary</h2>
<p><strong>Total Passed:</strong> ${totalPassed}</p>
<p><strong>Total Failed:</strong> ${totalFailed}</p>
<p><strong>Success Rate:</strong> ${((totalPassed / (totalPassed + totalFailed)) * 100).toFixed(1)}%</p>
</div>
</div>
</body>
</html>
`;
const reportPath = path.join(config.reportsDir, 'web-test-report.html');
fs.writeFileSync(reportPath, html);
console.log(` ✅ Report saved: ${reportPath}`);
// Also save JSON
const jsonReport = {
timestamp: new Date().toISOString(),
config,
results: this.results,
summary: {
totalPassed,
totalFailed,
successRate: ((totalPassed / (totalPassed + totalFailed)) * 100).toFixed(1),
},
};
fs.writeFileSync(
path.join(config.reportsDir, 'web-test-report.json'),
JSON.stringify(jsonReport, null, 2)
);
}
}
// Main execution
async function main() {
const runner = new WebTestRunner();
try {
await runner.runAll();
const totalFailed =
runner.results.visual.failed +
runner.results.links.failed +
runner.results.forms.failed +
runner.results.console.failed;
console.log('\n═══════════════════════════════════════════════════');
console.log(' Tests Complete');
console.log('═══════════════════════════════════════════════════');
console.log(` Total Failed: ${totalFailed}`);
process.exit(totalFailed > 0 ? 1 : 0);
} catch (error) {
console.error('\n❌ Test runner failed:', error.message);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* Screenshot Capture Script for Visual Regression Testing
*
* Captures screenshots of web pages at multiple viewports using Playwright.
* Used to create baseline or current screenshots.
*
* Usage: node capture-screenshots.js [baseline|current]
* baseline - Save to tests/visual/baseline/
* current - Save to tests/visual/current/
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const { BASE_ARGS } = require('./lib/browser-launcher');
const TARGET_URL = process.env.TARGET_URL || 'http://host.docker.internal:3000';
const MODE = process.argv[2] || 'current';
const VIEWPORTS = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
];
const PAGES = [
{ name: 'homepage', path: '/' },
{ name: 'admin-login', path: '/admin/login' },
{ name: 'product', path: '/product.php?slug=domo-glamping-pvc-d5m' },
];
const SCREENSHOT_BASE = path.join(__dirname, '..', 'visual');
async function captureScreenshots() {
const outputDir = path.join(SCREENSHOT_BASE, MODE);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
console.log(`=== Screenshot Capture: ${MODE} ===\n`);
console.log(`Target URL: ${TARGET_URL}`);
console.log(`Output: ${outputDir}\n`);
const browser = await chromium.launch({
headless: true,
args: [...BASE_ARGS, '--disable-setuid-sandbox'],
});
let totalCaptured = 0;
let totalFailed = 0;
for (const page_config of PAGES) {
for (const viewport of VIEWPORTS) {
const filename = `${page_config.name}_${viewport.name}.png`;
const filePath = path.join(outputDir, filename);
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 1,
});
const page = await context.newPage();
try {
const url = `${TARGET_URL}${page_config.path}`;
console.log(` Capturing: ${url} [${viewport.name}]`);
await page.goto(url, { waitUntil: 'commit', timeout: 30000 });
await page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1000);
await page.screenshot({
path: filePath,
fullPage: true,
});
const fileSize = fs.statSync(filePath).size;
console.log(` ✅ Saved: ${filename} (${(fileSize / 1024).toFixed(1)} KB)`);
totalCaptured++;
} catch (error) {
console.log(` ❌ Failed: ${filename} - ${error.message}`);
totalFailed++;
} finally {
await context.close();
}
}
}
await browser.close();
console.log(`\n📊 Summary:`);
console.log(` Mode: ${MODE}`);
console.log(` ✅ Captured: ${totalCaptured}`);
console.log(` ❌ Failed: ${totalFailed}`);
console.log(` 📁 Output: ${outputDir}`);
process.exit(totalFailed > 0 ? 1 : 0);
}
captureScreenshots().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env node
/**
* Visual Regression Testing Script
*
* Compares current screenshots with baseline using pixelmatch
* Reports visual differences: overlaps, font shifts, color mismatches
*
* Usage: node compare-screenshots.js [options]
* Options:
* --threshold 0.05 - Pixel difference threshold (default: 5%)
* --baseline ./baseline - Baseline directory
* --current ./current - Current screenshots directory
* --diff ./diff - Diff output directory
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const config = {
baselineDir: process.env.BASELINE_DIR || './tests/visual/baseline',
currentDir: process.env.CURRENT_DIR || './tests/visual/current',
diffDir: process.env.DIFF_DIR || './tests/visual/diff',
reportsDir: process.env.REPORTS_DIR || './tests/reports',
threshold: parseFloat(process.env.PIXELMATCH_THRESHOLD || '0.05'),
};
// Ensure directories exist
[config.diffDir, config.reportsDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
/**
* Compare two PNG images using pixelmatch
*/
async function compareImages(baselinePath, currentPath, diffPath) {
const pixelmatch = require('pixelmatch');
const PNG = require('pngjs').PNG;
const baselineImg = PNG.sync.read(fs.readFileSync(baselinePath));
const currentImg = PNG.sync.read(fs.readFileSync(currentPath));
const { width, height } = baselineImg;
// Check if sizes match
if (width !== currentImg.width || height !== currentImg.height) {
return {
success: false,
error: `Size mismatch: baseline ${width}x${height} vs current ${currentImg.width}x${currentImg.height}`,
diffPixels: -1,
totalPixels: width * height,
};
}
// Create diff image
const diffImg = new PNG({ width, height });
// Compare
const diffPixels = pixelmatch(
baselineImg.data,
currentImg.data,
diffImg.data,
width,
height,
{
threshold: 0.1, // Pixel similarity threshold
diffColor: [255, 0, 0], // Red for differences
diffColorAlt: [255, 255, 0], // Yellow for anti-aliased
}
);
// Save diff image
fs.writeFileSync(diffPath, PNG.sync.write(diffImg));
const diffPercent = (diffPixels / (width * height)) * 100;
return {
success: diffPercent <= (config.threshold * 100),
diffPixels,
totalPixels: width * height,
diffPercent: diffPercent.toFixed(2),
width,
height,
};
}
/**
* Detect specific visual issues
*/
function detectVisualIssues(baselinePath, currentPath) {
// This would ideally use Playwright for element-level analysis
// For now, return generic analysis
return {
potentialIssues: [
'element_overlap',
'font_shift',
'color_mismatch',
'layout_break',
]
};
}
/**
* Get all PNG files from a directory
*/
function getPNGFiles(dir) {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.endsWith('.png'))
.map(f => path.basename(f, '.png'));
}
/**
* Main comparison function
*/
async function main() {
console.log('=== Visual Regression Testing ===\n');
console.log(`Baseline: ${config.baselineDir}`);
console.log(`Current: ${config.currentDir}`);
console.log(`Diff: ${config.diffDir}`);
console.log(`Threshold: ${config.threshold * 100}%\n`);
const baselineFiles = getPNGFiles(config.baselineDir);
const currentFiles = getPNGFiles(config.currentDir);
const results = [];
let passed = 0;
let failed = 0;
let missing = 0;
// Check for missing baselines
for (const file of currentFiles) {
if (!baselineFiles.includes(file)) {
console.log(`⚠️ New screenshot: ${file}`);
missing++;
results.push({
name: file,
status: 'NEW',
message: 'No baseline exists - will be created as baseline',
});
}
}
// Compare existing baselines
for (const file of baselineFiles) {
const baselinePath = path.join(config.baselineDir, `${file}.png`);
const currentPath = path.join(config.currentDir, `${file}.png`);
const diffPath = path.join(config.diffDir, `${file}_diff.png`);
if (!fs.existsSync(currentPath)) {
console.log(`❌ Missing: ${file}`);
failed++;
results.push({
name: file,
status: 'MISSING',
message: 'Current screenshot not found',
});
continue;
}
try {
console.log(`🔍 Comparing: ${file}...`);
const result = await compareImages(baselinePath, currentPath, diffPath);
if (result.success) {
console.log(`✅ PASS: ${file} (${result.diffPercent}% diff)`);
passed++;
} else {
console.log(`❌ FAIL: ${file} (${result.diffPercent}% diff)`);
console.log(` ${result.diffPixels} pixels changed of ${result.totalPixels}`);
failed++;
}
results.push({
name: file,
status: result.success ? 'PASS' : 'FAIL',
diffPercent: result.diffPercent,
diffPixels: result.diffPixels,
totalPixels: result.totalPixels,
width: result.width,
height: result.height,
diffPath: diffPath,
});
} catch (error) {
console.log(`❌ ERROR: ${file} - ${error.message}`);
failed++;
results.push({
name: file,
status: 'ERROR',
message: error.message,
});
}
}
// Generate report
const report = {
timestamp: new Date().toISOString(),
threshold: config.threshold,
summary: {
total: baselineFiles.length,
passed,
failed,
missing,
newScreenshots: missing,
},
results,
};
const reportPath = path.join(config.reportsDir, 'visual-regression-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📊 Summary:`);
console.log(` Total: ${baselineFiles.length}`);
console.log(` ✅ Pass: ${passed}`);
console.log(` ❌ Fail: ${failed}`);
console.log(` ⚠️ New: ${missing}`);
console.log(`\n📄 Report saved to: ${reportPath}`);
// Exit with error code if failures
process.exit(failed > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env node
/**
* Console Error Monitor (Standalone)
*
* Captures console errors from web pages using Playwright directly
* (no Playwright MCP dependency). Detects JS errors, network failures, warnings.
*
* Usage: node console-error-monitor-standalone.js
*
* Environment:
* TARGET_URL - App URL (default: http://host.docker.internal:3000)
* REPORTS_DIR - Reports output dir
* GITEA_ISSUE - Gitea issue number to post results (optional)
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const gitea = require('./lib/gitea-client');
const { BASE_ARGS } = require('./lib/browser-launcher');
const TARGET_URL = process.env.TARGET_URL || 'http://host.docker.internal:3000';
const REPORTS_DIR = process.env.REPORTS_DIR || path.join(__dirname, '..', 'reports');
const GITEA_ISSUE = parseInt(process.env.GITEA_ISSUE, 10) || null;
const PAGES = [
{ name: 'homepage', path: '/' },
{ name: 'admin-login', path: '/admin/login' },
];
const VIEWPORT = { width: 1280, height: 720 };
async function main() {
console.log('═══════════════════════════════════════════════════');
console.log(' Console Error Monitor (Standalone)');
console.log('═══════════════════════════════════════════════════\n');
console.log(`Target: ${TARGET_URL}\n`);
if (!fs.existsSync(REPORTS_DIR)) fs.mkdirSync(REPORTS_DIR, { recursive: true });
const browser = await chromium.launch({
headless: true,
args: [...BASE_ARGS, '--disable-setuid-sandbox'],
});
const allErrors = [];
const allWarnings = [];
const allNetworkErrors = [];
for (const pageConf of PAGES) {
const url = `${TARGET_URL}${pageConf.path}`;
console.log(`🔍 Checking: ${pageConf.name} (${url})`);
const context = await browser.newContext({ viewport: VIEWPORT, deviceScaleFactor: 1 });
const page = await context.newPage();
const consoleErrors = [];
const consoleWarnings = [];
const networkErrors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push({ text: msg.text(), location: msg.location() });
} else if (msg.type() === 'warning') {
consoleWarnings.push({ text: msg.text(), location: msg.location() });
}
});
page.on('requestfailed', request => {
networkErrors.push({
url: request.url(),
method: request.method(),
failure: request.failure()?.errorText || 'Unknown',
});
});
page.on('response', response => {
if (response.status() >= 400) {
networkErrors.push({
url: response.url(),
status: response.status(),
method: response.request().method(),
});
}
});
try {
const response = await page.goto(url, { waitUntil: 'commit', timeout: 30000 });
await page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
if (!response || response.status() >= 400) {
console.log(` ❌ HTTP ${response?.status() || 'no response'}`);
} else {
console.log(` ✅ HTTP ${response.status()}`);
}
} catch (err) {
console.log(` ❌ Navigation error: ${err.message}`);
}
if (consoleErrors.length > 0) {
console.log(` ❌ Console errors: ${consoleErrors.length}`);
consoleErrors.forEach(e => console.log(` - ${e.text.slice(0, 100)}`));
} else {
console.log(` ✅ No console errors`);
}
if (consoleWarnings.length > 0) {
console.log(` ⚠️ Console warnings: ${consoleWarnings.length}`);
consoleWarnings.forEach(w => console.log(` - ${w.text.slice(0, 100)}`));
}
if (networkErrors.length > 0) {
console.log(` ❌ Network errors: ${networkErrors.length}`);
networkErrors.forEach(e => console.log(` - ${e.status || e.failure} ${e.url.slice(0, 80)}`));
} else {
console.log(` ✅ No network errors`);
}
allErrors.push(...consoleErrors.map(e => ({ ...e, page: pageConf.name })));
allWarnings.push(...consoleWarnings.map(w => ({ ...w, page: pageConf.name })));
allNetworkErrors.push(...networkErrors.map(e => ({ ...e, page: pageConf.name })));
await context.close();
console.log('');
}
await browser.close();
const totalIssues = allErrors.length + allNetworkErrors.length;
const report = {
timestamp: new Date().toISOString(),
targetUrl: TARGET_URL,
pages: PAGES.map(p => p.name),
summary: {
consoleErrors: allErrors.length,
consoleWarnings: allWarnings.length,
networkErrors: allNetworkErrors.length,
totalIssues,
},
consoleErrors: allErrors,
consoleWarnings: allWarnings,
networkErrors: allNetworkErrors,
};
const reportPath = path.join(REPORTS_DIR, 'console-error-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log('═══════════════════════════════════════════════════');
console.log(` 📊 Results:`);
console.log(` Console errors: ${allErrors.length}`);
console.log(` Console warnings: ${allWarnings.length}`);
console.log(` Network errors: ${allNetworkErrors.length}`);
console.log(` Total issues: ${totalIssues}`);
console.log(` 📄 Report: ${reportPath}`);
console.log('═══════════════════════════════════════════════════\n');
if (GITEA_ISSUE) {
try {
console.log(`📤 Posting results to Gitea Issue #${GITEA_ISSUE}...`);
const commentBody = gitea.formatConsoleReport(report);
await gitea.postComment(GITEA_ISSUE, commentBody);
console.log(' ✅ Posted comment to Gitea');
} catch (err) {
console.error(` ❌ Gitea posting failed: ${err.message}`);
}
}
process.exit(totalIssues > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal:', err);
process.exit(1);
});

View File

@@ -0,0 +1,352 @@
#!/usr/bin/env node
/**
* Console Error Aggregator
*
* Collects all console errors from Playwright sessions
* Reports: error message, file, line number, stack trace
* Auto-creates Gitea Issues for critical errors
*/
const http = require('http');
const https = require('https');
const { URL } = require('url');
// Configuration
const config = {
playwrightMcpUrl: process.env.PLAYWRIGHT_MCP_URL || 'http://localhost:8931/mcp',
giteaApiUrl: process.env.GITEA_API_URL || 'https://git.softuniq.eu/api/v1',
giteaToken: process.env.GITEA_TOKEN || '',
giteaRepo: process.env.GITEA_REPO || 'UniqueSoft/APAW',
targetUrl: process.env.TARGET_URL || 'http://localhost:3000',
reportsDir: process.env.REPORTS_DIR || './reports',
autoCreateIssues: process.env.AUTO_CREATE_ISSUES === 'true',
ignoredPatterns: (process.env.IGNORED_ERROR_PATTERNS || '').split(','),
};
/**
* Make HTTP request to Playwright MCP
*/
async function mcpRequest(method, params) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method,
params,
});
const url = new URL(config.playwrightMcpUrl);
const req = http.request({
hostname: url.hostname,
port: url.port || 8931,
path: '/mcp',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
});
req.on('error', reject);
req.write(body);
req.end();
});
}
/**
* Navigate to URL
*/
async function navigateTo(url) {
return mcpRequest('tools/call', {
name: 'browser_navigate',
arguments: { url },
});
}
/**
* Get console messages
*/
async function getConsoleMessages(level = 'error', all = true) {
return mcpRequest('tools/call', {
name: 'browser_console_messages',
arguments: { level, all },
});
}
/**
* Get network requests (for failed requests)
*/
async function getNetworkRequests(filter = 'failed') {
return mcpRequest('tools/call', {
name: 'browser_network_requests',
arguments: { filter },
});
}
/**
* Take screenshot for error context
*/
async function takeScreenshot(filename) {
return mcpRequest('tools/call', {
name: 'browser_take_screenshot',
arguments: { filename },
});
}
/**
* Parse console error to extract file and line number
*/
function parseErrorDetails(error) {
const result = {
message: error,
file: null,
line: null,
column: null,
stack: [],
};
// Try to parse stack trace
const stackMatch = error.match(/at\s+(?:(.+)\s+\()?([^:]+):(\d+):(\d+)\)?/);
if (stackMatch) {
result.file = stackMatch[2];
result.line = parseInt(stackMatch[3]);
result.column = parseInt(stackMatch[4]);
}
// Parse Chrome-style stack traces
const chromePattern = /at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/g;
let match;
while ((match = chromePattern.exec(error)) !== null) {
result.stack.push({
function: match[1],
file: match[2],
line: parseInt(match[3]),
column: parseInt(match[4]),
});
}
return result;
}
/**
* Check if error should be ignored
*/
function shouldIgnoreError(error) {
const message = error.message || error;
return config.ignoredPatterns.some(pattern =>
pattern && message.includes(pattern)
);
}
/**
* Create Gitea Issue for error
*/
async function createGiteaIssue(errorData) {
if (!config.giteaToken || !config.autoCreateIssues) {
return null;
}
const fs = require('fs');
const path = require('path');
const title = `[Console Error] ${errorData.parsed.message.slice(0, 100)}`;
const body = `## Console Error
**Error Type**: ${errorData.type}
**Message**:
\`\`\`
${errorData.parsed.message}
\`\`\`
**Location**: ${errorData.parsed.file || 'Unknown'}:${errorData.parsed.line || '?'}
**Page URL**: ${errorData.pageUrl}
### Stack Trace
\`\`\`
${errorData.parsed.stack.map(s => `${s.function} (${s.file}:${s.line}:${s.column})`).join('\n') || 'No stack trace available'}
\`\`\`
## Auto-Fix Required
- [ ] Investigate the root cause
- [ ] Implement fix
- [ ] Add test case
- [ ] Verify fix
---
**Detected by**: Kilo Code Web Testing
`;
return new Promise((resolve, reject) => {
const url = new URL(`${config.giteaApiUrl}/repos/${config.giteaRepo}/issues`);
const bodyData = JSON.stringify({ title, body });
const client = url.protocol === 'https:' ? https : http;
const req = client.request({
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `token ${config.giteaToken}`,
'Content-Length': Buffer.byteLength(bodyData),
},
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.write(bodyData);
req.end();
});
}
/**
* Main console monitoring function
*/
async function main() {
console.log('=== Console Error Monitor ===\n');
console.log(`Target URL: ${config.targetUrl}`);
console.log(`Auto-create Issues: ${config.autoCreateIssues}\n`);
const errors = {
consoleErrors: [],
networkErrors: [],
uncaughtExceptions: [],
};
try {
// Navigate to target
console.log('📡 Navigating to target URL...');
await navigateTo(config.targetUrl);
// Wait a bit for page to load
await new Promise(resolve => setTimeout(resolve, 2000));
// Get console messages
console.log('🔍 Collecting console messages...');
const consoleResult = await getConsoleMessages('error', true);
if (consoleResult.result?.content) {
const messages = consoleResult.result.content;
for (const msg of messages) {
if (shouldIgnoreError(msg)) {
console.log(' ⏭️ Ignored:', msg.slice(0, 80));
continue;
}
const parsed = parseErrorDetails(msg);
const errorData = {
type: 'console',
message: msg,
parsed,
pageUrl: config.targetUrl,
timestamp: new Date().toISOString(),
};
errors.consoleErrors.push(errorData);
console.log(' ❌ Console Error:', msg.slice(0, 80));
}
}
// Get failed network requests
console.log('🔍 Checking network requests...');
const networkResult = await getNetworkRequests('failed');
if (networkResult.result?.content) {
for (const req of networkResult.result.content) {
if (req.status >= 400) {
errors.networkErrors.push({
type: 'network',
url: req.url,
status: req.status,
method: req.method,
pageUrl: config.targetUrl,
timestamp: new Date().toISOString(),
});
console.log(` ❌ Network Error: ${req.status} ${req.url}`);
}
}
}
// Take screenshot for context
const screenshotFilename = `error-context-${Date.now()}.png`;
await takeScreenshot(screenshotFilename);
console.log(`📸 Screenshot saved: ${screenshotFilename}`);
// Create Gitea Issues for critical errors
if (config.autoCreateIssues) {
console.log('\n📝 Creating Gitea Issues...');
for (const error of errors.consoleErrors) {
try {
const issue = await createGiteaIssue(error);
error.giteaIssue = issue?.html_url || null;
if (issue) {
console.log(` ✅ Issue created: ${issue.html_url}`);
error.issueNumber = issue.number;
}
} catch (err) {
console.log(` ❌ Failed to create issue: ${err.message}`);
}
}
}
} catch (error) {
console.error('Error during monitoring:', error.message);
}
// Generate report
const fs = require('fs');
const path = require('path');
const report = {
timestamp: new Date().toISOString(),
config: {
targetUrl: config.targetUrl,
autoCreateIssues: config.autoCreateIssues,
},
summary: {
consoleErrors: errors.consoleErrors.length,
networkErrors: errors.networkErrors.length,
totalErrors: errors.consoleErrors.length + errors.networkErrors.length,
},
errors,
};
const reportPath = path.join(config.reportsDir, 'console-errors-report.json');
if (!fs.existsSync(config.reportsDir)) {
fs.mkdirSync(config.reportsDir, { recursive: true });
}
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log('\n📊 Summary:');
console.log(` Console Errors: ${errors.consoleErrors.length}`);
console.log(` Network Errors: ${errors.networkErrors.length}`);
console.log(` Total Errors: ${report.summary.totalErrors}`);
console.log(`\n📄 Report saved to: ${reportPath}`);
// Exit with error if errors found
process.exit(report.summary.totalErrors > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,64 @@
/**
* Shared browser launch configuration and navigation helpers.
*
* Fixes:
* - DNS resolution inside Docker (--dns-resolution-order=hostname-first)
* - Slow sites: uses waitUntil: 'commit' + waitForLoadState instead of 'networkidle'
* - UA fingerprinting: realistic Chrome user agent
*
* Usage:
* const { launchBrowser, navigateTo } = require('./lib/browser-launcher');
* const browser = await launchBrowser();
* const page = ...;
* await navigateTo(page, 'https://example.com');
*/
const { chromium } = require('playwright');
const USE_DNS_FIX = process.env.DNS_RESOLUTION_ORDER === 'hostname-first';
const BASE_ARGS = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
...(USE_DNS_FIX ? ['--dns-resolution-order=hostname-first'] : []),
];
const DEFAULT_UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
async function launchBrowser(options = {}) {
const args = [...BASE_ARGS, ...(options.extraArgs || [])];
return chromium.launch({
headless: options.headless !== undefined ? options.headless : true,
args,
});
}
async function newContext(browser, options = {}) {
return browser.newContext({
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1,
userAgent: DEFAULT_UA,
...options,
});
}
async function navigateTo(page, url, options = {}) {
const waitUntil = options.waitUntil || 'commit';
const timeout = options.timeout || 60000;
const response = await page.goto(url, { waitUntil, timeout });
if (options.waitForDom !== false) {
await page.waitForLoadState('domcontentloaded', { timeout: options.domTimeout || 15000 }).catch(() => {});
}
const delay = options.delay || 2000;
if (delay > 0) await page.waitForTimeout(delay);
return response;
}
module.exports = { launchBrowser, newContext, navigateTo, BASE_ARGS, DEFAULT_UA };

View File

@@ -0,0 +1,263 @@
/**
* Gitea API Client — Lightweight helper for posting test results to Gitea Issues.
*
* Auth flow: Basic Auth → create token → use token for API calls.
*
* Usage:
* const gitea = require('./lib/gitea-client');
* await gitea.postComment(issueNumber, body);
* await gitea.uploadAttachment(issueNumber, filePath);
*
* Environment:
* GITEA_API_URL - API base (default: https://git.softuniq.eu/api/v1)
* GITEA_TOKEN - Pre-existing API token (skips Basic Auth if set)
* GITEA_USER - Username for Basic Auth (default: NW)
* GITEA_PASSWORD - Password for Basic Auth (required if no token)
* GITEA_REPO - Repository path (default: UniqueSoft/APAW)
*/
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const GITEA_API_URL = process.env.GITEA_API_URL || 'https://git.softuniq.eu/api/v1';
const GITEA_USER = process.env.GITEA_USER || '';
const GITEA_PASSWORD = process.env.GITEA_PASSWORD || '';
const GITEA_REPO = process.env.GITEA_REPO || 'UniqueSoft/APAW';
let _cachedToken = process.env.GITEA_TOKEN || null;
function request(urlStr, options, body) {
return new Promise((resolve, reject) => {
const url = new URL(urlStr);
const mod = url.protocol === 'https:' ? https : http;
const opts = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: options.method || 'GET',
headers: options.headers || {},
};
const req = mod.request(opts, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try { resolve(JSON.parse(data)); } catch { resolve(data); }
} else {
reject(new Error(`Gitea API ${res.statusCode}: ${data.slice(0, 300)}`));
}
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
async function getToken() {
if (_cachedToken) return _cachedToken;
const credentials = Buffer.from(`${GITEA_USER}:${GITEA_PASSWORD}`).toString('base64');
const urlStr = `${GITEA_API_URL}/users/${GITEA_USER}/tokens`;
const body = JSON.stringify({ name: `vt-${Date.now()}`, scopes: ['all'] });
const result = await request(urlStr, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${credentials}`,
},
}, body);
_cachedToken = result.sha1;
return _cachedToken;
}
async function authHeaders() {
const token = await getToken();
return { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' };
}
async function postComment(issueNumber, body) {
const headers = await authHeaders();
const url = `${GITEA_API_URL}/repos/${GITEA_REPO}/issues/${issueNumber}/comments`;
return request(url, { method: 'POST', headers }, JSON.stringify({ body }));
}
async function uploadAttachment(issueNumber, filePath) {
const token = await getToken();
const fileContent = fs.readFileSync(filePath);
const filename = path.basename(filePath);
const boundary = `----FormBoundary${Date.now()}`;
let body = `--${boundary}\r\n`.getBytes?.() || Buffer.from(`--${boundary}\r\n`);
body = Buffer.concat([
Buffer.from(`--${boundary}\r\n`),
Buffer.from(`Content-Disposition: form-data; name="attachment"; filename="${filename}"\r\n`),
Buffer.from(`Content-Type: image/png\r\n\r\n`),
fileContent,
Buffer.from(`\r\n--${boundary}--\r\n`),
]);
const url = new URL(`${GITEA_API_URL}/repos/${GITEA_REPO}/issues/${issueNumber}/assets`);
const mod = url.protocol === 'https:' ? https : http;
return new Promise((resolve, reject) => {
const req = mod.request({
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Authorization': `token ${token}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length,
},
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try { resolve(JSON.parse(data)); } catch { resolve(data); }
} else {
reject(new Error(`Gitea upload ${res.statusCode}: ${data.slice(0, 300)}`));
}
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
async function uploadAndComment(issueNumber, filePaths, commentBody) {
const uuids = [];
for (const fp of filePaths) {
try {
const result = await uploadAttachment(issueNumber, fp);
uuids.push({ filename: path.basename(fp), uuid: result.uuid });
} catch (err) {
console.error(` ⚠️ Upload failed ${path.basename(fp)}: ${err.message}`);
}
}
let fullBody = commentBody;
if (uuids.length > 0) {
fullBody += '\n\n### 📸 Screenshots\n\n';
for (const u of uuids) {
fullBody += `![${u.filename}](/attachments/${u.uuid})\n`;
}
}
return postComment(issueNumber, fullBody);
}
function formatVisualReport(report) {
const s = report.summary;
const lines = [
'## 📊 Visual Test Results',
'',
`**URL**: \`${report.targetUrl}\``,
`**Pages**: ${report.pages.join(', ')}`,
`**Viewports**: ${report.viewports.join(', ')}`,
`**Threshold**: ${report.threshold * 100}%`,
'',
'### Summary',
'',
`| Metric | Count |`,
`|--------|-------|`,
`| Screenshots captured | ${s.screenshotsCaptured} |`,
`| Screenshots failed | ${s.screenshotsFailed} |`,
`| Comparisons passed | ${s.comparisonsPassed} |`,
`| Comparisons failed | ${s.comparisonsFailed} |`,
`| UI elements extracted | ${s.totalElements} |`,
`| Console errors | ${s.totalConsoleErrors} |`,
`| Network errors | ${s.totalNetworkErrors} |`,
'',
`**Overall**: ${s.overallPassed ? '✅ PASSED' : '❌ FAILED'}`,
];
if (report.comparison?.length) {
lines.push('', '### Comparison Details', '');
lines.push('| Screenshot | Status | Diff % |');
lines.push('|------------|--------|--------|');
for (const c of report.comparison) {
lines.push(`| ${c.filename} | ${c.status === 'PASS' ? '✅' : '❌'} ${c.status} | ${c.diffPercent || 'N/A'} |`);
}
}
if (report.consoleErrors?.length > 0) {
lines.push('', '### Console Errors', '');
for (const e of report.consoleErrors.slice(0, 5)) {
lines.push(`- [${e.page}/${e.viewport}] ${e.error?.slice(0, 120)}`);
}
if (report.consoleErrors.length > 5) {
lines.push(`- ... and ${report.consoleErrors.length - 5} more`);
}
}
if (report.networkErrors?.length > 0) {
lines.push('', '### Network Errors', '');
for (const e of report.networkErrors.slice(0, 5)) {
lines.push(`- [${e.page}/${e.viewport}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
}
if (report.networkErrors.length > 5) {
lines.push(`- ... and ${report.networkErrors.length - 5} more`);
}
}
return lines.join('\n');
}
function formatConsoleReport(report) {
const s = report.summary;
const lines = [
'## 📊 Console Error Monitor Results',
'',
`**URL**: \`${report.targetUrl}\``,
`**Pages**: ${report.pages.join(', ')}`,
'',
'### Summary',
'',
`| Metric | Count |`,
`|--------|-------|`,
`| Console errors | ${s.consoleErrors} |`,
`| Console warnings | ${s.consoleWarnings} |`,
`| Network errors | ${s.networkErrors} |`,
`| **Total issues** | **${s.totalIssues}** |`,
'',
`**Status**: ${s.totalIssues === 0 ? '✅ CLEAN' : '❌ ISSUES FOUND'}`,
];
if (report.consoleErrors?.length > 0) {
lines.push('', '### Console Errors', '');
for (const e of report.consoleErrors.slice(0, 8)) {
lines.push(`- [${e.page}] ${e.text?.slice(0, 120)}`);
}
if (report.consoleErrors.length > 8) {
lines.push(`- ... and ${report.consoleErrors.length - 8} more`);
}
}
if (report.networkErrors?.length > 0) {
lines.push('', '### Network Errors', '');
for (const e of report.networkErrors.slice(0, 8)) {
lines.push(`- [${e.page}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
}
if (report.networkErrors.length > 8) {
lines.push(`- ... and ${report.networkErrors.length - 8} more`);
}
}
return lines.join('\n');
}
module.exports = {
postComment,
uploadAttachment,
uploadAndComment,
formatVisualReport,
formatConsoleReport,
};

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env node
/**
* Link Checker Script for Web Applications
*
* Finds all links on pages and checks for broken ones (404, 500, etc.)
* Reports broken links with context (page URL, link text)
*/
const http = require('http');
const https = require('https');
const { URL } = require('url');
// Playwright MCP endpoint
const MCP_ENDPOINT = process.env.PLAYWRIGHT_MCP_URL || 'http://localhost:8931/mcp';
// Configuration
const config = {
targetUrl: process.env.TARGET_URL || 'http://localhost:3000',
maxDepth: parseInt(process.env.MAX_DEPTH || '2'),
timeout: parseInt(process.env.TIMEOUT || '5000'),
concurrency: parseInt(process.env.CONCURRENCY || '5'),
ignorePatterns: (process.env.IGNORE_PATTERNS || '').split(','),
reportsDir: process.env.REPORTS_DIR || './reports',
};
/**
* Make HTTP request to Playwright MCP
*/
async function mcpRequest(method, params) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method,
params,
});
const url = new URL(MCP_ENDPOINT);
const options = {
hostname: url.hostname,
port: url.port,
path: url.path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
};
const client = url.protocol === 'https:' ? https : http;
const req = client.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.setTimeout(config.timeout, () => {
req.destroy();
reject(new Error('Timeout'));
});
req.write(body);
req.end();
});
}
/**
* Navigate to URL using Playwright MCP
*/
async function navigateTo(url) {
const result = await mcpRequest('tools/call', {
name: 'browser_navigate',
arguments: { url },
});
return result;
}
/**
* Get page snapshot with all links
*/
async function getPageSnapshot() {
const result = await mcpRequest('tools/call', {
name: 'browser_snapshot',
arguments: {},
});
return result;
}
/**
* Extract links from accessibility tree
*/
function extractLinks(snapshot) {
// Parse accessibility tree for links
const links = [];
// This would parse the snapshot content returned by Playwright MCP
// For now, return placeholder
return links;
}
/**
* Check if a URL is valid
*/
async function checkUrl(url, baseUrl) {
return new Promise((resolve) => {
try {
const parsedUrl = new URL(url, baseUrl);
// Skip anchor links
if (url.startsWith('#')) {
resolve({ url, status: 'SKIP', message: 'Anchor link' });
return;
}
// Skip mailto and tel links
if (parsedUrl.protocol === 'mailto:' || parsedUrl.protocol === 'tel:') {
resolve({ url, status: 'SKIP', message: 'Non-HTTP protocol' });
return;
}
// Check ignore patterns
for (const pattern of config.ignorePatterns) {
if (pattern && url.includes(pattern)) {
resolve({ url, status: 'SKIP', message: 'Ignored pattern' });
return;
}
}
// Make HEAD request to check URL
const client = parsedUrl.protocol === 'https:' ? https : http;
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
method: 'HEAD',
timeout: config.timeout,
};
const req = client.request(options, (res) => {
resolve({
url,
status: res.statusCode >= 400 ? 'BROKEN' : 'OK',
statusCode: res.statusCode,
});
});
req.on('error', (err) => {
resolve({ url, status: 'ERROR', message: err.message });
});
req.on('timeout', () => {
req.destroy();
resolve({ url, status: 'TIMEOUT', message: 'Request timed out' });
});
req.end();
} catch (err) {
resolve({ url, status: 'ERROR', message: err.message });
}
});
}
/**
* Main link checking function
*/
async function main() {
console.log('=== Link Checker ===\n');
console.log(`Target URL: ${config.targetUrl}`);
console.log(`Max Depth: ${config.maxDepth}\n`);
const visitedUrls = new Set();
const brokenLinks = [];
const allLinks = [];
// Connect to Playwright MCP
console.log('📡 Connecting to Playwright MCP...');
// Start with target URL
const toVisit = [config.targetUrl];
while (toVisit.length > 0) {
const url = toVisit.shift();
if (visitedUrls.has(url)) {
continue;
}
visitedUrls.add(url);
console.log(`🔍 Checking: ${url}`);
try {
// Navigate to URL
await navigateTo(url);
// Get page content
const snapshot = await getPageSnapshot();
const links = extractLinks(snapshot);
// Check each link
for (const link of links) {
const result = await checkUrl(link.href, url);
allLinks.push({
sourcePage: url,
linkText: link.text || '[no text]',
href: link.href,
...result,
});
if (result.status === 'BROKEN' || result.status === 'ERROR') {
brokenLinks.push(allLinks[allLinks.length - 1]);
console.log(`${link.href} - ${result.statusCode || result.message}`);
} else {
console.log(`${link.href}`);
}
// Add to visit queue if same origin
if (result.status === 'OK') {
try {
const parsedUrl = new URL(link.href, config.targetUrl);
const parsedBaseUrl = new URL(config.targetUrl);
if (parsedUrl.origin === parsedBaseUrl.origin) {
toVisit.push(link.href);
}
} catch (e) {
// Skip invalid URLs
}
}
}
} catch (error) {
console.log(`❌ Error checking ${url}: ${error.message}`);
brokenLinks.push({
sourcePage: url,
href: url,
status: 'ERROR',
message: error.message,
});
}
}
// Generate report
const report = {
timestamp: new Date().toISOString(),
config,
summary: {
totalLinks: allLinks.length,
brokenLinks: brokenLinks.length,
pagesChecked: visitedUrls.size,
},
allLinks,
brokenLinks,
};
const fs = require('fs');
const path = require('path');
const reportPath = path.join(config.reportsDir, 'link-check-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📊 Summary:`);
console.log(` Pages Checked: ${visitedUrls.size}`);
console.log(` Total Links: ${allLinks.length}`);
console.log(` Broken Links: ${brokenLinks.length}`);
console.log(`\n📄 Report saved to: ${reportPath}`);
// Exit with error if broken links found
process.exit(brokenLinks.length > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,357 @@
#!/usr/bin/env node
/**
* Visual Test Pipeline — Full Analysis
*
* Captures screenshots, extracts UI elements with bounding boxes,
* detects console errors, and compares against baselines.
*
* Usage: node visual-test-pipeline.js [URL]
*
* Environment:
* TARGET_URL - App URL (default: http://host.docker.internal:3000)
* PIXELMATCH_THRESHOLD - Diff threshold (default: 0.05 = 5%)
* PAGES - Comma-separated page paths (default: /,/admin/login)
* GITEA_ISSUE - Gitea issue number to post results (optional)
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const gitea = require('./lib/gitea-client');
const { BASE_ARGS } = require('./lib/browser-launcher');
const TARGET_URL = process.argv[2] || process.env.TARGET_URL || 'http://host.docker.internal:3000';
const THRESHOLD = parseFloat(process.env.PIXELMATCH_THRESHOLD || '0.05');
const PAGES_ARG = process.env.PAGES || '/,/admin/login';
const PAGE_PATHS = PAGES_ARG.split(',').map(p => p.trim()).filter(Boolean);
const GITEA_ISSUE = parseInt(process.env.GITEA_ISSUE, 10) || null;
const VISUAL_DIR = path.join(__dirname, '..', 'visual');
const BASELINE_DIR = path.join(VISUAL_DIR, 'baseline');
const CURRENT_DIR = path.join(VISUAL_DIR, 'current');
const DIFF_DIR = path.join(VISUAL_DIR, 'diff');
const REPORTS_DIR = path.join(__dirname, '..', 'reports');
const VIEWPORTS = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
];
function pageNameFromPath(p) {
if (p === '/' || p === '') return 'homepage';
return p.replace(/^\//, '').replace(/[\/\.]/g, '-');
}
const PAGES = PAGE_PATHS.map(p => ({ name: pageNameFromPath(p), path: p.startsWith('/') ? p : '/' + p }));
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
/**
* Extract UI elements with bounding boxes from page
*/
async function extractElements(page) {
return await page.evaluate(() => {
const elements = [];
const seen = new Set();
function processNode(node) {
if (node.nodeType !== 1) return;
const tag = node.tagName.toLowerCase();
const skipTags = new Set(['script','style','link','meta','noscript','svg','path','br','hr','wbr']);
if (skipTags.has(tag)) return;
const rect = node.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) return;
const id = `${tag}-` + (node.id || '') + '-' + Math.random().toString(36).slice(2, 8);
if (seen.has(id)) return;
seen.add(id);
const styles = window.getComputedStyle(node);
const el = {
tag,
id: node.id || null,
className: node.className?.toString()?.slice(0, 120) || null,
text: (node.textContent || '').slice(0, 80).trim() || null,
href: node.href || null,
type: node.type || null,
placeholder: node.placeholder || null,
role: node.getAttribute('role') || null,
ariaLabel: node.getAttribute('aria-label') || null,
visible: styles.display !== 'none' && styles.visibility !== 'hidden' && styles.opacity !== '0',
bbox: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
},
};
elements.push(el);
}
function walk(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
let node;
while (node = walker.nextNode()) processNode(node);
}
walk(document.body);
return elements;
});
}
/**
* Capture screenshots and extract elements for a single page+viewport
*/
async function capturePage(browser, pageConf, vp, outputDir, mode) {
const filename = `${pageConf.name}_${vp.name}.png`;
const filePath = path.join(outputDir, filename);
const url = `${TARGET_URL}${pageConf.path}`;
const context = await browser.newContext({
viewport: { width: vp.width, height: vp.height },
deviceScaleFactor: 1,
});
const page = await context.newPage();
const consoleErrors = [];
const networkErrors = [];
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
page.on('response', resp => {
if (resp.status() >= 400) networkErrors.push({ url: resp.url(), status: resp.status() });
});
page.on('requestfailed', req => {
networkErrors.push({ url: req.url(), failure: req.failure()?.errorText || 'failed' });
});
try {
console.log(` Capturing: ${pageConf.name} @ ${vp.name} (${vp.width}x${vp.height})`);
const response = await page.goto(url, { waitUntil: 'commit', timeout: 30000 });
await page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1500);
await page.screenshot({ path: filePath, fullPage: true });
const fileSize = fs.statSync(filePath).size;
const elements = await extractElements(page);
const title = await page.title();
console.log(`${filename} (${(fileSize / 1024).toFixed(1)} KB, ${elements.length} elements)`);
return {
filename, page: pageConf.name, viewport: vp.name, status: 'PASS', size: fileSize,
url, httpStatus: response?.status() || null, title,
elements, consoleErrors, networkErrors,
};
} catch (err) {
console.log(`${filename}: ${err.message}`);
return { filename, page: pageConf.name, viewport: vp.name, status: 'FAIL', error: err.message, elements: [], consoleErrors, networkErrors: [] };
} finally {
await context.close();
}
}
async function captureAll(mode) {
ensureDir(mode === 'baseline' ? BASELINE_DIR : CURRENT_DIR);
const outputDir = mode === 'baseline' ? BASELINE_DIR : CURRENT_DIR;
console.log(`\n📸 Capturing ${mode} screenshots...`);
console.log(` Target: ${TARGET_URL}`);
console.log(` Pages: ${PAGES.map(p => p.path).join(', ')}`);
console.log(` Output: ${outputDir}\n`);
const browser = await chromium.launch({ headless: true, args: [...BASE_ARGS, '--disable-setuid-sandbox'] });
const results = [];
for (const pageConf of PAGES) {
for (const vp of VIEWPORTS) {
const r = await capturePage(browser, pageConf, vp, outputDir, mode);
results.push(r);
}
}
await browser.close();
return results;
}
async function compareScreenshots() {
const pixelmatch = require('pixelmatch');
const PNG = require('pngjs').PNG;
ensureDir(DIFF_DIR);
console.log(`\n🔍 Comparing screenshots (threshold: ${THRESHOLD * 100}%)...\n`);
const baselines = fs.existsSync(BASELINE_DIR)
? fs.readdirSync(BASELINE_DIR).filter(f => f.endsWith('.png'))
: [];
const results = [];
let passed = 0, failed = 0;
for (const file of baselines) {
const currentPath = path.join(CURRENT_DIR, file);
const diffPath = path.join(DIFF_DIR, file.replace('.png', '_diff.png'));
if (!fs.existsSync(currentPath)) {
console.log(` ⚠️ Missing current: ${file}`);
results.push({ filename: file, status: 'MISSING', diffPercent: null });
failed++;
continue;
}
try {
const baselineImg = PNG.sync.read(fs.readFileSync(path.join(BASELINE_DIR, file)));
const currentImg = PNG.sync.read(fs.readFileSync(currentPath));
const { width, height } = baselineImg;
if (width !== currentImg.width || height !== currentImg.height) {
console.log(` ❌ Size mismatch: ${file}`);
results.push({ filename: file, status: 'SIZE_MISMATCH', diffPercent: null });
failed++;
continue;
}
const diffImg = new PNG({ width, height });
const diffPixels = pixelmatch(baselineImg.data, currentImg.data, diffImg.data, width, height, { threshold: 0.1, diffColor: [255, 0, 0] });
fs.writeFileSync(diffPath, PNG.sync.write(diffImg));
const diffPercent = (diffPixels / (width * height)) * 100;
const ok = diffPercent <= THRESHOLD * 100;
ok ? passed++ : failed++;
console.log(` ${ok ? '✅' : '❌'} ${file}: ${diffPercent.toFixed(2)}% diff`);
results.push({ filename: file, status: ok ? 'PASS' : 'FAIL', diffPercent: diffPercent.toFixed(2), diffPixels, totalPixels: width * height });
} catch (err) {
console.log(` ❌ Error: ${file}: ${err.message}`);
results.push({ filename: file, status: 'ERROR', error: err.message });
failed++;
}
}
return { results, passed, failed };
}
async function main() {
console.log('═══════════════════════════════════════════════════');
console.log(' Visual Test Pipeline — Full Analysis');
console.log('═══════════════════════════════════════════════════\n');
ensureDir(REPORTS_DIR);
const hasBaselines = fs.existsSync(BASELINE_DIR) &&
fs.readdirSync(BASELINE_DIR).filter(f => f.endsWith('.png')).length > 0;
if (!hasBaselines) {
console.log('⚠️ No baselines — capturing baseline screenshots first.\n');
await captureAll('baseline');
console.log('\n✅ Baselines created. Now capturing current screenshots.\n');
}
const captureResults = await captureAll('current');
const compareResult = await compareScreenshots();
const allElements = {};
const allConsoleErrors = [];
const allNetworkErrors = [];
for (const r of captureResults) {
const key = `${r.page}_${r.viewport}`;
allElements[key] = r.elements || [];
if (r.consoleErrors?.length) allConsoleErrors.push(...r.consoleErrors.map(e => ({ page: r.page, viewport: r.viewport, error: e })));
if (r.networkErrors?.length) allNetworkErrors.push(...r.networkErrors.map(e => ({ page: r.page, viewport: r.viewport, ...e })));
}
const report = {
timestamp: new Date().toISOString(),
targetUrl: TARGET_URL,
pages: PAGES.map(p => p.path),
viewports: VIEWPORTS.map(v => v.name),
threshold: THRESHOLD,
summary: {
screenshotsCaptured: captureResults.filter(r => r.status === 'PASS').length,
screenshotsFailed: captureResults.filter(r => r.status === 'FAIL').length,
comparisonsPassed: compareResult.passed,
comparisonsFailed: compareResult.failed,
totalElements: Object.values(allElements).reduce((s, a) => s + a.length, 0),
totalConsoleErrors: allConsoleErrors.length,
totalNetworkErrors: allNetworkErrors.length,
overallPassed: compareResult.passed >= compareResult.failed && captureResults.filter(r => r.status === 'FAIL').length === 0,
},
capture: captureResults.map(r => ({
filename: r.filename, page: r.page, viewport: r.viewport, status: r.status,
httpStatus: r.httpStatus, title: r.title,
elementCount: r.elements?.length || 0,
consoleErrorCount: r.consoleErrors?.length || 0,
networkErrorCount: r.networkErrors?.length || 0,
})),
elements: allElements,
consoleErrors: allConsoleErrors,
networkErrors: allNetworkErrors,
comparison: compareResult.results,
};
const reportPath = path.join(REPORTS_DIR, 'visual-test-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log('\n═══════════════════════════════════════════════════');
console.log(` 📊 RESULTS SUMMARY`);
console.log(` ─────────────────────────────────────────────────`);
console.log(` Screenshots: ${report.summary.screenshotsCaptured} captured, ${report.summary.screenshotsFailed} failed`);
console.log(` Elements: ${report.summary.totalElements}`);
console.log(` Comparison: ${compareResult.passed} passed, ${compareResult.failed} failed`);
console.log(` Console Errs: ${allConsoleErrors.length}`);
console.log(` Network Errs: ${allNetworkErrors.length}`);
if (allConsoleErrors.length > 0) {
console.log(`\n ❌ Console Errors:`);
for (const e of allConsoleErrors.slice(0, 10)) {
console.log(` [${e.page}/${e.viewport}] ${e.error.slice(0, 120)}`);
}
}
if (allNetworkErrors.length > 0) {
console.log(`\n ❌ Network Errors:`);
for (const e of allNetworkErrors.slice(0, 10)) {
console.log(` [${e.page}/${e.viewport}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
}
}
console.log(`\n 📄 Report: ${reportPath}`);
console.log('═══════════════════════════════════════════════════\n');
if (GITEA_ISSUE) {
try {
console.log(`📤 Posting results to Gitea Issue #${GITEA_ISSUE}...`);
const commentBody = gitea.formatVisualReport(report);
const diffFiles = fs.existsSync(DIFF_DIR)
? fs.readdirSync(DIFF_DIR).filter(f => f.endsWith('.png')).map(f => path.join(DIFF_DIR, f))
: [];
const currentFiles = fs.existsSync(CURRENT_DIR)
? fs.readdirSync(CURRENT_DIR).filter(f => f.endsWith('.png')).map(f => path.join(CURRENT_DIR, f))
: [];
if (diffFiles.length > 0) {
await gitea.uploadAndComment(GITEA_ISSUE, diffFiles, commentBody);
console.log(` ✅ Posted comment with ${diffFiles.length} diff screenshots`);
} else if (currentFiles.length > 0) {
await gitea.uploadAndComment(GITEA_ISSUE, currentFiles, commentBody);
console.log(` ✅ Posted comment with ${currentFiles.length} current screenshots`);
} else {
await gitea.postComment(GITEA_ISSUE, commentBody);
console.log(' ✅ Posted comment (no screenshots to upload)');
}
} catch (err) {
console.error(` ❌ Gitea posting failed: ${err.message}`);
}
}
process.exit(report.summary.overallPassed ? 0 : 1);
}
main().catch(err => { console.error('Fatal:', err); process.exit(1); });

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"resolveJsonModule": true,
"isolatedModules": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowImportingTsExtensions": false,
"rewriteRelativeImportExtensions": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}