Files
Software Orchestrator 5769c1cd22 Initial commit: a0_software_orchestrator v1.0
- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung)
- 25 Tests gruen (Pytest)
- block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block)
- 4 Restbaustellen gefixt
- DB-Schema: plugin_settings-Tabelle hinzugefuegt
- 3 Schattenprojekte aus DB geloescht
- Plan v3 + Refactor-Plan + Worklog dokumentiert
2026-06-16 22:13:06 +00:00

1681 lines
99 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 60109 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 60109 ruft bei unbekanntem Namen **automatisch** ein `INSERT INTO projects` auf, ohne Plausiprüfung. Sechs Folge-Bugs + ein Siebter (Fund bei Modus-Wechsel):
| # | Bug | Datei / Zeile | Folge |
|---|-----|--------------|-------|
| 1 | `resolve_project()` akzeptiert jeden String | `db_state_store.py:60-109` | Schattenprojekte wie `a0`, `a0-development`, `workdir` (3 bereits gelöscht) |
-| 2 | `register_project()` ohne Plausiprüfung | `db_state_store.py:644-687` | Auch der 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, 241 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 395419 (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=<name> 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 16 adressiert
-- [ ] 18 Call-Sites (10 Tools + 7 Helper + 1 Skript) angepasst
+- [ ] Bug 17 adressiert (inkl. Bug 7: plan_mode_guard-Umbau)
+- [ ] 17 Call-Sites (9 Tools + 7 Helper + 1 Skript) angepasst
+- [ ] `plan_mode_guard.py` umgebaut (Bug 7, §3.7)
- [ ] 2 basename-Fallbacks entfernt
- [ ] Modul-Docstring aktualisiert (Bug 6)
-- [ ] 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:** 57 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:** 56 Subagent-Calls (1× großer Patch-Block via implementation_engineer, 1× Tests via test_debug_engineer, 1× Quality-Review, 1× Final-Verify).
**Reviewer-Verdict:** CONDITIONAL → nach Einarbeitung dieser 3 Blocker freigabefähig.
-- (a) Call-Site-Inventur: 14 → 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 60109 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 60109 ruft bei unbekanntem Namen **automatisch** ein `INSERT INTO projects` auf, ohne Plausiprüfung. Sechs Folge-Bugs + ein Siebter (Fund bei Modus-Wechsel):
+
+ | # | Bug | Datei / Zeile | Folge |
+ |---|-----|--------------|-------|
+ | 1 | `resolve_project()` akzeptiert jeden String | `db_state_store.py:60-109` | Schattenprojekte wie `a0`, `a0-development`, `workdir` (3 bereits gelöscht) |
+-| 2 | `register_project()` ohne Plausiprüfung | `db_state_store.py:644-687` | Auch der 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, 241 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 395419 (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=<name> 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 16 adressiert
+-- [ ] 18 Call-Sites (10 Tools + 7 Helper + 1 Skript) angepasst
++- [ ] Bug 17 adressiert (inkl. Bug 7: plan_mode_guard-Umbau)
++- [ ] 17 Call-Sites (9 Tools + 7 Helper + 1 Skript) angepasst
++- [ ] `plan_mode_guard.py` umgebaut (Bug 7, §3.7)
+ - [ ] 2 basename-Fallbacks entfernt
+ - [ ] Modul-Docstring aktualisiert (Bug 6)
+-- [ ] 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:** 57 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:** 56 Subagent-Calls (1× großer Patch-Block via implementation_engineer, 1× Tests via test_debug_engineer, 1× Quality-Review, 1× Final-Verify).
+
+ **Reviewer-Verdict:** CONDITIONAL → nach Einarbeitung dieser 3 Blocker freigabefähig.
+-- (a) Call-Site-Inventur: 14 → 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 <project_root>
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=<gültiger Name> 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=<abs path>
+ # übergeben wird, wird basename genutzt MIT sichtbarem WARN-Hinweis,
+ # damit der User den Projektnamen explizit setzen kann.
+ if not args.project:
+ print("ERROR: --project=<name> 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 '<none>'.
+ """
+ db = _db()
+ rows = db.conn.execute("""
+ SELECT p.id, p.name, COALESCE(ps.status, '<none>') 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