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

16 KiB
Raw Blame History

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):

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):

@_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):

@_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):

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:

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):

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):

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):

@_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 ✓