Initial commit: a0_software_orchestrator v1.0
- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung) - 25 Tests gruen (Pytest) - block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block) - 4 Restbaustellen gefixt - DB-Schema: plugin_settings-Tabelle hinzugefuegt - 3 Schattenprojekte aus DB geloescht - Plan v3 + Refactor-Plan + Worklog dokumentiert
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
# Bugfix-Plan: Auto-Registration in `resolve_project()`
|
||||
|
||||
**Datum:** 2026-06-16
|
||||
**Status:** Plan v3 – CONDITIONAL adressiert + Block 7b (plan_mode_guard) ergänzt
|
||||
**Phase:** Plan-Mode `implementation_allowed` (gesetzt für `a0_software_orchestrator` Meta-Projekt)
|
||||
**Betroffenes Plugin:** `a0_software_orchestrator`
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem (Ist-Zustand)
|
||||
|
||||
`resolve_project(name)` in `helpers/db_state_store.py` Zeile 60–109 ruft bei unbekanntem Namen **automatisch** ein `INSERT INTO projects` auf, ohne Plausiprüfung. Sechs Folge-Bugs + ein Siebter (Fund bei Modus-Wechsel):
|
||||
|
||||
| # | Bug | Datei / Zeile | Folge |
|
||||
|---|-----|--------------|-------|
|
||||
| 1 | `resolve_project()` akzeptiert jeden String | `db_state_store.py:60-109` | Schattenprojekte wie `a0`, `a0-development`, `workdir` (3 bereits gelöscht) |
|
||||
| 2 | `register_project()` ohne Plausiprüfung | `db_state_store.py:644-687` | Auch der explekite Pfad akzeptiert Müll |
|
||||
| 3 | `os.path.basename(...)` als Projektname | `migrate_a0_to_db.py:398` | Verzeichnisname wird stillschweigend zum Projektnamen |
|
||||
| 4 | `os.path.basename(...)` als Projektname | `api/project_state_get.py:16` | API-Call mit `project_root` erzeugt Phantom-Projekt |
|
||||
| 5 | `resolve_project()` macht UPSERT auf `project_state.last_active_at` bei JEDEM Aufruf | `db_state_store.py:82-89` | Versteckter Side-Effect: jeder Tool-Aufruf verändert DB-State |
|
||||
| 6 | Modul-Docstring empfiehlt `resolve_project()` weiterhin | `db_state_store.py:1-19, 127` | Nach Fix irreführend → muss mit-aktualisiert werden |
|
||||
| 7 | `plan_mode_guard.py` ruft `resolve_project()` (Auto-Register-Bug) | `plan_mode_guard.py:76` | Modus-Wechsel für unbekannte Projektnamen erzeugt Phantom; nach Fix scheitert LookupError |
|
||||
|
||||
## 2. Soll-Zustand
|
||||
|
||||
- **Zwei saubere Funktionen mit klarer Semantik:**
|
||||
- `get_project_id(name)` – nur Lesen, gibt `int | None` zurück.
|
||||
- `register_project(name, git_url, description, ...)` – nur Anlegen/Aktualisieren, **mit Plausiprüfung**.
|
||||
- `resolve_project()` wird zum dünnen Komfort-Wrapper, der **bei fehlendem Projekt `LookupError` wirft** statt heimlich anzulegen. Der `last_active_at`-UPSERT bleibt erhalten.
|
||||
- `plan_mode_guard.py` arbeitet **projekt-unabhängig**: liest/schreibt einen globalen Key `orchestrator_mode` (nicht pro project_id). Kein `resolve_project()`-Aufruf mehr.
|
||||
- Pattern + Blacklist verhindern ungültige Namen direkt bei `register_project()`.
|
||||
- `basename`-Fallbacks in `migrate_a0_to_db.py` und `api/project_state_get.py` werden entfernt.
|
||||
|
||||
**Pattern-Akzeptanzkriterien (explizit):**
|
||||
- Erlaubt: `^[a-z][a-z0-9-]{1,40}$` (lowercase, Buchstaben/Ziffern/Bindestrich, 2–41 Zeichen, mit Buchstabe beginnend).
|
||||
- **Nicht erlaubt:** Punkte, Underscores, Umlaute, Leerzeichen, Großbuchstaben.
|
||||
|
||||
**Blacklist-Klärung:**
|
||||
- `a0-development` ist **bewusst** in der Blacklist. Wer es künftig als DB-Eintrag will: Blacklist-Eintrag entfernen + explizit `register_project("a0-development", ...)` aufrufen.
|
||||
|
||||
**Meta-Projekt `a0_software_orchestrator`:**
|
||||
- Bereits vor dem Fix explizit registriert (project_id=5) als Träger für Plugin-Level-State (plan_mode etc.).
|
||||
- Matcht das neue Pattern NICHT (Underscore), aber Existenz in DB bleibt erhalten (register_project war vor dem neuen Pattern aktiv).
|
||||
- Wird nach dem Fix nicht von `resolve_project` blockiert, weil `plan_mode_guard` kein `resolve_project` mehr ruft (siehe §3.7).
|
||||
|
||||
**Out-of-scope (explizit):**
|
||||
- `helpers/library/db.py:PatternDB.register_project` (DB-Layer, nicht Project-Layer) – separater Fix.
|
||||
- Side-Note: `leocrm` hat bereits orch_kv-Einträge (`phase=implementation`, `orchestrator_mode=implementation_allowed`, `project_root=/a0/usr/workdir/dev-projects/leocrm`) aus früherer Session. Inkonsistenz mit `project_state.plan_mode=None`. **Nicht Teil dieses Bugs**, separater Audit-Task.
|
||||
|
||||
## 3. Konkrete Änderungen
|
||||
|
||||
### 3.1 `helpers/db_state_store.py`
|
||||
|
||||
**A) Neue Konstanten + Validator** (nach Zeile 20, vor `_db_error_handler`):
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
PROJECT_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]{1,40}$")
|
||||
FORBIDDEN_PROJECT_NAMES = frozenset({
|
||||
"a0", "a0-development", "workdir", "dev-projects",
|
||||
"usr", "tmp", "home", "root", "skills", "plugins",
|
||||
"opt", "lib", "etc", "var", "data", "venv",
|
||||
})
|
||||
|
||||
def _validate_project_name(name: str) -> str:
|
||||
"""Strippt, prüft Pattern + Blacklist. Wirft ValueError bei Verletzung."""
|
||||
if not name or not name.strip():
|
||||
raise ValueError("project name must be non-empty")
|
||||
name = name.strip()
|
||||
if name in FORBIDDEN_PROJECT_NAMES:
|
||||
raise ValueError(
|
||||
f"project name '{name}' is reserved/forbidden "
|
||||
f"(likely a system directory, not a software project)"
|
||||
)
|
||||
if not PROJECT_NAME_PATTERN.match(name):
|
||||
raise ValueError(
|
||||
f"project name '{name}' does not match pattern "
|
||||
f"{PROJECT_NAME_PATTERN.pattern} (lowercase letters, digits, hyphens)"
|
||||
)
|
||||
return name
|
||||
```
|
||||
|
||||
**B) `resolve_project()` umbauen** (Zeile 60–109):
|
||||
|
||||
```python
|
||||
@_db_error_handler
|
||||
def resolve_project(name: str) -> int:
|
||||
"""Löst einen Projektnamen zur project_id auf.
|
||||
|
||||
WICHTIG: Legt KEIN neues Projekt mehr an. Bei unbekanntem Namen
|
||||
wird LookupError geworfen. Zum Anlegen explizit register_project()
|
||||
oder project_registry action=register verwenden.
|
||||
"""
|
||||
name = _validate_project_name(name)
|
||||
db = _db()
|
||||
row = db.conn.execute(
|
||||
"SELECT id FROM projects WHERE name = ?", (name,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise LookupError(
|
||||
f"project '{name}' is not registered. "
|
||||
f"Call register_project() or project_registry action=register first."
|
||||
)
|
||||
project_id = int(row[0])
|
||||
db.conn.execute(
|
||||
"""
|
||||
INSERT INTO project_state (project_id, status, phase, last_active_at)
|
||||
VALUES (?, 'active', 'intake', datetime('now'))
|
||||
ON CONFLICT(project_id) DO UPDATE SET last_active_at = datetime('now')
|
||||
""",
|
||||
(project_id,),
|
||||
)
|
||||
db.conn.commit()
|
||||
return project_id
|
||||
```
|
||||
|
||||
**C) `register_project()` Plausiprüfung rein** (Zeile 644):
|
||||
|
||||
```python
|
||||
@_db_error_handler
|
||||
def register_project(name: str, git_url: str = "", description: str = "",
|
||||
tech_stack: str = "{}", status: str = "active",
|
||||
phase: str = "intake") -> int:
|
||||
"""Register or update a project. Returns project_id."""
|
||||
name = _validate_project_name(name)
|
||||
# ... Rest wie bisher (INSERT … ON CONFLICT, project_state upsert, set_kv)
|
||||
```
|
||||
|
||||
**D) Modul-Docstring oben anpassen** (Zeile 1–19, Bug 6):
|
||||
|
||||
```
|
||||
Verwendung:
|
||||
# Neues Projekt anlegen (explizit):
|
||||
pid = register_project("crm-system", git_url="https://...")
|
||||
set_kv(pid, "phase", "implementation")
|
||||
|
||||
# Existierendes Projekt nachschlagen (nur lesen):
|
||||
existing_pid = get_project_id("crm-system")
|
||||
|
||||
# Nur in create-on-missing-Aufrufern (z.B. Migration):
|
||||
pid = resolve_project("crm-system") # wirft LookupError wenn fehlt
|
||||
```
|
||||
|
||||
**E) Hinweis-Text in `get_project_id()`-Docstring** (Zeile 123–136, Bug 6):
|
||||
|
||||
```python
|
||||
def get_project_id(name: str) -> Optional[int]:
|
||||
"""Return existing project_id without creating or mutating project state.
|
||||
|
||||
This is the preferred read-only lookup. For create-or-lookup semantics
|
||||
use resolve_project(); to create explicitly use register_project().
|
||||
"""
|
||||
```
|
||||
|
||||
### 3.2 Tool-Aufrufer migrieren (10 Tools / 10 Call-Sites)
|
||||
|
||||
Alle Stellen, die aktuell `pid = resolve_project(name)` machen, ändern auf:
|
||||
|
||||
```python
|
||||
pid = get_project_id(name)
|
||||
if pid is None:
|
||||
raise LookupError(f"project '{name}' is not registered")
|
||||
```
|
||||
|
||||
Betroffen (alle in `tools/`):
|
||||
- `block_compactor.py:297`
|
||||
- `block_resume.py:54`
|
||||
- `next_step.py:49`
|
||||
- `orchestrator_state.py:48`
|
||||
- `plan_mode_guard.py:76` → wird komplett umgebaut, siehe §3.7
|
||||
- `quality_gate.py:388`
|
||||
- `repo_manifest.py:64`
|
||||
- `resume_checker.py:40`
|
||||
- `scorecard_update.py:43`
|
||||
- `tool_registry.py:50`
|
||||
|
||||
**NICHT geändert** (verwenden bereits `get_project_id`):
|
||||
- `extensions/python/monologue_start/load_project_state.py:30`
|
||||
|
||||
### 3.3 Helper-Aufrufer migrieren (4 Helper-Dateien / 7 Call-Sites)
|
||||
|
||||
| Datei | Call-Sites |
|
||||
|-------|-----------|
|
||||
| `helpers/artifact_rules.py` | 1 (Z. 53) |
|
||||
| `helpers/briefing_file.py` | 2 (Z. 62, 134) |
|
||||
| `helpers/compact_protocol.py` | 3 (Z. 177, 218, 243) |
|
||||
| `helpers/resume_state.py` | 1 (Z. 61) |
|
||||
|
||||
### 3.4 `utils/migrate_a0_to_db.py`
|
||||
|
||||
**Zeile 395–419 (basename-Fallback entfernen, Bug 3):**
|
||||
|
||||
```python
|
||||
if not args.project:
|
||||
print("ERROR: --project=<name> ist Pflicht (basename-Fallback entfernt)")
|
||||
sys.exit(1)
|
||||
project_name = args.project
|
||||
if os.path.isabs(args.project) and os.path.isdir(args.project):
|
||||
project_root = args.project
|
||||
elif args.project_root:
|
||||
project_root = args.project_root
|
||||
else:
|
||||
# ... vorhandene DB-Lookup-Logik bleibt
|
||||
```
|
||||
|
||||
**Zeile 213:** `pid = resolve_project(project_name)` → `pid = register_project(project_name, git_url="")`
|
||||
|
||||
### 3.5 `api/project_state_get.py`
|
||||
|
||||
**Zeile 16 ersetzen (Bug 4):**
|
||||
|
||||
```python
|
||||
project_name = input.get("project_name")
|
||||
if not project_name:
|
||||
return {"ok": False, "error": "project_name is required (basename fallback removed)"}
|
||||
```
|
||||
|
||||
### 3.6 Legacy-Daten-Migration (Blocker b)
|
||||
|
||||
**Neue Datei:** `utils/migrate_legacy_project_names.py`
|
||||
|
||||
Logik: SELECT alle Projekte → prüfe ob Name Pattern + nicht in Blacklist → wenn nicht: status='archived' in project_state, WARN-Log → Bericht "X archived, Y active".
|
||||
|
||||
**Aufruf:** Manuell. Im aktuellen Datenbestand nur leocrm (matcht) → leerer Archivierungs-Lauf.
|
||||
|
||||
### 3.7 `tools/plan_mode_guard.py` umbauen (Bug 7, Block 7b)
|
||||
|
||||
**Problem:** `plan_mode_guard.py:76` ruft `resolve_project(name)` auf. Nach dem Fix wirft das für unbekannte Namen `LookupError`. Außerdem: Plan-Mode ist plugin-weit, nicht projekt-spezifisch.
|
||||
|
||||
**Lösung:** Plan-Mode als globaler Key ohne Projekt-Bindung.
|
||||
|
||||
**Konzept:**
|
||||
- Statt `orch_kv[pid]['orchestrator_mode']`: globaler Key z.B. in `project_state` mit `project_id=0` (Platzhalter) ODER in einer neuen Tabelle `plugin_settings(key, value_json)`.
|
||||
- Pragmatisch: erstere Variante (Platzhalter project_id=0) – keine Schema-Änderung.
|
||||
|
||||
**Konkret (Pseudocode für `plan_mode_guard.py`):**
|
||||
|
||||
```python
|
||||
@_db_error_handler
|
||||
def _get_plugin_mode() -> dict:
|
||||
db = _db()
|
||||
row = db.conn.execute(
|
||||
"SELECT value_json FROM orch_kv "
|
||||
"WHERE project_id = 0 AND key = 'orchestrator_mode'"
|
||||
).fetchone()
|
||||
if not row:
|
||||
return dict(_DEFAULT_MODE)
|
||||
return _normalize_mode(json.loads(row[0]))
|
||||
|
||||
@_db_error_handler
|
||||
def _set_plugin_mode(mode_data: dict) -> None:
|
||||
db = _db()
|
||||
db.conn.execute(
|
||||
"INSERT INTO orch_kv (project_id, key, value_json, updated_at) "
|
||||
"VALUES (0, 'orchestrator_mode', ?, datetime('now')) "
|
||||
"ON CONFLICT(project_id, key) DO UPDATE SET "
|
||||
" value_json = excluded.value_json, updated_at = datetime('now')",
|
||||
(json.dumps(mode_data),)
|
||||
)
|
||||
db.conn.commit()
|
||||
|
||||
class PlanModeGuard(Tool):
|
||||
async def execute(self, requested_action: str = "", new_mode: str = "", **kwargs):
|
||||
# KEIN resolve_project-Aufruf mehr!
|
||||
if new_mode:
|
||||
if new_mode not in _MODE_POLICIES:
|
||||
return Response(message=json.dumps({"verdict": "ERROR", "error": f"invalid mode: {new_mode}"}), break_loop=False)
|
||||
mode_data = {
|
||||
"mode": new_mode,
|
||||
"allowed_actions": list(_MODE_POLICIES[new_mode]["allowed_actions"]),
|
||||
"blocked_actions": list(_MODE_POLICIES[new_mode]["blocked_actions"]),
|
||||
}
|
||||
_set_plugin_mode(mode_data)
|
||||
return Response(message=json.dumps({"verdict": "ALLOWED", "mode": new_mode, "transitioned": True}, indent=2), break_loop=False)
|
||||
|
||||
mode_data = _get_plugin_mode()
|
||||
current_mode = mode_data["mode"]
|
||||
allowed = mode_data["allowed_actions"]
|
||||
blocked = mode_data["blocked_actions"]
|
||||
if requested_action and requested_action in blocked:
|
||||
return Response(message=f"BLOCKED: action '{requested_action}' not allowed in mode '{current_mode}'", break_loop=False)
|
||||
if not requested_action or requested_action in allowed:
|
||||
return Response(message=json.dumps({"verdict": "ALLOWED", "mode": current_mode, "action": requested_action}, indent=2), break_loop=False)
|
||||
return Response(message=json.dumps({"verdict": "UNKNOWN", "mode": current_mode, "action": requested_action}, indent=2), break_loop=False)
|
||||
```
|
||||
|
||||
**Migration:** Bestehende `orch_kv[pid]['orchestrator_mode']` (z.B. für leocrm, a0_software_orchestrator) werden IGNORIERT. Neue globale Sicht unter `project_id=0` gewinnt. Cleanup der Alt-Einträge: manuell oder in Legacy-Migration §3.6 mit-erledigen.
|
||||
|
||||
## 4. Test-Plan
|
||||
|
||||
**Neue Datei:** `helpers/tests/test_db_state_store.py`
|
||||
|
||||
Testfälle (pytest, in-memory sqlite, Fixture für `PatternDB`):
|
||||
|
||||
**Validator (3 Cases):**
|
||||
1. `test_validate_project_name_ok` – `"crm-system"`, `"web-cad"`, `"rentman-clone"`, `"a1b"` → ok
|
||||
2. `test_validate_project_name_pattern_fail` – `"A0"`, `"Crm System"`, `"a"`, `""`, `"my.app"`, `"cool_app"`, `"müller"` → ValueError
|
||||
3. `test_validate_project_name_blacklist` – `"a0"`, `"a0-development"`, `"workdir"`, `"dev-projects"` → ValueError
|
||||
4. `test_validate_project_name_whitespace_strip` – `" crm "` → `"crm"`
|
||||
|
||||
**register_project (4 Cases):**
|
||||
5. `test_register_project_new`
|
||||
6. `test_register_project_idempotent`
|
||||
7. `test_register_project_value_error_wraps_validator`
|
||||
8. `test_register_project_idempotent_overwrite_semantics`
|
||||
|
||||
**resolve_project (4 Cases):**
|
||||
9. `test_resolve_project_existing`
|
||||
10. `test_resolve_project_missing_raises` (kein Auto-Register mehr!)
|
||||
11. `test_resolve_project_invalid_name_raises` (Blacklist wirft VOR SELECT)
|
||||
12. `test_resolve_project_touches_last_active_at`
|
||||
|
||||
**get_project_id (1 Case):**
|
||||
13. `test_get_project_id_missing_returns_none`
|
||||
|
||||
**Legacy-Migration (1 Case):**
|
||||
14. `test_legacy_row_with_invalid_name_is_archived`
|
||||
|
||||
**End-to-End (1 Case):**
|
||||
15. `test_e2e_next_step_with_unknown_project_raises`
|
||||
|
||||
**plan_mode_guard global (2 Cases):**
|
||||
16. `test_plan_mode_guard_set_get` – set implementation_active, read back, no project_id needed
|
||||
17. `test_plan_mode_guard_no_resolve_project_called` – Mock resolve_project, ensure NOT called
|
||||
|
||||
## 5. Risiken & Migrationspfad
|
||||
|
||||
- **Breaking Change** für 17 resolve_project-Aufrufer (10 Tools – 1 plan_mode_guard = 9 + 7 Helper-Sites + 1 Skript = 17). `plan_mode_guard` wird komplett umgebaut (kein resolve_project). Mitigation: LookupError-Handling + globaler Mode-Key.
|
||||
- **Legacy-Daten:** §3.6.
|
||||
- **Aktive User-Flows:** `resolve_project` mit projektnamen aus User-Eingabe bricht jetzt hart mit LookupError. Mitigation: klarer Fehlertext.
|
||||
- **Plugin-Reload mitten im Tool-Call:** minor.
|
||||
- **Concurrency:** SQLite single-writer, kein neues Risiko. ✓
|
||||
- **Out-of-scope:** `PatternDB.register_project` (library), `leocrm`-Daten-Audit.
|
||||
- **Bestehende Projekte:** `leocrm` matcht Pattern und ist nicht in Blacklist → keine Daten-Migration nötig.
|
||||
- **plan_mode_guard-Migration:** globale Sicht unter `project_id=0` löst pro-projekt-Sicht ab. Alt-Einträge werden ignoriert, Cleanup optional.
|
||||
|
||||
## 6. Freigabe-Checkliste
|
||||
|
||||
- [ ] User hat Plan v3 gelesen
|
||||
- [ ] User gibt frei: Implementierung über `implementation_engineer` Subagent
|
||||
- [ ] Bug 1–7 adressiert (inkl. Bug 7: plan_mode_guard-Umbau)
|
||||
- [ ] 17 Call-Sites (9 Tools + 7 Helper + 1 Skript) angepasst
|
||||
- [ ] `plan_mode_guard.py` umgebaut (Bug 7, §3.7)
|
||||
- [ ] 2 basename-Fallbacks entfernt
|
||||
- [ ] Modul-Docstring aktualisiert (Bug 6)
|
||||
- [ ] 17 Test-Cases geschrieben + grün
|
||||
- [ ] Legacy-Migrations-Snippet erstellt + ausgeführt
|
||||
- [ ] Optional: `quality_reviewer` Re-Review nach Implementierung
|
||||
|
||||
---
|
||||
|
||||
**Geschätzter Aufwand:** 5–6 Subagent-Calls (1× großer Patch-Block via implementation_engineer, 1× Tests via test_debug_engineer, 1× Quality-Review, 1× Final-Verify).
|
||||
|
||||
**Reviewer-Verdict:** CONDITIONAL → nach Einarbeitung dieser 3 Blocker freigabefähig.
|
||||
- (a) Call-Site-Inventur: 14 → 17 (plan_mode_guard ausgenommen) ✓
|
||||
- (b) Legacy-Migration: §3.6 + Test 14 ✓
|
||||
- (c) PatternDB.register_project: out-of-scope ✓
|
||||
- (d) Bug 7 (plan_mode_guard): §3.7 + Tests 16-17 ergänzt ✓
|
||||
Reference in New Issue
Block a user