Files
a0_software_orchestrator/docs/diff-v3-vs-original.patch
T

1681 lines
99 KiB
Diff
Raw Normal View History

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