Files
a0_software_orchestrator/docs/bugfix-auto-registration.md
T
Software Orchestrator 5769c1cd22 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
2026-06-16 22:13:06 +00:00

360 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 60109 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, 241 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 60109):
```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 119, 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 123136, 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 395419 (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 17 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:** 56 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 ✓