Fix 14 tool bugs: capability_check tool_name, quality_gate pipefail, 12 doc param fixes, 3 test-suite fixes

This commit is contained in:
Software Orchestrator
2026-06-17 23:33:57 +00:00
parent 53758f0755
commit 2343f0e717
24 changed files with 829 additions and 166 deletions
@@ -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
@@ -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
@@ -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`).
---
+9
View File
@@ -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
+9
View File
@@ -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
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+5 -3
View File
@@ -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")
+4
View File
@@ -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:
+6 -2
View File
@@ -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.
+17 -1
View File
@@ -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:
@@ -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.
+11
View File
@@ -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.
@@ -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:
+10 -2
View File
@@ -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.
@@ -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:
+5 -1
View File
@@ -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)
+594 -137
View File
@@ -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/<name>-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,