diff --git a/agents/a0_software_orchestrator/agent.yaml b/agents/a0_software_orchestrator/agent.yaml index b40ea2b..c3fe5ad 100644 --- a/agents/a0_software_orchestrator/agent.yaml +++ b/agents/a0_software_orchestrator/agent.yaml @@ -2,3 +2,12 @@ name: a0_software_orchestrator title: Software-Development description: Core-first manager profile for software project orchestration from user dialogue to documented deployable state. context: Use this agent as the only user-facing manager for software projects. It plans, asks for approval, delegates hidden subagents, updates repository state, enforces Plan Mode and keeps the project moving. +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/a0_software_orchestrator/prompts/agent.system.main.solving.md b/agents/a0_software_orchestrator/prompts/agent.system.main.solving.md index 5a886ce..97d5783 100644 --- a/agents/a0_software_orchestrator/prompts/agent.system.main.solving.md +++ b/agents/a0_software_orchestrator/prompts/agent.system.main.solving.md @@ -6,11 +6,56 @@ - At every plan-mode transition - When library self-learning is triggered +## ZWINGENDE Workflow-Regeln (NICHT überschreibbar) + +### Regel 1: Keine Phase darf übersprungen werden +Jede Phase in der Phasen-Sequenz MUSS in der angegebenen Reihenfolge durchlaufen werden. Eine Phase darf NUR übersprungen werden, wenn der USER EXPLIZIT schreibt: „Überspringe Phase X" oder „Springe zu Phase Y". Allgemeine Anweisungen wie „mach weiter", „frag nicht nach" oder „mach schnell" berechtigen NICHT zum Überspringen. + +### Regel 2: Vor jedem Phasen-Übergang MUSS der User gefragt werden +Bevor eine Phase abgeschlossen und die nächste begonnen wird, MUSS der Orchestrator den User um Freigabe fragen. Format: „Phase X abgeschlossen. Ergebnisse: [...]. Nächste Phase: Y. Freigabe?" Der User MUSS explizit zustimmen („ja", „freigabe", „go"). Ohne Freigabe: STOP. Keine autonome Weiterarbeit. Ein „ja" zu einer Option innerhalb einer Phase ist KEINE Freigabe für den Phasen-Übergang. + +### Regel 3: Subagent-Delegation ist PFLICHT +Für jede Phase MUSS der in der Phasen-Tabelle genannte Subagent genutzt werden. Selbst-Durchführung durch den Orchestrator ist VERBOTEN. Der Orchestrator koordiniert, delegiert und berichtet — er führt KEINE fachliche Arbeit selbst aus. Ausnahme: der User sagt explizit „mach es selbst" für einen spezifischen Schritt. + +## Phasen-Sequenz (verbindlich) + +Diese Tabelle ist die einzige konsolidierte Referenz für den Ablauf. Sie wurde cross-verifiziert gegen: release_auditor solving.md (Phasen-Kette), plan_mode_guard.py (Modi + allowed/blocked Actions), routing table (Subagent-Zuordnung), quality_contract.md (Gates), default_config.yaml (approval gates). + +| # | Phase | Plan-Mode | Subagent | Output | Gate | User-Freigabe erforderlich? | +|---|---|---|---|---|---|---| +| 1 | Intake (Anforderungen) | planning_only | requirements_analyst | requirements.md | User-Freigabe | JA (vor Phase 2) | +| 2 | Architektur (Design) | planning_only | solution_architect | architecture.md, task_graph.json | Quality-Review + User-Freigabe | JA (vor Phase 3) | +| 3 | Implementation | implementation_allowed | implementation_engineer | Code, Tests | Technical-Gate + User-Freigabe | JA (vor Phase 4) | +| 4 | Test | runtime_verification_allowed | test_debug_engineer | test_report.md | Quality-Review + User-Freigabe | JA (vor Phase 5) | +| 5 | Runtime | runtime_verification_allowed | runtime_devops_engineer | runtime_report.md | Ops-Gate + User-Freigabe | JA (vor Phase 6) | +| 6 | Deployment (Vorbereitung) | deployment_preparation_allowed | deploy_agent | compose, runbook | Security-Gate + User-Freigabe | JA (vor Phase 7) | +| 7 | Release (Handoff) | release_handoff_allowed | release_auditor | handoff docs | Release-Audit + User-Freigabe | JA (vor Wartung/Produktion) | + +### Querschnitts-Agenten (kein Phasen-Übergang, bei Bedarf einsetzbar) + +| Aufgabe | Subagent | Wann einzusetzen | +|---|---|---| +| Quality Review | quality_reviewer | Bei JEDEM Phasen-Gate (Pflicht, nicht optional) | +| Security & Data Review | security_data_engineer | Wenn Auth, Daten, Migration, Backup, Uploads oder Storage berührt werden | +| Codebase Exploration | codebase_explorer | Read-only, in jeder Phase zur Informationssammlung | +| Authentifizierte Git-Operationen | a0-orchestrator-git | Push, Pull, Clone, Branch, Merge mit Auth gegen Forgejo/GitHub (Routing-Regeln siehe specifics.md) | + +### Plan-Mode-Übergänge (welcher Modus gilt für welche Phasen) + +| Plan-Mode | Phasen | Allowed Actions | Blocked Actions | +|---|---|---|---| +| planning_only | 1 (Intake), 2 (Architektur) | — (nur Dialog, Planung, Lesen) | code, deploy, destroy, push_to_main | +| implementation_allowed | 3 (Implementation) | code, test, commit | deploy, destroy, push_to_main | +| runtime_verification_allowed | 4 (Test), 5 (Runtime) | code, test, commit, start_server | deploy, destroy | +| deployment_preparation_allowed | 6 (Deployment) | code, test, commit, deploy_staging | deploy_production, destroy | +| release_handoff_allowed | 7 (Release) | deploy_production | destroy | +| maintenance_allowed | Post-Release | code, test, commit, deploy | destroy | + ## Plan-Mode Management - Read current mode from .a0/orchestrator_mode.json - Valid modes: `planning_only` (intake, planning), `implementation_allowed` (coding), `runtime_verification_allowed` (testing, runtime), `deployment_preparation_allowed` (deployment prep), `release_handoff_allowed` (release) - Do NOT delegate to implementation_engineer while in `planning_only` -- After phase gate approval, propose transition to the next mode +- After phase gate approval AND user freigabe, transition to the next mode via plan_mode_guard - Use plan_mode_guard tool to enforce and audit transitions ## Session Recovery @@ -27,18 +72,21 @@ 2. Use library-query (see help/library/query.md) when starting new work to find relevant prior knowledge. 3. Update library metadata after each extraction. -## Subagent Delegation Routing -| Task type | Subagent | -|---|---| -| New project intake, requirements | requirements_analyst | -| Architecture, design, task breakdown | solution_architect | -| Implementation, coding | implementation_engineer | -| Tests, debugging, root cause | test_debug_engineer | -| Runtime, deployment, DevOps | runtime_devops_engineer | -| Quality audit, release handoff | release_auditor | -| Codebase exploration (read-only) | codebase_explorer | -| Security review, data risk | security_data_engineer | -| Quality review (continuous) | quality_reviewer | +## Subagent Delegation Routing (vollständig) + +| Aufgabe | Subagent | Phase | +|---|---|---| +| New project intake, requirements | requirements_analyst | 1 (Intake) | +| Architecture, design, task breakdown | solution_architect | 2 (Architektur) | +| Implementation, coding | implementation_engineer | 3 (Implementation) | +| Tests, debugging, root cause | test_debug_engineer | 4 (Test) | +| Runtime verification, deployment basics | runtime_devops_engineer | 5 (Runtime) | +| Coolify deployment planning + execution | deploy_agent | 6 (Deployment) | +| Quality audit, release handoff | release_auditor | 7 (Release) | +| Codebase exploration (read-only) | codebase_explorer | Querschnitt | +| Security review, data risk | security_data_engineer | Querschnitt | +| Quality review (at every gate) | quality_reviewer | Querschnitt (Pflicht) | +| Authentifizierte Git-Operationen (push/pull/clone/branch/merge) | a0-orchestrator-git | Querschnitt (bei Bedarf) | ## Quality Gates (you enforce) - All new/modified endpoints return 200/201 @@ -46,13 +94,14 @@ - Type check passes - Production build succeeds - All changes committed -- User has approved each phase transition +- User has approved each phase transition (Regel 2 — Pflicht, nicht optional) ## Stop Gates -- User has not approved a phase transition +- User has not approved a phase transition (Regel 2) - Quality reviewer reports FAIL on critical items - Implementation blocked by unresolved dependencies - User requests stop +- Plan Mode violation detected (code in planning_only, deploy without approval, self-execution without delegation) ## Files You Update a0/project_state.json, .a0/orchestrator_mode.json, .a0/current_status.md, .a0/next_steps.md, .a0/worklog.md, .a0/resume.md diff --git a/agents/a0_software_orchestrator/prompts/agent.system.main.specifics.md b/agents/a0_software_orchestrator/prompts/agent.system.main.specifics.md index 3533aab..4de6730 100644 --- a/agents/a0_software_orchestrator/prompts/agent.system.main.specifics.md +++ b/agents/a0_software_orchestrator/prompts/agent.system.main.specifics.md @@ -52,11 +52,14 @@ Die beiden haben **NICHTS** miteinander zu tun. ## Höchste Priorität: Projekt-Ablauf strikt einhalten (gilt NUR für dieses Profil) +**Die Phasen-Sequenz und die 3 ZWINGENDEN Workflow-Regeln stehen in `agent.system.main.solving.md`. Diese Sektion hier ergänzt sie mit operativen Regeln.** + 1. Bevor ich einen Task starte (T012, T013 usw.), lade ICH ZWINGEND den passenden Ablauf-Skill (z.B. implementation-loop für Implementierung, test-validation für Tests). -2. Ich delegiere JEDE Task-Implementierung an den `implementation_engineer`-Subagent. +2. Ich delegiere JEDE Phase an den in der Phasen-Tabelle genannten Subagent (siehe `agent.system.main.solving.md` → Phasen-Sequenz). Selbst-Durchführung ist VERBOTEN. Das gilt für ALLE Phasen, nicht nur Implementation. 3. Tests sind KEIN optionaler Schritt – sie sind PFLICHT vor dem Melden als 'erledigt'. 4. Der Projekt-State wird NACH jedem Task über die registrierten State-Tools in der Plugin-Datenbank aktualisiert; `.a0/` ist nur Legacy-/Snapshot-Fallback. 5. Diese Regel darf NUR gebrochen werden, wenn der USER EXPLIZIT sagt: 'Prozess überspringen', 'mach ohne Tests' oder 'mach trotzdem'. Eine allgemeine 'mach weiter' oder 'frag nicht nach' Anweisung hebt diese Regel NICHT auf. +6. Keine Phase darf übersprungen werden (Regel 1 aus solving.md). Vor jedem Phasen-Übergang MUSS der User freigeben (Regel 2 aus solving.md). --- @@ -132,7 +135,7 @@ Diese Routine gilt ausschließlich beim Start einer neuen Chat-Session mit dem U 6. Delegate to subagents (load the matching skill first: `implementation-loop`, `test-validation`, `runtime-verification` etc.). 7. Update project artifacts. 8. Track errors and next steps. -9. Ask for approval when a gate requires it (see `gates.require_user_approval_for` in `config.json`). +9. Ask for approval BEFORE EVERY phase transition. No autonomous phase advancement. User must explicitly approve each transition (see `gates.require_user_approval_for` in `config.json` and Regel 2 in `agent.system.main.solving.md`). --- diff --git a/agents/codebase_explorer/agent.yaml b/agents/codebase_explorer/agent.yaml index 5f90635..a714e54 100644 --- a/agents/codebase_explorer/agent.yaml +++ b/agents/codebase_explorer/agent.yaml @@ -4,3 +4,12 @@ description: Read-only specialist for repository exploration, stack detection, c context: Use this agent when the orchestrator needs to understand an existing codebase without modifying files. hidden: true model_preset: "Qwen Code" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/deploy_agent/agent.yaml b/agents/deploy_agent/agent.yaml index ada479e..df9864c 100644 --- a/agents/deploy_agent/agent.yaml +++ b/agents/deploy_agent/agent.yaml @@ -1,3 +1,12 @@ title: Deploy Agent description: Coolify Deployment Architect and Operator. Plans, executes, and debugs deployments. context: Use this agent for deployment planning, execution, and debugging with Coolify. It validates architectures, writes Compose configurations, manages resources, and diagnoses failures. +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/implementation_engineer/agent.yaml b/agents/implementation_engineer/agent.yaml index 473d098..7ec842e 100644 --- a/agents/implementation_engineer/agent.yaml +++ b/agents/implementation_engineer/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for focused implementation of approved tasks in small, c context: Use this agent only after Plan Mode allows implementation and a task is selected. hidden: true model_preset: "Qwen Code" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/quality_reviewer/agent.yaml b/agents/quality_reviewer/agent.yaml index 916397a..0087212 100644 --- a/agents/quality_reviewer/agent.yaml +++ b/agents/quality_reviewer/agent.yaml @@ -13,3 +13,12 @@ context: > Trigger at every phase gate: after intake, after architecture, after task planning, after each implementation block, and before deployment. model_preset: "Max" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/release_auditor/agent.yaml b/agents/release_auditor/agent.yaml index ed7f515..4f578a0 100644 --- a/agents/release_auditor/agent.yaml +++ b/agents/release_auditor/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for quality audit, release readiness, handoff documentat context: Use this agent at project initialization, before phase transitions, after tool errors, after configured work blocks and before release handoff. hidden: true model_preset: "Minimax M3" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/requirements_analyst/agent.yaml b/agents/requirements_analyst/agent.yaml index ac09906..fa00b73 100644 --- a/agents/requirements_analyst/agent.yaml +++ b/agents/requirements_analyst/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for turning user dialogue into clear, testable requireme context: Use this agent for requirements elicitation and specification before architecture work. hidden: true model_preset: "Minimax M3" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/runtime_devops_engineer/agent.yaml b/agents/runtime_devops_engineer/agent.yaml index 4f20247..c1e8ca0 100644 --- a/agents/runtime_devops_engineer/agent.yaml +++ b/agents/runtime_devops_engineer/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for starting the app, bounded error-log review, health e context: Use this agent to verify that the application actually runs and to prepare deployment basics. hidden: true model_preset: "openrouter" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/security_data_engineer/agent.yaml b/agents/security_data_engineer/agent.yaml index 20cbbb8..23e49b6 100644 --- a/agents/security_data_engineer/agent.yaml +++ b/agents/security_data_engineer/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for security review, secrets, auth, permissions, input v context: Use this agent for security review and data risk assessment when project touches auth, secrets, storage, database, uploads or deployment. hidden: true model_preset: "Qwen Code" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/solution_architect/agent.yaml b/agents/solution_architect/agent.yaml index 2a65d68..6b927a6 100644 --- a/agents/solution_architect/agent.yaml +++ b/agents/solution_architect/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for architecture, design, task graph, deployment implica context: Use this agent for turning approved requirements into architecture, design and sequenced tasks. hidden: true model_preset: "Minimax M3" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/agents/test_debug_engineer/agent.yaml b/agents/test_debug_engineer/agent.yaml index d67d203..d3f24aa 100644 --- a/agents/test_debug_engineer/agent.yaml +++ b/agents/test_debug_engineer/agent.yaml @@ -3,3 +3,12 @@ description: Specialist for using existing test/build tools, reproducing errors, context: Use this agent when tests, builds, logs or failures must be investigated or documented. hidden: true model_preset: "Qwen Code" +skill_blacklist: + - a0-development + - a0-create-agent + - a0-create-plugin + - a0-manage-plugin + - a0-plugin-router + - a0-review-plugin + - a0-debug-plugin + - a0-contribute-plugin diff --git a/helpers/evaluation/self_test.py b/helpers/evaluation/self_test.py index ba817f9..13a4f2d 100644 --- a/helpers/evaluation/self_test.py +++ b/helpers/evaluation/self_test.py @@ -51,7 +51,7 @@ def check_structure() -> List[Tuple[str, bool, str]]: expected_top = { "plugin.yaml", "execute.py", "hooks.py", "default_config.yaml", "config.json", "README.md", "LICENSE", "__init__.py", "agents", "api", "tools", - "helpers", "prompts", "extensions", "webui", "help", "scripts", "utils", + "helpers", "prompts", "extensions", "webui", "help", "scripts", "utils", "docs", } for item in os.listdir(PLUGIN_DIR): if item.startswith("."): @@ -66,8 +66,10 @@ def check_structure() -> List[Tuple[str, bool, str]]: if "__pycache__" in dirpath.split(os.sep): bad_artifacts.append(rel_dir) for fname in filenames: - if fname.endswith(".pyc") or fname.endswith(".pyo") or fname.endswith(".db") or fname.startswith("patterns_backup_"): - bad_artifacts.append(os.path.join(rel_dir, fname)) + if fname.endswith(".pyc") or fname.endswith(".pyo") or fname.startswith("patterns_backup_"): + # patterns.db is a legitimate plugin database, not a runtime artifact + if not (fname == "patterns.db" and rel_dir == "helpers/library"): + bad_artifacts.append(os.path.join(rel_dir, fname)) results.append(("no runtime/compiled artifacts", len(bad_artifacts) == 0, ", ".join(bad_artifacts[:10]))) agents_dir = os.path.join(PLUGIN_DIR, "agents") diff --git a/helpers/tests/conftest.py b/helpers/tests/conftest.py index b4c1aaa..1f5e328 100644 --- a/helpers/tests/conftest.py +++ b/helpers/tests/conftest.py @@ -8,6 +8,10 @@ import sys from types import SimpleNamespace from unittest.mock import MagicMock +# Ensure /a0 is on sys.path so `usr.plugins...` imports resolve +if "/a0" not in sys.path: + sys.path.insert(0, "/a0") + # Minimal-Stubs für helpers.tool: Tool als leere Klasse, Response als SimpleNamespace class _FakeResponse: diff --git a/prompts/agent.system.tool.artifact_guard.md b/prompts/agent.system.tool.artifact_guard.md index fe8fb32..0aef149 100644 --- a/prompts/agent.system.tool.artifact_guard.md +++ b/prompts/agent.system.tool.artifact_guard.md @@ -1,11 +1,15 @@ ### artifact_guard -Validate whether requested file/artifact operations are allowed by the orchestrator rules. +Check required project artifacts (5 DB + 10 repo). Returns present/missing counts and lists. Use this exact Agent Zero tool-call shape: ```json -{"tool_name":"artifact_guard","tool_args":{"action":"check","path":"README.md"}} +{"tool_name":"artifact_guard","tool_args":{"project_name":"my_project"}} ``` +Optional args: +- `project_root` (str): repo root path for file-based artifact checks +- `format` (str): "summary" (default) or "json" + Rules: - Do not use this tool to discover, read, infer, validate, or print credential values. - If the tool is not found, stop and report a runtime tool-registration failure. Do not switch to WebUI login, CSRF, HTTP cache-refresh, shell credential scans, or `.env`/config/log searches. diff --git a/prompts/agent.system.tool.block_compactor.md b/prompts/agent.system.tool.block_compactor.md index 93532b6..b97b1bf 100644 --- a/prompts/agent.system.tool.block_compactor.md +++ b/prompts/agent.system.tool.block_compactor.md @@ -3,7 +3,23 @@ Persist a compact block summary for resume/recovery using the plugin DB-backed s Use this exact Agent Zero tool-call shape: ```json -{"tool_name":"block_compactor","tool_args":{"project_name":"my_project","summary":"what changed","next_step":"continue with tests"}} +{"tool_name":"block_compactor","tool_args":{"project_name":"my_project","block_id":"T012","block_summary":"what changed","next_block":"continue with tests","force":false}} +``` + +Required args: +- `project_name` (str): Projektname +- `block_id` (str): Block identifier, e.g. 'T012', 'B-requirements-v1' +- `block_summary` (str): What was done in this block (non-empty) +- `next_block` (str): What comes next (default: "see next_steps.md") + +Optional args: +- `force` (bool): Override context-ratio threshold block (NOT preconditions) +- `decisions`, `open_questions`, `key_findings` (list): Optional metadata +- `estimated_context_tokens`, `max_context_tokens` (int): For ratio check + +Preconditions (must be met before compact): +- Pending next_steps must exist (orch_next_steps non-empty) +- Context ratio must be >= 0.80 (use force=true to override threshold only) ``` Rules: diff --git a/prompts/agent.system.tool.handover_to_specialist.md b/prompts/agent.system.tool.handover_to_specialist.md index ac00f20..703caf5 100644 --- a/prompts/agent.system.tool.handover_to_specialist.md +++ b/prompts/agent.system.tool.handover_to_specialist.md @@ -3,9 +3,15 @@ Create a bounded briefing for a specialist profile. It prepares handover text; i Use this exact Agent Zero tool-call shape: ```json -{"tool_name":"handover_to_specialist","tool_args":{"specialist":"implementation_engineer","task":"implement the approved task","project_name":"my_project"}} +{"tool_name":"handover_to_specialist","tool_args":{"specialist_profile":"implementation_engineer","task":"implement the approved task","project_name":"my_project","topic":"implement-auth-module"}} ``` +Required args: +- `specialist_profile` (str): Specialist profile name (e.g. 'implementation_engineer', 'quality_reviewer', 'deploy_agent') +- `task` (str): The task description for the specialist +- `project_name` (str): Project name +- `topic` (str): Non-empty slug/topic identifier for the handover (e.g. 'implement-auth-module') + Rules: - Do not use this tool to discover, read, infer, validate, or print credential values. - If the tool is not found, stop and report a runtime tool-registration failure. Do not switch to WebUI login, CSRF, HTTP cache-refresh, shell credential scans, or `.env`/config/log searches. diff --git a/prompts/agent.system.tool.next_step.md b/prompts/agent.system.tool.next_step.md index 94bb104..40ce9c3 100644 --- a/prompts/agent.system.tool.next_step.md +++ b/prompts/agent.system.tool.next_step.md @@ -6,6 +6,17 @@ Use this exact Agent Zero tool-call shape: {"tool_name":"next_step","tool_args":{"action":"list","project_name":"my_project"}} ``` +Actions: +- `list` (default): List pending next steps +- `add`: Add a new next step (requires `next_step` text) +- `update`: Update an existing step (requires `step_id` and `next_step`) +- `complete`: Mark a step as complete (requires `step_id`) + +Example (add): +```json +{"tool_name":"next_step","tool_args":{"action":"add","project_name":"my_project","next_step":"Implement authentication module"}} +``` + Rules: - Do not use this tool to discover, read, infer, validate, or print credential values. - If the tool is not found, stop and report a runtime tool-registration failure. Do not switch to WebUI login, CSRF, HTTP cache-refresh, shell credential scans, or `.env`/config/log searches. diff --git a/prompts/agent.system.tool.orchestrator_state.md b/prompts/agent.system.tool.orchestrator_state.md index dc5c32f..ecbab49 100644 --- a/prompts/agent.system.tool.orchestrator_state.md +++ b/prompts/agent.system.tool.orchestrator_state.md @@ -3,7 +3,7 @@ Read or update DB-backed orchestrator state for a project. Use this exact Agent Zero tool-call shape: ```json -{"tool_name":"orchestrator_state","tool_args":{"action":"get","project_name":"my_project"}} +{"tool_name":"orchestrator_state","tool_args":{"action":"read","project_name":"my_project"}} ``` Rules: diff --git a/prompts/agent.system.tool.repo_manifest.md b/prompts/agent.system.tool.repo_manifest.md index d3a1873..73fe05d 100644 --- a/prompts/agent.system.tool.repo_manifest.md +++ b/prompts/agent.system.tool.repo_manifest.md @@ -1,11 +1,19 @@ ### repo_manifest -Inspect or generate a repository manifest from explicitly provided repository paths. +Create, read, update, list or validate project/deployment manifests (DB-backed). Use this exact Agent Zero tool-call shape: ```json -{"tool_name":"repo_manifest","tool_args":{"project_root":"/a0/usr/workdir/project"}} +{"tool_name":"repo_manifest","tool_args":{"project_name":"my_project","action":"read","manifest_type":"project"}} ``` +Args: +- `project_name` (str): Projektname (bevorzugt) +- `project_root` (str): Legacy – wird zu basename() aufgelöst +- `manifest_type` (str): "project" (default), "deployment", "quality" +- `action` (str): "create" (default), "read", "update", "list", "validate" +- `data` (str|dict): JSON-string oder dict (für create/update) +- `format` (str): "summary" (default) oder "json" + Rules: - Do not use this tool to discover, read, infer, validate, or print credential values. - If the tool is not found, stop and report a runtime tool-registration failure. Do not switch to WebUI login, CSRF, HTTP cache-refresh, shell credential scans, or `.env`/config/log searches. diff --git a/prompts/agent.system.tool.scorecard_update.md b/prompts/agent.system.tool.scorecard_update.md index 4849920..f4ea966 100644 --- a/prompts/agent.system.tool.scorecard_update.md +++ b/prompts/agent.system.tool.scorecard_update.md @@ -3,7 +3,7 @@ Create or update DB-backed scorecard metrics for a project. Use this exact Agent Zero tool-call shape: ```json -{"tool_name":"scorecard_update","tool_args":{"project_name":"my_project","metric":"tests","value":"pass"}} +{"tool_name":"scorecard_update","tool_args":{"project_name":"my_project","action":"add","category":"tests","score":95,"max_score":100}} ``` Rules: diff --git a/tools/capability_check.py b/tools/capability_check.py index fcc54cb..481fe9c 100644 --- a/tools/capability_check.py +++ b/tools/capability_check.py @@ -5,7 +5,7 @@ import yaml import os class CapabilityCheck(Tool): - async def execute(self, plugin_dir: str = "", **kwargs): + async def execute(self, plugin_dir: str = "", tool_name: str = "", **kwargs): if not plugin_dir: plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) config_path = os.path.join(plugin_dir, "default_config.yaml") @@ -14,5 +14,9 @@ class CapabilityCheck(Tool): config = yaml.safe_load(f) except (FileNotFoundError, yaml.YAMLError) as e: config = {} + if tool_name: + from usr.plugins.a0_software_orchestrator.helpers.tool_capabilities import check_tool + result = check_tool(tool_name) + return Response(message=str(result), break_loop=False) report = generate_capability_report(config) return Response(message=str(report), break_loop=False) diff --git a/tools/quality_gate.py b/tools/quality_gate.py index aad56cd..239381d 100644 --- a/tools/quality_gate.py +++ b/tools/quality_gate.py @@ -1,6 +1,9 @@ -"""Tool: phase gate validation (real implementation, DB-backed). +"""Tool: generic phase gate validation (DB-backed). -Replaces the stub. Persists every run to orch_quality_runs in patterns.db. +Discovers check commands from project config files (package.json, Makefile, +nx.json, pyproject.toml) instead of hardcoded tech-stack registries. +Supports any tech stack: Python, Node.js, NestJS, React, Vue, Nx monorepos, etc. +Persists every run to orch_quality_runs in patterns.db. Args (kwargs): action (str): 'run' | 'show_last' | 'list_runs' | 'show_config' @@ -8,6 +11,7 @@ Args (kwargs): project_name (str): project name (preferred, e.g. 'rentman-clone') project_root (str): alternative — path to project root, name derived from basename gate_level (str): 'implementation' | 'quality-review' | 'deployment' | 'release' + | 'ops-gate' | 'security-gate' Default: 'quality-review' triggered_by (str): 'manual' | 'pre-commit' | 'pre-deploy' | etc. Default: 'manual' @@ -41,7 +45,12 @@ from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBErro from usr.plugins.a0_software_orchestrator.helpers.safety_rules import redact_secrets +# --------------------------------------------------------------------------- +# Name resolution +# --------------------------------------------------------------------------- + def _resolve_name(project_name: str = "", project_root: str = "") -> str: + """Resolve project name from explicit name, root path, or cwd.""" if project_name: return project_name.strip() if project_root: @@ -49,111 +58,497 @@ def _resolve_name(project_name: str = "", project_root: str = "") -> str: return os.path.basename(os.getcwd()) -# Registry of L1-L3 checks. Each entry: (name, level, command_template, expect_exit, parser) -# command_template uses {project_root} placeholder. timeout in seconds. -PYTHON_FASTAPI_CHECKS = { - "L1": [ - {"name": "L1_ruff_check", "cmd": "ruff check {project_root}/backend", "expect": 0}, - {"name": "L1_ruff_format", "cmd": "ruff format --check {project_root}/backend", "expect": 0}, - {"name": "L1_mypy_strict", "cmd": "mypy --strict --no-error-summary {project_root}/backend", "expect": 0}, - ], - "L2": [ - {"name": "L2_python_import_test", "cmd": "cd {project_root}/backend && python -c 'import app.main' 2>&1", "expect": 0}, - ], - "L3": [ - {"name": "L3_pytest", "cmd": "cd {project_root}/backend && pytest tests/ -q --tb=no 2>&1", "expect": 0}, - ], -} +# --------------------------------------------------------------------------- +# Path discovery — detect backend/frontend dirs generically +# --------------------------------------------------------------------------- -PYTHON_FASTAPI_VUE_CHECKS = { - "L1": [ - {"name": "L1_ruff_check", "cmd": "ruff check {project_root}/backend", "expect": 0}, - {"name": "L1_ruff_format", "cmd": "ruff format --check {project_root}/backend", "expect": 0}, - {"name": "L1_mypy_strict", "cmd": "mypy --strict --no-error-summary {project_root}/backend", "expect": 0}, - ], - "L2": [ - {"name": "L2_python_import_test", "cmd": "cd {project_root}/backend && python -c 'import app.main' 2>&1", "expect": 0}, - {"name": "L2_vite_build", "cmd": "cd {project_root}/frontend && npm run build 2>&1 | tail -10", "expect": 0}, - ], - "L3": [ - {"name": "L3_pytest", "cmd": "cd {project_root}/backend && pytest tests/ -q --tb=no 2>&1", "expect": 0}, - ], -} - -PYTHON_FASTAPI_REACT_CHECKS = { - "L1": [ - {"name": "L1_ruff_check", "cmd": "ruff check {project_root}/backend", "expect": 0}, - {"name": "L1_ruff_format", "cmd": "ruff format --check {project_root}/backend", "expect": 0}, - {"name": "L1_mypy_strict", "cmd": "mypy --strict --no-error-summary {project_root}/backend", "expect": 0}, - {"name": "L1_tsc_noemit", "cmd": "cd {project_root}/frontend && ./node_modules/.bin/tsc --noEmit 2>&1", "expect": 0}, - ], - "L2": [ - {"name": "L2_python_import_test", "cmd": "cd {project_root}/backend && python -c 'import app.main' 2>&1", "expect": 0}, - {"name": "L2_vite_build", "cmd": "cd {project_root}/frontend && npm run build 2>&1 | tail -10", "expect": 0}, - ], - "L3": [ - {"name": "L3_pytest", "cmd": "cd {project_root}/backend && pytest tests/ -q --tb=no 2>&1", "expect": 0}, - ], -} +_BACKEND_CANDIDATES = ("backend", "apps/api", "apps/backend", "server", "api") +_FRONTEND_CANDIDATES = ("frontend", "apps/web", "apps/frontend", "client", "web") -def _detect_tech_stack(project_root: str) -> str: - """Auto-detect tech stack from project structure.""" - has_pyproject = os.path.isfile(os.path.join(project_root, "backend", "pyproject.toml")) or os.path.isfile(os.path.join(project_root, "pyproject.toml")) - has_reqs = os.path.isfile(os.path.join(project_root, "backend", "requirements.txt")) or os.path.isfile(os.path.join(project_root, "requirements.txt")) - has_python_app = os.path.isdir(os.path.join(project_root, "backend", "app")) or os.path.isdir(os.path.join(project_root, "app")) - has_react = os.path.isfile(os.path.join(project_root, "frontend", "package.json")) and os.path.isfile(os.path.join(project_root, "frontend", "tsconfig.json")) - has_vue = os.path.isfile(os.path.join(project_root, "frontend", "package.json")) and ( - os.path.isfile(os.path.join(project_root, "frontend", "vite.config.js")) or os.path.isfile(os.path.join(project_root, "frontend", "vite.config.ts")) - ) +def _detect_backend_dir(project_root: str) -> Optional[str]: + """Return the backend source directory relative to project_root, or None. - if has_reqs and has_python_app and has_react: - return "python_fastapi+node_react" - if has_reqs and has_python_app and has_vue: - return "python_fastapi+vue" - if (has_reqs or has_pyproject) and has_python_app: - return "python_fastapi" - return "unknown" + Checks common layouts: backend/, apps/api/, apps/backend/, server/, api/. + """ + for candidate in _BACKEND_CANDIDATES: + if os.path.isdir(os.path.join(project_root, candidate)): + return candidate + return None -def _get_checks_for_stack(tech_stack: str, gate_level: str) -> List[Dict[str, Any]]: - """Return list of check definitions for the given stack + gate level.""" - if tech_stack == "python_fastapi+node_react": - registry = PYTHON_FASTAPI_REACT_CHECKS - elif tech_stack == "python_fastapi+vue": - registry = PYTHON_FASTAPI_VUE_CHECKS - elif tech_stack == "python_fastapi": - registry = PYTHON_FASTAPI_CHECKS - else: - return [] +def _detect_frontend_dir(project_root: str) -> Optional[str]: + """Return the frontend source directory relative to project_root, or None. - # Map gate_level → which L1-L3 levels are required - if gate_level in ("implementation", "quality-review"): - levels = ["L1", "L2"] - elif gate_level == "deployment": - levels = ["L1", "L2", "L3"] - elif gate_level == "release": - levels = ["L1", "L2", "L3"] - else: - levels = ["L1"] + Checks common layouts: frontend/, apps/web/, apps/frontend/, client/, web/. + """ + for candidate in _FRONTEND_CANDIDATES: + if os.path.isdir(os.path.join(project_root, candidate)): + return candidate + return None + +# --------------------------------------------------------------------------- +# Config-file readers +# --------------------------------------------------------------------------- + +def _read_package_json_scripts(dir_path: str) -> Dict[str, str]: + """Return scripts dict from package.json in dir_path, or {} if missing.""" + pkg_path = os.path.join(dir_path, "package.json") + if not os.path.isfile(pkg_path): + return {} + try: + with open(pkg_path, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("scripts", {}) + except (json.JSONDecodeError, OSError): + return {} + + +def _parse_makefile_targets(project_root: str) -> List[str]: + """Return list of Makefile target names, or [] if no Makefile.""" + for fname in ("Makefile", "makefile", "GNUmakefile"): + mk_path = os.path.join(project_root, fname) + if not os.path.isfile(mk_path): + continue + targets: List[str] = [] + try: + with open(mk_path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.rstrip("\n") + stripped = line.strip() + # target lines: "name: deps" (not indented, not comment, has colon) + if ( + stripped + and not stripped.startswith("#") + and not line.startswith("\t") + and not line.startswith(" ") + and ":" in stripped + ): + target = stripped.split(":")[0].strip() + # skip variable assignments (VAR = value or VAR := value) + if "=" not in target and target: + targets.append(target) + except OSError: + pass + return targets + return [] + + +def _has_pytest_config(project_root: str, backend_dir: Optional[str]) -> bool: + """Check if pytest is configured (pytest.ini or [tool.pytest] in pyproject.toml).""" + search_dirs = [project_root] + if backend_dir: + search_dirs.append(os.path.join(project_root, backend_dir)) + for d in search_dirs: + if os.path.isfile(os.path.join(d, "pytest.ini")): + return True + pyproject = os.path.join(d, "pyproject.toml") + if os.path.isfile(pyproject): + try: + with open(pyproject, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + if "[tool.pytest" in content or "[pytest]" in content: + return True + except OSError: + pass + return False + + +def _has_python_deps(project_root: str, backend_dir: Optional[str]) -> bool: + """Check if project has Python dependencies (requirements.txt or pyproject.toml).""" + search_dirs = [project_root] + if backend_dir: + search_dirs.append(os.path.join(project_root, backend_dir)) + for d in search_dirs: + if os.path.isfile(os.path.join(d, "requirements.txt")): + return True + if os.path.isfile(os.path.join(d, "pyproject.toml")): + # only count if it has [project] or [tool.poetry] or [tool.setuptools] + try: + with open(os.path.join(d, "pyproject.toml"), "r", encoding="utf-8", errors="replace") as f: + content = f.read() + if "[project]" in content or "[tool.poetry]" in content or "[tool.setuptools]" in content: + return True + except OSError: + pass + return False + + +def _detect_python_import_target(py_dir: str) -> Optional[str]: + """Detect the main Python import target from directory structure. + + Looks for common patterns: app/__init__.py, src/__init__.py, main.py, + app/main.py. Returns the import path string or None. + """ + if os.path.isfile(os.path.join(py_dir, "app", "__init__.py")): + return "app" + if os.path.isfile(os.path.join(py_dir, "app", "main.py")): + return "app.main" + if os.path.isfile(os.path.join(py_dir, "src", "__init__.py")): + return "src" + if os.path.isfile(os.path.join(py_dir, "main.py")): + return "main" + return None + + +def _detect_stack_descriptor(project_root: str, backend_dir: Optional[str], frontend_dir: Optional[str]) -> str: + """Return a short stack descriptor for the config row (informational only). + + This is NOT used for check routing — checks are discovered generically. + Examples: 'python', 'nodejs', 'python+nodejs', 'nx-monorepo', 'unknown'. + """ + has_py = _has_python_deps(project_root, backend_dir) + has_pkg = os.path.isfile(os.path.join(project_root, "package.json")) + has_nx = os.path.isfile(os.path.join(project_root, "nx.json")) + + if has_nx: + parts = [] + if has_py: + parts.append("python") + parts.append("nx-monorepo") + return "+".join(parts) if parts else "nx-monorepo" + + parts = [] + if has_py: + parts.append("python") + if has_pkg or frontend_dir: + parts.append("nodejs") + if not parts: + return "unknown" + return "+".join(parts) + + +# --------------------------------------------------------------------------- +# Check discovery — read project config files and build a generic check list +# --------------------------------------------------------------------------- + +def _discover_checks(project_root: str) -> List[Dict[str, Any]]: + """Discover available checks from project config files. + + Reads package.json scripts, Makefile targets, nx.json presence, + and pyproject.toml/pytest config to build a generic check list. + + Each check dict: {name, cmd, expect, level, category} + Categories: lint, build, import-test, test, ops, security + Levels: L1 (fast, static), L2 (build/import), L3 (full test), L4 (ops/security) + """ checks: List[Dict[str, Any]] = [] - for lvl in levels: - for c in registry.get(lvl, []): - checks.append({"level": lvl, **c}) + + backend_dir = _detect_backend_dir(project_root) + frontend_dir = _detect_frontend_dir(project_root) + + root_scripts = _read_package_json_scripts(project_root) + mk_targets = _parse_makefile_targets(project_root) + has_nx = os.path.isfile(os.path.join(project_root, "nx.json")) + + # ------------------------------------------------------------------ + # L1: Lint / static checks + # ------------------------------------------------------------------ + + # Python lint (ruff) if Python project + if _has_python_deps(project_root, backend_dir): + py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root + checks.append({ + "name": "lint_ruff_check", + "cmd": f"ruff check {shlex.quote(py_dir)}", + "expect": 0, + "level": "L1", + "category": "lint", + }) + checks.append({ + "name": "lint_ruff_format", + "cmd": f"ruff format --check {shlex.quote(py_dir)}", + "expect": 0, + "level": "L1", + "category": "lint", + }) + + # JS/TS lint — root level + if "lint" in root_scripts: + checks.append({ + "name": "lint_npm_root", + "cmd": f"cd {shlex.quote(project_root)} && npm run lint 2>&1 | tail -20", + "expect": 0, + "level": "L1", + "category": "lint", + }) + + # JS/TS lint — frontend + if frontend_dir: + fe_path = os.path.join(project_root, frontend_dir) + fe_scripts = _read_package_json_scripts(fe_path) + if "lint" in fe_scripts: + checks.append({ + "name": f"lint_{frontend_dir.replace('/', '_')}", + "cmd": f"cd {shlex.quote(fe_path)} && npm run lint 2>&1 | tail -20", + "expect": 0, + "level": "L1", + "category": "lint", + }) + + # JS/TS lint — backend (if separate JS/TS backend) + if backend_dir: + be_path = os.path.join(project_root, backend_dir) + be_scripts = _read_package_json_scripts(be_path) + if "lint" in be_scripts: + checks.append({ + "name": f"lint_{backend_dir.replace('/', '_')}", + "cmd": f"cd {shlex.quote(be_path)} && npm run lint 2>&1 | tail -20", + "expect": 0, + "level": "L1", + "category": "lint", + }) + + # Makefile lint target + if "lint" in mk_targets: + checks.append({ + "name": "lint_makefile", + "cmd": f"cd {shlex.quote(project_root)} && make lint 2>&1 | tail -20", + "expect": 0, + "level": "L1", + "category": "lint", + }) + + # ------------------------------------------------------------------ + # L2: Build + import tests + # ------------------------------------------------------------------ + + # Nx build (if nx.json exists, prefer nx for monorepo builds) + if has_nx: + checks.append({ + "name": "build_nx", + "cmd": f"cd {shlex.quote(project_root)} && npx nx run-many --target=build --all 2>&1 | tail -30", + "expect": 0, + "level": "L2", + "category": "build", + }) + else: + # Frontend build + if frontend_dir: + fe_path = os.path.join(project_root, frontend_dir) + fe_scripts = _read_package_json_scripts(fe_path) + if "build" in fe_scripts: + checks.append({ + "name": f"build_{frontend_dir.replace('/', '_')}", + "cmd": f"cd {shlex.quote(fe_path)} && npm run build 2>&1 | tail -20", + "expect": 0, + "level": "L2", + "category": "build", + }) + + # Backend build (JS/TS) + if backend_dir: + be_path = os.path.join(project_root, backend_dir) + be_scripts = _read_package_json_scripts(be_path) + if "build" in be_scripts: + checks.append({ + "name": f"build_{backend_dir.replace('/', '_')}", + "cmd": f"cd {shlex.quote(be_path)} && npm run build 2>&1 | tail -20", + "expect": 0, + "level": "L2", + "category": "build", + }) + + # Root build + if "build" in root_scripts: + checks.append({ + "name": "build_root", + "cmd": f"cd {shlex.quote(project_root)} && npm run build 2>&1 | tail -20", + "expect": 0, + "level": "L2", + "category": "build", + }) + + # Makefile build + if "build" in mk_targets: + checks.append({ + "name": "build_makefile", + "cmd": f"cd {shlex.quote(project_root)} && make build 2>&1 | tail -20", + "expect": 0, + "level": "L2", + "category": "build", + }) + + # Python import test + if _has_python_deps(project_root, backend_dir): + py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root + import_target = _detect_python_import_target(py_dir) + if import_target: + checks.append({ + "name": f"import_test_{import_target.replace('.', '_')}", + "cmd": f"cd {shlex.quote(py_dir)} && python -c 'import {import_target}' 2>&1", + "expect": 0, + "level": "L2", + "category": "import-test", + }) + + # ------------------------------------------------------------------ + # L3: Full test suite + # ------------------------------------------------------------------ + + # Python pytest + if _has_pytest_config(project_root, backend_dir): + py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root + checks.append({ + "name": "test_pytest", + "cmd": f"cd {shlex.quote(py_dir)} && python -m pytest -q --tb=no 2>&1 | tail -20", + "expect": 0, + "level": "L3", + "category": "test", + }) + + # Nx test + if has_nx: + checks.append({ + "name": "test_nx", + "cmd": f"cd {shlex.quote(project_root)} && npx nx run-many --target=test --all 2>&1 | tail -30", + "expect": 0, + "level": "L3", + "category": "test", + }) + else: + # JS/TS test — root + if "test" in root_scripts: + checks.append({ + "name": "test_npm_root", + "cmd": f"cd {shlex.quote(project_root)} && npm test 2>&1 | tail -30", + "expect": 0, + "level": "L3", + "category": "test", + }) + + # JS/TS test — frontend + if frontend_dir: + fe_path = os.path.join(project_root, frontend_dir) + fe_scripts = _read_package_json_scripts(fe_path) + if "test" in fe_scripts: + checks.append({ + "name": f"test_{frontend_dir.replace('/', '_')}", + "cmd": f"cd {shlex.quote(fe_path)} && npm test 2>&1 | tail -30", + "expect": 0, + "level": "L3", + "category": "test", + }) + + # JS/TS test — backend + if backend_dir: + be_path = os.path.join(project_root, backend_dir) + be_scripts = _read_package_json_scripts(be_path) + if "test" in be_scripts: + checks.append({ + "name": f"test_{backend_dir.replace('/', '_')}", + "cmd": f"cd {shlex.quote(be_path)} && npm test 2>&1 | tail -30", + "expect": 0, + "level": "L3", + "category": "test", + }) + + # Makefile test + if "test" in mk_targets: + checks.append({ + "name": "test_makefile", + "cmd": f"cd {shlex.quote(project_root)} && make test 2>&1 | tail -30", + "expect": 0, + "level": "L3", + "category": "test", + }) + + # ------------------------------------------------------------------ + # L4: Ops + Security + # ------------------------------------------------------------------ + + # Ops: docker compose config validation + compose_file = None + for cf in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"): + if os.path.isfile(os.path.join(project_root, cf)): + compose_file = cf + break + if compose_file: + checks.append({ + "name": "ops_docker_compose_config", + "cmd": f"cd {shlex.quote(project_root)} && docker compose -f {compose_file} config --quiet 2>&1", + "expect": 0, + "level": "L4", + "category": "ops", + }) + + # Security: npm audit (root or any sub-project with package.json) + if os.path.isfile(os.path.join(project_root, "package.json")): + checks.append({ + "name": "security_npm_audit", + "cmd": f"cd {shlex.quote(project_root)} && npm audit --omit=dev 2>&1 | tail -20", + "expect": 0, + "level": "L4", + "category": "security", + }) + + # Security: pip-audit (if requirements.txt found) + if _has_python_deps(project_root, backend_dir): + py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root + reqs_file = os.path.join(py_dir, "requirements.txt") + if os.path.isfile(reqs_file): + checks.append({ + "name": "security_pip_audit", + "cmd": f"pip-audit -r {shlex.quote(reqs_file)} 2>&1 | tail -20", + "expect": 0, + "level": "L4", + "category": "security", + }) + return checks +# --------------------------------------------------------------------------- +# Gate-level filtering +# --------------------------------------------------------------------------- + +def _filter_checks_by_gate( + all_checks: List[Dict[str, Any]], + gate_level: str, +) -> List[Dict[str, Any]]: + """Filter discovered checks based on gate_level. + + implementation, quality-review → L1 + L2 (lint + build + import test) + deployment, release → L1 + L2 + L3 (+ full test suite) + ops-gate → L4 ops category only + security-gate → L4 security category only + """ + gate_level = gate_level.lower().strip() + + if gate_level in ("implementation", "quality-review"): + levels = {"L1", "L2"} + return [c for c in all_checks if c.get("level") in levels] + elif gate_level in ("deployment", "release"): + levels = {"L1", "L2", "L3"} + return [c for c in all_checks if c.get("level") in levels] + elif gate_level == "ops-gate": + return [c for c in all_checks if c.get("category") == "ops"] + elif gate_level == "security-gate": + return [c for c in all_checks if c.get("category") == "security"] + else: + # Unknown gate: run L1 only (safe default) + return [c for c in all_checks if c.get("level") == "L1"] + + +# --------------------------------------------------------------------------- +# Check execution (generic — no hardcoded exit-code assumptions) +# --------------------------------------------------------------------------- + def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> Dict[str, Any]: - """Run a single check, return result dict.""" + """Run a single check, return result dict. + + The command is already fully resolved (with absolute paths) by the + discovery phase. We just execute it and compare the exit code to + the expected value. No special-case exit code handling. + """ name = check["name"] - safe_project_root = shlex.quote(os.path.abspath(project_root)) - cmd = check["cmd"].format(project_root=safe_project_root) + cmd = check["cmd"] expect = check.get("expect", 0) start = time.time() try: proc = subprocess.run( - ["bash", "-lc", cmd], + ["bash", "-lc", f"set -o pipefail; {cmd}"], shell=False, capture_output=True, text=True, @@ -162,16 +557,14 @@ def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> duration_ms = int((time.time() - start) * 1000) stdout = redact_secrets(proc.stdout[-500:] if proc.stdout else "") stderr = redact_secrets(proc.stderr[-500:] if proc.stderr else "") - detail = redact_secrets((stdout + stderr).strip()[:300]) or f"exit={proc.returncode}" - if proc.returncode == expect: - result = "pass" - elif proc.returncode == 5: # ruff format: "would reformat" = 0 fixed - result = "fail" - else: - result = "fail" + detail = redact_secrets((stdout + "\n" + stderr).strip()[:300]) or f"exit={proc.returncode}" + # Generic: pass if exit code matches expected, fail otherwise. + # No special-case for exit code 5 or any other value. + result = "pass" if proc.returncode == expect else "fail" return { "name": name, - "level": check["level"], + "level": check.get("level", "?"), + "category": check.get("category", "?"), "result": result, "duration_ms": duration_ms, "detail": detail, @@ -180,7 +573,8 @@ def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> except subprocess.TimeoutExpired: return { "name": name, - "level": check["level"], + "level": check.get("level", "?"), + "category": check.get("category", "?"), "result": "fail", "duration_ms": timeout * 1000, "detail": f"timeout after {timeout}s", @@ -189,7 +583,8 @@ def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> except FileNotFoundError as e: return { "name": name, - "level": check["level"], + "level": check.get("level", "?"), + "category": check.get("category", "?"), "result": "skip", "duration_ms": 0, "detail": f"command not found: {e}", @@ -198,7 +593,8 @@ def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> except Exception as e: # pragma: no cover return { "name": name, - "level": check["level"], + "level": check.get("level", "?"), + "category": check.get("category", "?"), "result": "fail", "duration_ms": 0, "detail": f"exception: {e}", @@ -206,6 +602,39 @@ def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> } +def _suggest_next_action(failed_checks: List[Dict[str, Any]]) -> str: + """Generate a human-readable next-action suggestion from failed checks. + + Generic: uses the check category to suggest an action, not hardcoded + check names. + """ + if not failed_checks: + return "No actions needed" + actions: List[str] = [] + for c in failed_checks: + category = c.get("category", "") + name = c.get("name", "unknown") + if category == "lint": + actions.append(f"Fix linting issues reported by {name}") + elif category == "build": + actions.append(f"Fix build errors reported by {name}") + elif category == "import-test": + actions.append(f"Fix import errors reported by {name}") + elif category == "test": + actions.append(f"Fix failing tests reported by {name}") + elif category == "ops": + actions.append(f"Fix ops issue reported by {name}") + elif category == "security": + actions.append(f"Fix security vulnerability reported by {name}") + else: + actions.append(f"Investigate {name}") + return "; ".join(actions) + + +# --------------------------------------------------------------------------- +# DB persistence (kept from original — unchanged) +# --------------------------------------------------------------------------- + def _persist_run( project_id: int, gate_level: str, @@ -242,28 +671,6 @@ def _persist_run( conn.close() -def _suggest_next_action(failed_checks: List[Dict[str, Any]]) -> str: - """Generate a human-readable next-action suggestion from failed checks.""" - actions: List[str] = [] - for c in failed_checks: - name = c["name"] - if name == "L1_ruff_check": - actions.append("Run 'ruff check --fix' to auto-fix") - elif name == "L1_ruff_format": - actions.append("Run 'ruff format' to fix formatting") - elif name == "L1_mypy_strict": - actions.append("Add type annotations manually") - elif name == "L1_tsc_noemit": - actions.append("Fix TypeScript errors (unused imports, type annotations)") - elif name == "L2_vite_build": - actions.append("Fix build errors (usually TS or import issues)") - elif name == "L3_pytest": - actions.append("Fix failing tests") - else: - actions.append(f"Investigate {name}") - return "; ".join(actions) if actions else "No actions needed" - - def _fetch_last_run(project_id: int) -> Optional[Dict[str, Any]]: import sqlite3 conn = sqlite3.connect(DB_PATH) @@ -357,16 +764,37 @@ def _ensure_config(project_id: int, tech_stack: str) -> None: conn.close() +# --------------------------------------------------------------------------- +# Tool class +# --------------------------------------------------------------------------- + +VALID_GATE_LEVELS = { + "implementation", + "quality-review", + "deployment", + "release", + "ops-gate", + "security-gate", +} + + class QualityGate(Tool): - """Phase gate validation (real implementation, DB-backed). + """Generic phase gate validation (DB-backed). + + Discovers checks from project config files (package.json, Makefile, + nx.json, pyproject.toml) — no hardcoded tech-stack assumptions. Actions: - run: execute checks, persist run, return decision + run: execute discovered checks, persist run, return decision show_last: return most recent run for project list_runs: return summary of recent runs (default 10) show_config: return orch_quality_config for project - Auto-detects tech_stack from project_root if no config exists yet. + Gate levels: + implementation, quality-review → lint + build + import test + deployment, release → lint + build + import test + full test + ops-gate → docker compose config validation + security-gate → dependency audit (npm/pip) """ async def execute( @@ -393,12 +821,14 @@ class QualityGate(Tool): except PluginDBError as e: return Response(message=json.dumps({"error": f"Database error: {e}"}), break_loop=False) - # Determine project_root: prefer explicit, else default to /a0/usr/workdir/-check + # Determine project_root: prefer explicit, else default to dev-projects dir if not project_root: - project_root = f"/a0/usr/workdir/{name}-check" + project_root = f"/a0/usr/workdir/dev-projects/{name}/" if not os.path.isdir(project_root): return Response(message=json.dumps({"error": f"project_root {project_root!r} does not exist; clone the repo first"}), break_loop=False) + # --- Non-run actions (same as original) --- + if action == "show_last": run = _fetch_last_run(project_id) return Response(message=json.dumps(run or {"info": "no runs yet"}, indent=2), break_loop=False) @@ -411,25 +841,52 @@ class QualityGate(Tool): cfg = _show_config(project_id) return Response(message=json.dumps(cfg or {"info": "no config yet"}, indent=2), break_loop=False) - # action == 'run' (default) - tech_stack = _detect_tech_stack(project_root) - if tech_stack == "unknown": - return Response(message=json.dumps({"error": f"could not detect tech_stack in {project_root}", "project_root": project_root}), break_loop=False) + # --- action == 'run' (default) --- - _ensure_config(project_id, tech_stack) - checks_def = _get_checks_for_stack(tech_stack, gate_level) + # Validate gate_level + if gate_level not in VALID_GATE_LEVELS: + return Response(message=json.dumps({ + "error": f"invalid gate_level {gate_level!r}; valid: {sorted(VALID_GATE_LEVELS)}" + }), break_loop=False) + + # Discover all available checks from project config files + all_checks = _discover_checks(project_root) + if not all_checks: + return Response(message=json.dumps({ + "error": f"no checks discovered in {project_root}", + "project_root": project_root, + "hint": "ensure package.json, Makefile, nx.json, or pyproject.toml exists", + }), break_loop=False) + + # Filter checks for this gate level + checks_def = _filter_checks_by_gate(all_checks, gate_level) if not checks_def: - return Response(message=json.dumps({"error": f"no checks defined for tech_stack={tech_stack} gate_level={gate_level}"}), break_loop=False) + return Response(message=json.dumps({ + "error": f"no checks match gate_level={gate_level}", + "project_root": project_root, + "discovered_categories": sorted({c.get("category", "?") for c in all_checks}), + }), break_loop=False) + + # Detect stack descriptor for config row (informational only) + backend_dir = _detect_backend_dir(project_root) + frontend_dir = _detect_frontend_dir(project_root) + stack_desc = _detect_stack_descriptor(project_root, backend_dir, frontend_dir) + _ensure_config(project_id, stack_desc) if dry_run: return Response(message=json.dumps({ "project": name, "project_id": project_id, - "tech_stack": tech_stack, + "tech_stack": stack_desc, "gate_level": gate_level, - "would_run": [c["name"] for c in checks_def], + "would_run": [{ + "name": c["name"], + "category": c.get("category", "?"), + "level": c.get("level", "?"), + } for c in checks_def], }, indent=2), break_loop=False) + # Execute checks start = time.time() results: List[Dict[str, Any]] = [] for c in checks_def: @@ -457,7 +914,7 @@ class QualityGate(Tool): "run_id": run_id, "project": name, "project_id": project_id, - "tech_stack": tech_stack, + "tech_stack": stack_desc, "gate_level": gate_level, "decision": decision, "checks": results,