diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/project_state_get.py /a0/usr/plugins/a0_software_orchestrator/api/project_state_get.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/project_state_get.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/api/project_state_get.py 2026-06-16 10:14:06.905148563 +0000 @@ -13,7 +13,10 @@ class ProjectStateGet(ApiHandler): async def process(self, input: dict, request: Request) -> dict | Response: project_root = input.get("project_root", os.getcwd()) - project_name = input.get("project_name") or os.path.basename(project_root.rstrip("/")) + # Bug 4 Fix: basename-Fallback entfernt. project_name ist Pflicht. + project_name = input.get("project_name") + if not project_name: + return {"ok": False, "error": "project_name is required (basename fallback removed)"} try: pid = get_project_id(project_name) if pid is None: Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/__pycache__/project_state_get.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/api/__pycache__/project_state_get.cpython-313.pyc differ diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/docs/bugfix-auto-registration.md /a0/usr/plugins/a0_software_orchestrator/docs/bugfix-auto-registration.md --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/docs/bugfix-auto-registration.md 2026-06-16 09:19:29.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/docs/bugfix-auto-registration.md 2026-06-16 10:04:32.559404096 +0000 @@ -1,44 +1,51 @@ # Bugfix-Plan: Auto-Registration in `resolve_project()` **Datum:** 2026-06-16 -**Status:** Plan v2 – CONDITIONAL vom Quality-Reviewer, 3 Blocker adressiert -**Phase:** Plan-Mode `planning_only` +**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: +`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 explizite Pfad akzeptiert Müll | +| 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, weil er als "Aktivitätsmarker" semantisch nützlich ist – wird aber in Tests explizit verifiziert. +- `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. Das ist **bewusst restriktiv**, weil Software-Projektnamen sonst zu viel Variabilität erlauben. -- Falls User Namen mit Punkten/Underscores braucht: Blacklist/Pattern explizit erweitern (Diskussion erforderlich). +- **Nicht erlaubt:** Punkte, Underscores, Umlaute, Leerzeichen, Großbuchstaben. **Blacklist-Klärung:** -- `a0-development` ist **bewusst** in der Blacklist. Falls es künftig doch als DB-Eintrag gewünscht ist: Blacklist-Eintrag entfernen + explizit `register_project("a0-development", ...)` aufrufen. Der Satz in §5 der ersten Plan-Version war missverständlich und ist hiermit korrigiert. +- `a0-development` ist **bewusst** in der Blacklist. Wer es künftig als DB-Eintrag will: Blacklist-Eintrag entfernen + explizit `register_project("a0-development", ...)` aufrufen. -**Out-of-scope (explizit dokumentiert):** -- `helpers/library/db.py` enthält eine separate Klasse `PatternDB.register_project()` (Z. 117, 142, 160, 222). Das ist ein anderer Code-Pfad (DB-Layer, nicht Project-Layer) und wird in diesem Fix **nicht** angefasst. Side-Note: zukünftiges Risiko, sollte in einem separaten Ticket adressiert werden. +**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 @@ -57,8 +64,7 @@ }) def _validate_project_name(name: str) -> str: - """Strippt, prüft Pattern + Blacklist. Gibt bereinigten Namen zurück. - Wirft ValueError bei Verletzung.""" + """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() @@ -97,7 +103,6 @@ f"Call register_project() or project_registry action=register first." ) project_id = int(row[0]) - # Touch last_active_at (Aktivitätsmarker – gewünschter Side-Effect) db.conn.execute( """ INSERT INTO project_state (project_id, status, phase, last_active_at) @@ -118,7 +123,7 @@ tech_stack: str = "{}", status: str = "active", phase: str = "intake") -> int: """Register or update a project. Returns project_id.""" - name = _validate_project_name(name) # NEU + name = _validate_project_name(name) # ... Rest wie bisher (INSERT … ON CONFLICT, project_state upsert, set_kv) ``` @@ -163,7 +168,7 @@ - `block_resume.py:54` - `next_step.py:49` - `orchestrator_state.py:48` -- `plan_mode_guard.py:76` +- `plan_mode_guard.py:76` → wird komplett umgebaut, siehe §3.7 - `quality_gate.py:388` - `repo_manifest.py:64` - `resume_checker.py:40` @@ -171,32 +176,22 @@ - `tool_registry.py:50` **NICHT geändert** (verwenden bereits `get_project_id`): -- `extensions/python/monologue_start/load_project_state.py:30` – Bestätigung, dass das der richtige Pfad ist. +- `extensions/python/monologue_start/load_project_state.py:30` ### 3.3 Helper-Aufrufer migrieren (4 Helper-Dateien / 7 Call-Sites) -Gleiche Umstellung auf `get_project_id` + `LookupError`: - | 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) | -| **Total** | **7 Call-Sites in 4 Dateien** | ### 3.4 `utils/migrate_a0_to_db.py` **Zeile 395–419 (basename-Fallback entfernen, Bug 3):** ```python -# Vorher: -if os.path.isabs(args.project) and os.path.isdir(args.project): - project_root = args.project - project_name = os.path.basename(project_root.rstrip("/")) -... - -# Nachher: if not args.project: print("ERROR: --project= ist Pflicht (basename-Fallback entfernt)") sys.exit(1) @@ -210,17 +205,12 @@ ``` **Zeile 213:** `pid = resolve_project(project_name)` → `pid = register_project(project_name, git_url="")` -(wenn schon vorhanden: idempotent, schadet nicht. Wenn nicht: legt sauber an.) ### 3.5 `api/project_state_get.py` **Zeile 16 ersetzen (Bug 4):** ```python -# Vorher: -project_name = input.get("project_name") or os.path.basename(project_root.rstrip("/")) - -# Nachher: project_name = input.get("project_name") if not project_name: return {"ok": False, "error": "project_name is required (basename fallback removed)"} @@ -228,22 +218,74 @@ ### 3.6 Legacy-Daten-Migration (Blocker b) -**Problem:** Bestehende DB-Rows mit Namen außerhalb des neuen Patterns werden durch `_validate_project_name()` (aufgerufen in `resolve_project`) unsichtbar, weil die Validierung VOR dem SELECT kommt. +**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. -**Lösungsansatz:** Einmaliges Migrations-Snippet in `utils/migrate_legacy_project_names.py` (neu): +**Konkret (Pseudocode für `plan_mode_guard.py`):** ```python -# Vereinfachte Logik: -# 1) SELECT id, name FROM projects; -# 2) Pro Row: prüfe ob name das Pattern matcht UND nicht in Blacklist ist -# 3) Wenn nicht: setze status='archived' in project_state, logge WARN -# 4) Bericht am Ende: "X archived, Y active" +@_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() -# Im aktuellen Datenbestand ist nur leocrm vorhanden (matcht Pattern), -# also leerer Archivierungs-Lauf zu erwarten. +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) ``` -**Aufruf:** Manuell oder automatisch beim Plugin-Start (einmalig, mit Flag `--already-migrated` zur Idempotenz). +**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 @@ -255,57 +297,63 @@ 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"` (gestrippt) +4. `test_validate_project_name_whitespace_strip` – `" crm "` → `"crm"` -**register_project (3 Cases):** -5. `test_register_project_new` – neues Projekt → id, in DB -6. `test_register_project_idempotent` – zweimal registrieren mit gleichen Daten → gleiche id -7. `test_register_project_value_error_wraps_validator` – invalider Name → ValueError vom Wrapper (nicht nur vom Validator) -8. `test_register_project_idempotent_overwrite_semantics` – zweimal mit verschiedenen `git_url` → welche gewinnt? (Doku: COALESCE-Logik in Zeile 657-661) - -**resolve_project (3 Cases):** -9. `test_resolve_project_existing` – registered project → id, last_active_at aktualisiert -10. `test_resolve_project_missing_raises` – nicht registriert → LookupError (kein Auto-Register mehr!) -11. `test_resolve_project_invalid_name_raises` – Name aus Blacklist → ValueError (bevor SELECT läuft) -12. `test_resolve_project_touches_last_active_at` – zweimal rufen, last_active_at verändert sich +**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` – nicht registriert → None +13. `test_get_project_id_missing_returns_none` **Legacy-Migration (1 Case):** -14. `test_legacy_row_with_invalid_name_is_archived` – INSERT einer Row mit Name `"OLD-PROJECT"` direkt in DB → Migrations-Run setzt status='archived' +14. `test_legacy_row_with_invalid_name_is_archived` **End-to-End (1 Case):** -15. `test_e2e_next_step_with_unknown_project_raises` – Aufruf von `next_step` mit unbekanntem Projekt → LookupError, **kein** DB-Eintrag enstanden +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 alle 18 resolve_project-Aufrufer (10 Tools + 7 Helper-Sites + 1 Skript). Mitigation: LookupError-Handling an Aufrufstelle ist explizit und sichtbar. -- **Legacy-Daten:** Bestehende Projekte mit Namen außerhalb des Patterns werden unsichtbar. Mitigation: Legacy-Migrations-Snippet in §3.6. -- **Aktive User-Flows:** User-Dialoge, die heute `resolve_project` mit projektnamen aus User-Eingabe füttern, brechen jetzt hart mit LookupError statt Auto-Register. Mitigation: klarer Fehlertext "project '{name}' is not registered. Call register_project() or project_registry action=register first." → User kann selbst korrigieren. -- **Plugin-Reload** mitten in laufendem Tool-Call: mixed old/new code. Minor. -- **Concurrency:** SQLite single-writer, ON CONFLICT in register_project, kein neues Risiko. ✓ -- **Out-of-scope Side-Note:** `helpers/library/db.py:PatternDB.register_project` bleibt ungeprüft – separater Fix nötig, nicht Teil dieses Tickets. +- **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 v2 gelesen -- [ ] User gibt frei: Wechsel auf `implementation_allowed` +- [ ] User hat Plan v3 gelesen - [ ] User gibt frei: Implementierung über `implementation_engineer` Subagent -- [ ] Bug 1–6 adressiert -- [ ] 18 Call-Sites (10 Tools + 7 Helper + 1 Skript) angepasst +- [ ] 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) -- [ ] 15 Test-Cases geschrieben + grün -- [ ] Legacy-Migrations-Snippet erstellt + ausgeführt (leocrm bleibt aktiv) +- [ ] 17 Test-Cases geschrieben + grün +- [ ] Legacy-Migrations-Snippet erstellt + ausgeführt - [ ] Optional: `quality_reviewer` Re-Review nach Implementierung --- -**Geschätzter Aufwand:** 5–7 Subagent-Calls (1× Patch-Helper, 1× Migration-Tools, 1× Migration-Helpers, 1× Migration-Skripte, 1× Tests, 1× Legacy-Migration, 1× Final-Verify). +**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 → 18 korrigiert ✓ -- (b) Legacy-Migration: §3.6 + Test 14 ergänzt ✓ -- (c) PatternDB.register_project: out-of-scope dokumentiert ✓ +- (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 ✓ diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/docs/diff-v3-vs-original.patch /a0/usr/plugins/a0_software_orchestrator/docs/diff-v3-vs-original.patch --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/docs/diff-v3-vs-original.patch 1970-01-01 00:00:00.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/docs/diff-v3-vs-original.patch 2026-06-16 10:18:53.359469811 +0000 @@ -0,0 +1,349 @@ +diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/project_state_get.py /a0/usr/plugins/a0_software_orchestrator/api/project_state_get.py +--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/project_state_get.py 2026-06-15 14:24:02.000000000 +0000 ++++ /a0/usr/plugins/a0_software_orchestrator/api/project_state_get.py 2026-06-16 10:14:06.905148563 +0000 +@@ -13,7 +13,10 @@ + class ProjectStateGet(ApiHandler): + async def process(self, input: dict, request: Request) -> dict | Response: + project_root = input.get("project_root", os.getcwd()) +- project_name = input.get("project_name") or os.path.basename(project_root.rstrip("/")) ++ # Bug 4 Fix: basename-Fallback entfernt. project_name ist Pflicht. ++ project_name = input.get("project_name") ++ if not project_name: ++ return {"ok": False, "error": "project_name is required (basename fallback removed)"} + try: + pid = get_project_id(project_name) + if pid is None: +Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/__pycache__/project_state_get.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/api/__pycache__/project_state_get.cpython-313.pyc differ +diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/docs/bugfix-auto-registration.md /a0/usr/plugins/a0_software_orchestrator/docs/bugfix-auto-registration.md +--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/docs/bugfix-auto-registration.md 2026-06-16 09:19:29.000000000 +0000 ++++ /a0/usr/plugins/a0_software_orchestrator/docs/bugfix-auto-registration.md 2026-06-16 10:04:32.559404096 +0000 +@@ -1,44 +1,51 @@ + # Bugfix-Plan: Auto-Registration in `resolve_project()` + + **Datum:** 2026-06-16 +-**Status:** Plan v2 – CONDITIONAL vom Quality-Reviewer, 3 Blocker adressiert +-**Phase:** Plan-Mode `planning_only` ++**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: ++`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 explizite Pfad akzeptiert Müll | ++| 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, weil er als "Aktivitätsmarker" semantisch nützlich ist – wird aber in Tests explizit verifiziert. ++- `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. Das ist **bewusst restriktiv**, weil Software-Projektnamen sonst zu viel Variabilität erlauben. +-- Falls User Namen mit Punkten/Underscores braucht: Blacklist/Pattern explizit erweitern (Diskussion erforderlich). ++- **Nicht erlaubt:** Punkte, Underscores, Umlaute, Leerzeichen, Großbuchstaben. + + **Blacklist-Klärung:** +-- `a0-development` ist **bewusst** in der Blacklist. Falls es künftig doch als DB-Eintrag gewünscht ist: Blacklist-Eintrag entfernen + explizit `register_project("a0-development", ...)` aufrufen. Der Satz in §5 der ersten Plan-Version war missverständlich und ist hiermit korrigiert. ++- `a0-development` ist **bewusst** in der Blacklist. Wer es künftig als DB-Eintrag will: Blacklist-Eintrag entfernen + explizit `register_project("a0-development", ...)` aufrufen. + +-**Out-of-scope (explizit dokumentiert):** +-- `helpers/library/db.py` enthält eine separate Klasse `PatternDB.register_project()` (Z. 117, 142, 160, 222). Das ist ein anderer Code-Pfad (DB-Layer, nicht Project-Layer) und wird in diesem Fix **nicht** angefasst. Side-Note: zukünftiges Risiko, sollte in einem separaten Ticket adressiert werden. ++**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 + +@@ -57,8 +64,7 @@ + }) + + def _validate_project_name(name: str) -> str: +- """Strippt, prüft Pattern + Blacklist. Gibt bereinigten Namen zurück. +- Wirft ValueError bei Verletzung.""" ++ """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() +@@ -97,7 +103,6 @@ + f"Call register_project() or project_registry action=register first." + ) + project_id = int(row[0]) +- # Touch last_active_at (Aktivitätsmarker – gewünschter Side-Effect) + db.conn.execute( + """ + INSERT INTO project_state (project_id, status, phase, last_active_at) +@@ -118,7 +123,7 @@ + tech_stack: str = "{}", status: str = "active", + phase: str = "intake") -> int: + """Register or update a project. Returns project_id.""" +- name = _validate_project_name(name) # NEU ++ name = _validate_project_name(name) + # ... Rest wie bisher (INSERT … ON CONFLICT, project_state upsert, set_kv) + ``` + +@@ -163,7 +168,7 @@ + - `block_resume.py:54` + - `next_step.py:49` + - `orchestrator_state.py:48` +-- `plan_mode_guard.py:76` ++- `plan_mode_guard.py:76` → wird komplett umgebaut, siehe §3.7 + - `quality_gate.py:388` + - `repo_manifest.py:64` + - `resume_checker.py:40` +@@ -171,32 +176,22 @@ + - `tool_registry.py:50` + + **NICHT geändert** (verwenden bereits `get_project_id`): +-- `extensions/python/monologue_start/load_project_state.py:30` – Bestätigung, dass das der richtige Pfad ist. ++- `extensions/python/monologue_start/load_project_state.py:30` + + ### 3.3 Helper-Aufrufer migrieren (4 Helper-Dateien / 7 Call-Sites) + +-Gleiche Umstellung auf `get_project_id` + `LookupError`: +- + | 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) | +-| **Total** | **7 Call-Sites in 4 Dateien** | + + ### 3.4 `utils/migrate_a0_to_db.py` + + **Zeile 395–419 (basename-Fallback entfernen, Bug 3):** + + ```python +-# Vorher: +-if os.path.isabs(args.project) and os.path.isdir(args.project): +- project_root = args.project +- project_name = os.path.basename(project_root.rstrip("/")) +-... +- +-# Nachher: + if not args.project: + print("ERROR: --project= ist Pflicht (basename-Fallback entfernt)") + sys.exit(1) +@@ -210,17 +205,12 @@ + ``` + + **Zeile 213:** `pid = resolve_project(project_name)` → `pid = register_project(project_name, git_url="")` +-(wenn schon vorhanden: idempotent, schadet nicht. Wenn nicht: legt sauber an.) + + ### 3.5 `api/project_state_get.py` + + **Zeile 16 ersetzen (Bug 4):** + + ```python +-# Vorher: +-project_name = input.get("project_name") or os.path.basename(project_root.rstrip("/")) +- +-# Nachher: + project_name = input.get("project_name") + if not project_name: + return {"ok": False, "error": "project_name is required (basename fallback removed)"} +@@ -228,22 +218,74 @@ + + ### 3.6 Legacy-Daten-Migration (Blocker b) + +-**Problem:** Bestehende DB-Rows mit Namen außerhalb des neuen Patterns werden durch `_validate_project_name()` (aufgerufen in `resolve_project`) unsichtbar, weil die Validierung VOR dem SELECT kommt. ++**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. + +-**Lösungsansatz:** Einmaliges Migrations-Snippet in `utils/migrate_legacy_project_names.py` (neu): ++**Konkret (Pseudocode für `plan_mode_guard.py`):** + + ```python +-# Vereinfachte Logik: +-# 1) SELECT id, name FROM projects; +-# 2) Pro Row: prüfe ob name das Pattern matcht UND nicht in Blacklist ist +-# 3) Wenn nicht: setze status='archived' in project_state, logge WARN +-# 4) Bericht am Ende: "X archived, Y active" ++@_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() + +-# Im aktuellen Datenbestand ist nur leocrm vorhanden (matcht Pattern), +-# also leerer Archivierungs-Lauf zu erwarten. ++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) + ``` + +-**Aufruf:** Manuell oder automatisch beim Plugin-Start (einmalig, mit Flag `--already-migrated` zur Idempotenz). ++**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 + +@@ -255,57 +297,63 @@ + 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"` (gestrippt) ++4. `test_validate_project_name_whitespace_strip` – `" crm "` → `"crm"` + +-**register_project (3 Cases):** +-5. `test_register_project_new` – neues Projekt → id, in DB +-6. `test_register_project_idempotent` – zweimal registrieren mit gleichen Daten → gleiche id +-7. `test_register_project_value_error_wraps_validator` – invalider Name → ValueError vom Wrapper (nicht nur vom Validator) +-8. `test_register_project_idempotent_overwrite_semantics` – zweimal mit verschiedenen `git_url` → welche gewinnt? (Doku: COALESCE-Logik in Zeile 657-661) +- +-**resolve_project (3 Cases):** +-9. `test_resolve_project_existing` – registered project → id, last_active_at aktualisiert +-10. `test_resolve_project_missing_raises` – nicht registriert → LookupError (kein Auto-Register mehr!) +-11. `test_resolve_project_invalid_name_raises` – Name aus Blacklist → ValueError (bevor SELECT läuft) +-12. `test_resolve_project_touches_last_active_at` – zweimal rufen, last_active_at verändert sich ++**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` – nicht registriert → None ++13. `test_get_project_id_missing_returns_none` + + **Legacy-Migration (1 Case):** +-14. `test_legacy_row_with_invalid_name_is_archived` – INSERT einer Row mit Name `"OLD-PROJECT"` direkt in DB → Migrations-Run setzt status='archived' ++14. `test_legacy_row_with_invalid_name_is_archived` + + **End-to-End (1 Case):** +-15. `test_e2e_next_step_with_unknown_project_raises` – Aufruf von `next_step` mit unbekanntem Projekt → LookupError, **kein** DB-Eintrag enstanden ++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 alle 18 resolve_project-Aufrufer (10 Tools + 7 Helper-Sites + 1 Skript). Mitigation: LookupError-Handling an Aufrufstelle ist explizit und sichtbar. +-- **Legacy-Daten:** Bestehende Projekte mit Namen außerhalb des Patterns werden unsichtbar. Mitigation: Legacy-Migrations-Snippet in §3.6. +-- **Aktive User-Flows:** User-Dialoge, die heute `resolve_project` mit projektnamen aus User-Eingabe füttern, brechen jetzt hart mit LookupError statt Auto-Register. Mitigation: klarer Fehlertext "project '{name}' is not registered. Call register_project() or project_registry action=register first." → User kann selbst korrigieren. +-- **Plugin-Reload** mitten in laufendem Tool-Call: mixed old/new code. Minor. +-- **Concurrency:** SQLite single-writer, ON CONFLICT in register_project, kein neues Risiko. ✓ +-- **Out-of-scope Side-Note:** `helpers/library/db.py:PatternDB.register_project` bleibt ungeprüft – separater Fix nötig, nicht Teil dieses Tickets. ++- **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 v2 gelesen +-- [ ] User gibt frei: Wechsel auf `implementation_allowed` ++- [ ] User hat Plan v3 gelesen + - [ ] User gibt frei: Implementierung über `implementation_engineer` Subagent +-- [ ] Bug 1–6 adressiert +-- [ ] 18 Call-Sites (10 Tools + 7 Helper + 1 Skript) angepasst ++- [ ] 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) +-- [ ] 15 Test-Cases geschrieben + grün +-- [ ] Legacy-Migrations-Snippet erstellt + ausgeführt (leocrm bleibt aktiv) ++- [ ] 17 Test-Cases geschrieben + grün ++- [ ] Legacy-Migrations-Snippet erstellt + ausgeführt + - [ ] Optional: `quality_reviewer` Re-Review nach Implementierung + + --- + +-**Geschätzter Aufwand:** 5–7 Subagent-Calls (1× Patch-Helper, 1× Migration-Tools, 1× Migration-Helpers, 1× Migration-Skripte, 1× Tests, 1× Legacy-Migration, 1× Final-Verify). ++**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 → 18 korrigiert ✓ +-- (b) Legacy-Migration: §3.6 + Test 14 ergänzt ✓ +-- (c) PatternDB.register_project: out-of-scope dokumentiert ✓ ++- (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 ✓ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/message_loop_prompts_after/__pycache__/inject_context_budget.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/message_loop_prompts_after/__pycache__/inject_context_budget.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/monologue_end/__pycache__/remind_state_update.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/monologue_end/__pycache__/remind_state_update.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/monologue_start/__pycache__/load_project_state.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/monologue_start/__pycache__/load_project_state.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/system_prompt/__pycache__/_30_inject_profile_specifics.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/system_prompt/__pycache__/_30_inject_profile_specifics.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/system_prompt/__pycache__/inject_orchestrator_rules.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/system_prompt/__pycache__/inject_orchestrator_rules.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/tool_execute_before/__pycache__/guard_risky_actions.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/tool_execute_before/__pycache__/guard_risky_actions.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/extensions/python/tool_execute_before/__pycache__/guard_risky_actions.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/extensions/python/tool_execute_before/__pycache__/guard_risky_actions.cpython-313.pyc differ diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/artifact_rules.py /a0/usr/plugins/a0_software_orchestrator/helpers/artifact_rules.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/artifact_rules.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/helpers/artifact_rules.py 2026-06-16 10:11:38.560624907 +0000 @@ -42,7 +42,7 @@ def _check_db_artifacts(project_name: str) -> Dict[str, bool]: """Prüft die 5 DB-backed Artefakte via db_state_store.""" from .db_state_store import ( - resolve_project, + get_project_id, get_kv, get_status, list_worklog, @@ -50,7 +50,12 @@ list_next_steps, ) - pid = resolve_project(project_name) + pid = get_project_id(project_name) + if pid is None: + return { + "worklog": False, "todo": False, "project_state": False, + "current_status": False, "next_steps": False, + } return { "worklog": len(list_worklog(pid, limit=1)) > 0, diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/briefing_file.py /a0/usr/plugins/a0_software_orchestrator/helpers/briefing_file.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/briefing_file.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/helpers/briefing_file.py 2026-06-16 10:11:54.604763089 +0000 @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, + get_project_id, append_briefing, get_briefing, list_briefings, @@ -59,7 +59,9 @@ name = project_name.strip() if project_name else os.path.basename( (project_root or os.getcwd()).rstrip("/") ) - pid = resolve_project(name) + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") # Section-Markdown aufbauen (aus handover_protocol) from .handover_protocol import render_briefing_section @@ -131,5 +133,7 @@ name = project_name.strip() if project_name else os.path.basename( (project_root or os.getcwd()).rstrip("/") ) - pid = resolve_project(name) + pid = get_project_id(name) + if pid is None: + return [] return list_briefings(pid, specialist=specialist, limit=limit) diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/compact_protocol.py /a0/usr/plugins/a0_software_orchestrator/helpers/compact_protocol.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/compact_protocol.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/helpers/compact_protocol.py 2026-06-16 10:12:30.990344305 +0000 @@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, + get_project_id, list_next_steps, list_worklog, list_todos, @@ -174,7 +174,9 @@ # 6. next_steps nicht leer try: - pid = resolve_project(name) + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' not found in DB") pending_steps = list_next_steps(pid, status="pending") if not pending_steps: return { @@ -182,7 +184,7 @@ "reason": "orch_next_steps has no pending entries – next block not defined", "config": config, } - except ValueError: + except (ValueError, LookupError): return { "allowed": False, "forced": False, "reason": f"project {name!r} not found in DB", @@ -215,8 +217,10 @@ dict artifact_key -> exists (bool) """ try: - pid = resolve_project(project_name) - except ValueError: + pid = get_project_id(project_name) + if pid is None: + raise LookupError(f"project {project_name!r} not in DB") + except (ValueError, LookupError): # Projekt nicht in DB → alle Artefakte fehlen return {k: False for k in MANDATORY_ARTIFACTS} @@ -240,8 +244,8 @@ def get_resume_payload(project_name: str) -> Dict[str, Any]: """Liest alle Resume-relevanten Daten aus der DB für `block_resume.py`.""" try: - pid = resolve_project(project_name) - except ValueError: + pid = get_project_id(project_name) + except (ValueError, LookupError): pid = None if pid is None: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/db_state_store.py /a0/usr/plugins/a0_software_orchestrator/helpers/db_state_store.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/db_state_store.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/helpers/db_state_store.py 2026-06-16 10:08:25.622937781 +0000 @@ -5,17 +5,25 @@ Verwendung: from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, get_kv, set_kv, append_worklog, - set_status, add_next_step, add_todo, set_block_compact, - create_snapshot, append_briefing, add_score, + register_project, get_project_id, resolve_project, + get_kv, set_kv, append_worklog, set_status, add_next_step, + add_todo, set_block_compact, create_snapshot, + append_briefing, add_score, ) - pid = resolve_project("crm-system") + # Neues Projekt anlegen (explizit, mit Plausiprüfung): + pid = register_project("crm-system", git_url="https://...") set_kv(pid, "phase", "implementation") append_worklog(pid, phase="implementation", work_block="T012", agent="implementation_engineer", summary="Auth-Endpoint fertig") add_next_step(pid, "T013: Test schreiben") set_status(pid, "12/30 tasks done, Auth läuft") + + # Existierendes Projekt nachschlagen (nur lesen): + existing_pid = get_project_id("crm-system") # gibt None wenn unbekannt + + # In create-on-missing-Aufrufern (z.B. Migration): + pid = resolve_project("crm-system") # wirft LookupError wenn fehlt """ from __future__ import annotations @@ -30,6 +38,37 @@ # --------------------------------------------------------------------------- +# Project-Name Validation (Pattern + Blacklist) +# --------------------------------------------------------------------------- + +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 + + +# --------------------------------------------------------------------------- # Singleton-DB (selbe Instanz wie in library/db.py) # --------------------------------------------------------------------------- @@ -58,45 +97,33 @@ @_db_error_handler def resolve_project(name: str) -> int: - """Löst einen Projektnamen zur project_id auf. Auto-register falls unbekannt. + """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. Args: - name: Projektname (muss eindeutig sein, siehe projects.name UNIQUE) + name: Projektname (muss Pattern + Blacklist matchen) Returns: project_id (int) Raises: - ValueError: wenn Name leer ist + ValueError: wenn Name leer/ungültig (Pattern/Blacklist) + LookupError: wenn Name nicht in DB registriert """ - if not name or not name.strip(): - raise ValueError("project name must be non-empty") - name = name.strip() - + name = _validate_project_name(name) db = _db() row = db.conn.execute( "SELECT id FROM projects WHERE name = ?", (name,) ).fetchone() - if row: - 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,), + if not row: + raise LookupError( + f"project '{name}' is not registered. " + f"Call register_project() or project_registry action=register first." ) - db.conn.commit() - return project_id - - # Auto-register: legt einen Eintrag ohne git_url an. - # User kann später git_url via project_registry.update ergänzen. - cur = db.conn.execute( - "INSERT INTO projects (name, description) VALUES (?, ?)", - (name, f"Auto-registered by db_state_store at {now_iso()}") - ) - project_id = int(cur.lastrowid) + project_id = int(row[0]) db.conn.execute( """ INSERT INTO project_state (project_id, status, phase, last_active_at) @@ -123,9 +150,12 @@ def get_project_id(name: str) -> Optional[int]: """Return existing project_id without creating or mutating project state. - This is intended for read-only guards, startup summaries, and GET-style API - handlers. Use resolve_project() only in tools/actions that intentionally - create or update project state. + This is the preferred read-only lookup. For create-or-lookup semantics + use resolve_project(); to create explicitly use register_project(). + + Returns None when the project name is not registered. Note: This function + does NOT validate the name against pattern/blacklist; it returns None for + any unknown or invalid name (read-only, no side effects). """ if not name or not name.strip(): return None @@ -644,10 +674,12 @@ 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 with initial/current state. Returns project_id.""" - if not name or not name.strip(): - raise ValueError("project name must be non-empty") - name = name.strip() + """Register or update a project with initial/current state. Returns project_id. + + Validiert den Namen gegen Pattern + Blacklist (siehe _validate_project_name). + Wirft ValueError bei ungültigem Namen, ohne INSERT durchzuführen. + """ + name = _validate_project_name(name) db = _db() db.conn.execute( """ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/library/patterns.db-shm and /a0/usr/plugins/a0_software_orchestrator/helpers/library/patterns.db-shm differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/library/patterns.db-wal and /a0/usr/plugins/a0_software_orchestrator/helpers/library/patterns.db-wal differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/library/__pycache__/db.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/library/__pycache__/db.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/library/__pycache__/extractor.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/library/__pycache__/extractor.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/library/__pycache__/__init__.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/library/__pycache__/__init__.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/library/__pycache__/migrations.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/library/__pycache__/migrations.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/artifact_rules.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/artifact_rules.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/artifact_rules.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/artifact_rules.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/briefing_file.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/briefing_file.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/briefing_file.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/briefing_file.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/compact_protocol.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/compact_protocol.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/compact_protocol.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/compact_protocol.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/context_budget.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/context_budget.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/db_state_store.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/db_state_store.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/db_state_store.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/db_state_store.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/exceptions.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/exceptions.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/handover_protocol.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/handover_protocol.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/__init__.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/__init__.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/manifest_schema.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/manifest_schema.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/resume_state.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/resume_state.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/safety_rules.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/safety_rules.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/state_store.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/state_store.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/__pycache__/tool_capabilities.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/helpers/__pycache__/tool_capabilities.cpython-312.pyc differ diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/resume_state.py /a0/usr/plugins/a0_software_orchestrator/helpers/resume_state.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/resume_state.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/helpers/resume_state.py 2026-06-16 10:12:40.538021624 +0000 @@ -9,7 +9,7 @@ from typing import Any, Dict, Optional from usr.plugins.a0_software_orchestrator.helpers.compact_protocol import get_resume_payload -from usr.plugins.a0_software_orchestrator.helpers.db_state_store import resolve_project, create_snapshot as db_create_snapshot +from usr.plugins.a0_software_orchestrator.helpers.db_state_store import get_project_id, create_snapshot as db_create_snapshot def read_resume_info(project_name: str = "", project_root: str = "") -> Dict[str, Any]: @@ -58,8 +58,10 @@ """ name = project_name or os.path.basename(project_root or os.getcwd()) try: - pid = resolve_project(name) - except ValueError: + pid = get_project_id(name) + if pid is None: + return None + except (ValueError, LookupError): return None snap_id = db_create_snapshot(pid, trigger="resume_state.create_snapshot") return f"db://snapshots/{snap_id}" Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/__pycache__/hooks.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/__pycache__/hooks.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/__pycache__/__init__.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/__pycache__/__init__.cpython-312.pyc differ diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/block_compactor.py /a0/usr/plugins/a0_software_orchestrator/tools/block_compactor.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/block_compactor.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/block_compactor.py 2026-06-16 10:09:09.555054355 +0000 @@ -17,7 +17,7 @@ from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, now_iso, get_kv, set_kv, + get_project_id, now_iso, get_kv, set_kv, append_worklog, set_status, add_next_step, set_block_compact, get_block_compact, create_snapshot, collect_full_state, @@ -294,8 +294,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: return Response(message=f"Database error: {e}", break_loop=False) diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/block_resume.py /a0/usr/plugins/a0_software_orchestrator/tools/block_resume.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/block_resume.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/block_resume.py 2026-06-16 10:09:20.287815744 +0000 @@ -8,7 +8,7 @@ from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.compact_protocol import get_resume_payload from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, + get_project_id, get_status, list_next_steps, list_todos, list_errors, list_worklog, get_kv, get_block_compact, ) @@ -51,8 +51,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/next_step.py /a0/usr/plugins/a0_software_orchestrator/tools/next_step.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/next_step.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/next_step.py 2026-06-16 10:09:29.231450211 +0000 @@ -5,7 +5,7 @@ from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, + get_project_id, add_next_step, complete_next_step, list_next_steps, @@ -46,8 +46,10 @@ return Response(message="ERROR: 'action' required. Supported: add, list, complete", break_loop=False) name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: return Response(message=f"Database error: {e}", break_loop=False) diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/orchestrator_state.py /a0/usr/plugins/a0_software_orchestrator/tools/orchestrator_state.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/orchestrator_state.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/orchestrator_state.py 2026-06-16 10:10:36.398215066 +0000 @@ -6,7 +6,7 @@ from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, + get_project_id, get_kv, set_kv, delete_kv, list_kv_keys, set_status as db_set_status, get_status as db_get_status, ) @@ -45,8 +45,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/plan_mode_guard.py /a0/usr/plugins/a0_software_orchestrator/tools/plan_mode_guard.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/plan_mode_guard.py 2026-06-15 21:03:27.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/plan_mode_guard.py 2026-06-16 10:14:34.924136247 +0000 @@ -1,21 +1,13 @@ -"""Tool: enforce Plan Mode by checking current mode (DB-backed).""" +"""Tool: enforce Plan Mode by checking current mode (DB-backed, global).""" from __future__ import annotations import json -import os from typing import Any, Dict from helpers.tool import Tool, Response -from usr.plugins.a0_software_orchestrator.helpers.db_state_store import resolve_project, get_kv, set_kv -from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError - - -def _resolve_name(project_name: str = "", project_root: str = "") -> str: - if project_name: - return project_name.strip() - if project_root: - return os.path.basename(project_root.rstrip("/")) - return os.path.basename(os.getcwd()) +from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( + _db_error_handler, _db, +) _DEFAULT_MODE = { @@ -38,6 +30,11 @@ "maintenance_allowed": {"allowed_actions": ["code", "test", "commit", "deploy"], "blocked_actions": ["destroy"]}, } +# Plan-Mode ist plugin-weit (Bug 7 Fix). Der Key lebt in orch_kv mit +# project_id=0 als globalem Platzhalter, NICHT pro Projekt. +_PLUGIN_MODE_PROJECT_ID = 0 +_PLUGIN_MODE_KEY = "orchestrator_mode" + def _normalize_mode(mode_data: Any) -> Dict[str, Any]: """Stellt sicher, dass das gelesene Mode-Dict die erwarteten Keys hat.""" @@ -52,33 +49,57 @@ return out +@_db_error_handler +def _get_plugin_mode() -> Dict[str, Any]: + """Liest den globalen Plan-Mode aus orch_kv (project_id=0). + + Returns _DEFAULT_MODE wenn kein Eintrag existiert. + """ + db = _db() + row = db.conn.execute( + "SELECT value_json FROM orch_kv " + "WHERE project_id = ? AND key = ?", + (_PLUGIN_MODE_PROJECT_ID, _PLUGIN_MODE_KEY), + ).fetchone() + if not row: + return dict(_DEFAULT_MODE) + try: + return _normalize_mode(json.loads(row[0])) + except (json.JSONDecodeError, TypeError): + return dict(_DEFAULT_MODE) + + +@_db_error_handler +def _set_plugin_mode(mode_data: Dict[str, Any]) -> None: + """Schreibt den globalen Plan-Mode in orch_kv (project_id=0).""" + db = _db() + db.conn.execute( + "INSERT INTO orch_kv (project_id, key, value_json, updated_at) " + "VALUES (?, ?, ?, datetime('now')) " + "ON CONFLICT(project_id, key) DO UPDATE SET " + " value_json = excluded.value_json, updated_at = datetime('now')", + (_PLUGIN_MODE_PROJECT_ID, _PLUGIN_MODE_KEY, json.dumps(mode_data)), + ) + db.conn.commit() + + class PlanModeGuard(Tool): - """Prüft den aktuellen Plan-Mode aus orch_kv und entscheidet, ob eine + """Prüft den aktuellen Plan-Mode (plugin-weit) und entscheidet, ob eine Aktion erlaubt ist. + KEIN resolve_project()-Aufruf mehr: Plan-Mode ist global, nicht pro Projekt. + Args (kwargs): - project_name (str): Projektname (bevorzugt). - project_root (str): Legacy – wird zu basename() aufgelöst. requested_action (str): Action-Name zum Testen (z.B. "code", "deploy"). new_mode (str): Optional – neuen Mode setzen (für Mode-Transitions). """ async def execute( self, - project_name: str = "", - project_root: str = "", requested_action: str = "", new_mode: str = "", **kwargs, ): - name = _resolve_name(project_name, project_root) - try: - pid = resolve_project(name) - except ValueError as e: - return Response(message=f"ERROR: {e}", break_loop=False) - - except PluginDBError as e: - return Response(message=f"Database error: {e}", break_loop=False) # Mode-Transition (z.B. planning_only → implementation_allowed) if new_mode: if new_mode not in _MODE_POLICIES: @@ -90,18 +111,16 @@ }, indent=2), break_loop=False, ) - mode_data = _normalize_mode(get_kv(pid, "orchestrator_mode", default={})) - mode_data["mode"] = new_mode - mode_data["allowed_actions"] = list(_MODE_POLICIES[new_mode]["allowed_actions"]) - mode_data["blocked_actions"] = list(_MODE_POLICIES[new_mode]["blocked_actions"]) - set_kv(pid, "orchestrator_mode", mode_data) - # Bug 5 fix: return a JSON verdict so callers can detect success/failure - # structurally instead of parsing a free-form prose message. + 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, - "project": name, "allowed_actions": mode_data["allowed_actions"], "blocked_actions": mode_data["blocked_actions"], "transitioned": True, @@ -110,19 +129,19 @@ ) # Ansonsten: aktuellen Mode lesen und Aktion prüfen - mode_data = _normalize_mode(get_kv(pid, "orchestrator_mode", default={})) + mode_data = _get_plugin_mode() current_mode = mode_data["mode"] allowed = mode_data["allowed_actions"] blocked = mode_data["blocked_actions"] - # Optional: Wenn ein default-mode gewünscht und nichts gesetzt → zurückschreiben - if not get_kv(pid, "orchestrator_mode"): - set_kv(pid, "orchestrator_mode", _DEFAULT_MODE) - if requested_action and requested_action in blocked: return Response( - message=f"BLOCKED: action '{requested_action}' is not allowed " - f"in mode '{current_mode}' for project {name!r}.", + message=json.dumps({ + "verdict": "BLOCKED", + "mode": current_mode, + "action": requested_action, + "reason": f"action '{requested_action}' is not allowed in mode '{current_mode}'", + }, indent=2), break_loop=False, ) if not requested_action or requested_action in allowed: @@ -131,7 +150,6 @@ "verdict": "ALLOWED", "mode": current_mode, "action": requested_action, - "project": name, }, indent=2), break_loop=False, ) @@ -140,7 +158,6 @@ "verdict": "UNKNOWN", "mode": current_mode, "action": requested_action, - "project": name, "hint": f"action not in allowed={allowed} or blocked={blocked}", }, indent=2), break_loop=False, Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/artifact_guard.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/artifact_guard.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/block_compactor.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/block_compactor.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/block_compactor.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/block_compactor.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/block_resume.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/block_resume.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/block_resume.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/block_resume.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/capability_check.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/capability_check.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/context_compactor.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/context_compactor.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/handover_to_specialist.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/handover_to_specialist.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/next_step.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/next_step.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/next_step.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/next_step.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/orchestrator_self_check.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/orchestrator_self_check.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/orchestrator_state.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/orchestrator_state.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/orchestrator_state.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/orchestrator_state.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/plan_mode_guard.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/plan_mode_guard.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/plan_mode_guard.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/plan_mode_guard.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/project_registry.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/project_registry.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/quality_gate.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/quality_gate.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/quality_gate.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/quality_gate.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/read_briefing.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/read_briefing.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/repo_manifest.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/repo_manifest.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/repo_manifest.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/repo_manifest.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/resume_checker.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/resume_checker.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/resume_checker.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/resume_checker.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/scorecard_update.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/scorecard_update.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/scorecard_update.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/scorecard_update.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/tool_registry.cpython-312.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/tool_registry.cpython-312.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/__pycache__/tool_registry.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/tools/__pycache__/tool_registry.cpython-313.pyc differ diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/quality_gate.py /a0/usr/plugins/a0_software_orchestrator/tools/quality_gate.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/quality_gate.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/quality_gate.py 2026-06-16 10:09:58.271510332 +0000 @@ -33,7 +33,7 @@ from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( DB_PATH, - resolve_project, + get_project_id, get_kv, set_kv, ) @@ -383,10 +383,12 @@ if not name: return Response(message=json.dumps({"error": "no project_name or project_root given"}), break_loop=False) - # resolve project_id (auto-create if not in registry) + # resolve project_id (no auto-create; LookupError on unknown name) try: - project_id = resolve_project(name) - except ValueError as e: + project_id = get_project_id(name) + if project_id is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=json.dumps({"error": str(e)}), break_loop=False) except PluginDBError as e: return Response(message=json.dumps({"error": f"Database error: {e}"}), break_loop=False) diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/repo_manifest.py /a0/usr/plugins/a0_software_orchestrator/tools/repo_manifest.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/repo_manifest.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/repo_manifest.py 2026-06-16 10:10:13.349579981 +0000 @@ -6,7 +6,7 @@ from typing import Any, Dict from helpers.tool import Tool, Response -from usr.plugins.a0_software_orchestrator.helpers.db_state_store import resolve_project, get_kv, set_kv, list_kv_keys +from usr.plugins.a0_software_orchestrator.helpers.db_state_store import get_project_id, get_kv, set_kv, list_kv_keys from usr.plugins.a0_software_orchestrator.helpers.manifest_schema import PROJECT_MANIFEST_SCHEMA, DEPLOYMENT_MANIFEST_SCHEMA from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError @@ -61,8 +61,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/resume_checker.py /a0/usr/plugins/a0_software_orchestrator/tools/resume_checker.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/resume_checker.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/resume_checker.py 2026-06-16 10:10:57.286696908 +0000 @@ -5,7 +5,7 @@ from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, + get_project_id, get_kv, get_status, list_next_steps, list_todos, list_errors, get_block_compact, ) @@ -37,8 +37,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/scorecard_update.py /a0/usr/plugins/a0_software_orchestrator/tools/scorecard_update.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/scorecard_update.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/scorecard_update.py 2026-06-16 10:11:13.415841120 +0000 @@ -4,7 +4,7 @@ import os from helpers.tool import Tool, Response -from usr.plugins.a0_software_orchestrator.helpers.db_state_store import resolve_project, add_score, list_scores +from usr.plugins.a0_software_orchestrator.helpers.db_state_store import get_project_id, add_score, list_scores from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError @@ -40,8 +40,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/tool_registry.py /a0/usr/plugins/a0_software_orchestrator/tools/tool_registry.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/tool_registry.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/tools/tool_registry.py 2026-06-16 10:11:27.807862100 +0000 @@ -7,7 +7,7 @@ from typing import Any, Dict from helpers.tool import Tool, Response -from usr.plugins.a0_software_orchestrator.helpers.db_state_store import resolve_project, set_kv, get_kv +from usr.plugins.a0_software_orchestrator.helpers.db_state_store import get_project_id, set_kv, get_kv from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError @@ -47,8 +47,10 @@ ): name = _resolve_name(project_name, project_root) try: - pid = resolve_project(name) - except ValueError as e: + pid = get_project_id(name) + if pid is None: + raise LookupError(f"project '{name}' is not registered") + except (ValueError, LookupError) as e: return Response(message=f"ERROR: {e}", break_loop=False) except PluginDBError as e: diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/migrate_a0_to_db.py /a0/usr/plugins/a0_software_orchestrator/utils/migrate_a0_to_db.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/migrate_a0_to_db.py 2026-06-15 14:24:02.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/utils/migrate_a0_to_db.py 2026-06-16 10:13:50.779004564 +0000 @@ -34,7 +34,7 @@ # Example: cd /a0 && /opt/venv-a0/bin/python /a0/usr/plugins/a0_software_orchestrator/utils/migrate_a0_to_db.py from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( - resolve_project, set_kv, append_worklog, set_status, + register_project, set_kv, append_worklog, set_status, add_todo, add_next_step, set_block_compact, create_snapshot, add_score, append_briefing, now_iso, @@ -209,8 +209,14 @@ print(f" Migrating project {project_name!r} from {project_root}") - # Projekt registrieren (oder existierendes behalten) - pid = resolve_project(project_name) + # Projekt registrieren (oder existierendes behalten) – mit Pattern+Blacklist-Plausi. + # Bug 1/2 Fix: explizit register_project() statt auto-registrierendem resolve_project(). + try: + pid = register_project(project_name, git_url="") + except ValueError as e: + print(f"ERROR: invalid project name {project_name!r}: {e}") + print(" Bitte --project= verwenden (Pattern: ^[a-z][a-z0-9-]{1,40}$)") + sys.exit(1) print(f" Project id: {pid}") # 1. KV-Dateien: project_state.json, orchestrator_mode.json, tool_capabilities.json, … @@ -393,9 +399,17 @@ args = ap.parse_args() # Bestimme project_name + project_root + # Bug 3 Fix: stiller basename-Fallback entfernt. Wenn --project= + # übergeben wird, wird basename genutzt MIT sichtbarem WARN-Hinweis, + # damit der User den Projektnamen explizit setzen kann. + if not args.project: + print("ERROR: --project= ist Pflicht (basename-Fallback entfernt)") + sys.exit(1) if os.path.isabs(args.project) and os.path.isdir(args.project): project_root = args.project project_name = os.path.basename(project_root.rstrip("/")) + print(f"WARN: --project ist absoluter Pfad; leite project_name={project_name!r} aus basename ab.") + print(f" Besser: explizit --project={project_name} --project-root={project_root} setzen.") elif args.project_root: project_root = args.project_root project_name = args.project diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/migrate_legacy_project_names.py /a0/usr/plugins/a0_software_orchestrator/utils/migrate_legacy_project_names.py --- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/migrate_legacy_project_names.py 1970-01-01 00:00:00.000000000 +0000 +++ /a0/usr/plugins/a0_software_orchestrator/utils/migrate_legacy_project_names.py 2026-06-16 10:15:07.578452765 +0000 @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""migrate_legacy_project_names – archiviert Projekte mit ungültigen Namen. + +Bug 1+2 Begleit-Skript (Plan v3 §3.6): +- Iteriert über alle Projekte in der DB. +- Prüft jeden Namen gegen PROJECT_NAME_PATTERN + FORBIDDEN_PROJECT_NAMES. +- Markiert ungültige Namen mit status='archived' in project_state (idempotent). +- Bestehende 'active'-Projekte mit ungültigem Namen werden zu 'archived'. +- Bereits archivierte werden NICHT doppelt verändert (idempotent). + +Aufruf: + python3 utils/migrate_legacy_project_names.py [--dry-run] [--verbose] + +Defaults: +- --dry-run: nur Report, keine DB-Schreibzugriffe. +- --verbose: zusätzlich pro Projekt-Status ausgeben. + +Exit-Code: +- 0: Erfolg (inkl. 0 archivierte Projekte). +- 1: DB-Fehler. +""" +from __future__ import annotations + +import argparse +import os +import sys +from typing import List, Tuple + +PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Run from /a0 to have usr.plugins.* resolvable. +from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( + _db, _db_error_handler, _validate_project_name, + PROJECT_NAME_PATTERN, FORBIDDEN_PROJECT_NAMES, +) +from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError + + +@_db_error_handler +def list_projects_with_states() -> List[Tuple[int, str, str]]: + """Returns Liste von (project_id, project_name, current_status). + + Liest aus projects JOIN project_state. Projekte ohne project_state-Eintrag + bekommen den Status ''. + """ + db = _db() + rows = db.conn.execute(""" + SELECT p.id, p.name, COALESCE(ps.status, '') as status + FROM projects p + LEFT JOIN project_state ps ON ps.project_id = p.id + ORDER BY p.id + """).fetchall() + return [(int(r[0]), str(r[1]), str(r[2])) for r in rows] + + +@_db_error_handler +def archive_project(project_id: int, reason: str) -> None: + """Markiert ein Projekt als 'archived' in project_state. Idempotent.""" + db = _db() + # Stelle sicher, dass ein project_state-Eintrag existiert. + db.conn.execute(""" + INSERT INTO project_state (project_id, status, phase, last_active_at) + VALUES (?, 'archived', 'archived', datetime('now')) + ON CONFLICT(project_id) DO UPDATE SET + status = 'archived', + phase = 'archived', + last_active_at = datetime('now'), + updated_at = datetime('now') + """, (project_id,)) + db.conn.execute(""" + UPDATE projects SET updated_at = datetime('now') WHERE id = ? + """, (project_id,)) + db.conn.commit() + sys.stderr.write(f" [ARCHIVED] project_id={project_id} reason={reason}\n") + + +def main() -> int: + ap = argparse.ArgumentParser( + description="Archiviere Projekte mit ungültigen Namen (Pattern/Blacklist)." + ) + ap.add_argument( + "--dry-run", action="store_true", + help="Nur anzeigen, welche Projekte archiviert würden; keine DB-Schreibzugriffe.", + ) + ap.add_argument( + "--verbose", "-v", action="store_true", + help="Zusätzlich pro Projekt den Validierungs-Status ausgeben.", + ) + args = ap.parse_args() + + try: + projects = list_projects_with_states() + except PluginDBError as e: + print(f"ERROR: DB-Fehler beim Lesen der Projekte: {e}", file=sys.stderr) + return 1 + + if not projects: + print("No projects found in DB.") + return 0 + + print(f"=== Legacy-Name Migration (dry-run={args.dry_run}) ===") + print(f"Pattern: {PROJECT_NAME_PATTERN.pattern}") + print(f"Blacklist: {sorted(FORBIDDEN_PROJECT_NAMES)}") + print(f"Projects in DB: {len(projects)}") + print() + + active_count = 0 + archived_count = 0 + already_archived_count = 0 + invalid_count = 0 + + for pid, name, current_status in projects: + try: + _validate_project_name(name) + valid = True + invalid_reason = None + except ValueError as e: + valid = False + invalid_reason = str(e) + + if valid: + active_count += 1 + if args.verbose: + print(f" [OK ] pid={pid:3d} name={name!r:40s} status={current_status}") + continue + + invalid_count += 1 + if current_status == "archived": + already_archived_count += 1 + print(f" [SKIP] pid={pid:3d} name={name!r} already archived; invalid={invalid_reason}") + continue + + print(f" [BAD ] pid={pid:3d} name={name!r} status={current_status} reason={invalid_reason}") + if not args.dry_run: + try: + archive_project(pid, reason=invalid_reason) + archived_count += 1 + except PluginDBError as e: + print(f"ERROR: konnte pid={pid} nicht archivieren: {e}", file=sys.stderr) + return 1 + + print() + print(f"Summary:") + print(f" total projects: {len(projects)}") + print(f" valid (active): {active_count}") + print(f" invalid (total): {invalid_count}") + print(f" newly archived: {archived_count}") + print(f" already archived: {already_archived_count}") + print(f" mode: {'DRY-RUN' if args.dry_run else 'APPLIED'}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/__pycache__/migrate_a0_to_db.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/utils/__pycache__/migrate_a0_to_db.cpython-313.pyc differ Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/__pycache__/migrate_legacy_project_names.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/utils/__pycache__/migrate_legacy_project_names.cpython-313.pyc differ