# 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= 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 ✓