Fix 14 tool bugs: capability_check tool_name, quality_gate pipefail, 12 doc param fixes, 3 test-suite fixes
This commit is contained in:
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user