Files
bewerb/docs/superpowers/plans/2026-06-12-projekt-anlegen-erweiterung.md
tlg 23b5d37ce3 Implementierungsplan für projekt-anlegen-Erweiterung hinzufügen
4 Tasks: rahmenbedingungen.md, CLAUDE.md-Verweis, Skill-Erweiterung
(Extraktion + Schritt 3b Geokodierung + Review + CRM-Ablauf Firma→Kontakt→VC),
Abschlussverifikation mit Geokodierungs-Regressionscheck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:01:02 +02:00

419 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# projekt-anlegen-Erweiterung Implementierungsplan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Den Skill `projekt-anlegen` um automatische Rahmenbedingungs-Bewertung, Extraktion von Käufer-Typ/Ansprechperson/Firma und den CRM-Schreibablauf Firma→Kontakt→Verkaufschance (mit Dedup und korrekter Verknüpfung) erweitern.
**Architecture:** Drei Dateien: neue Vorgaben-Datei `vorgaben/rahmenbedingungen.md` (Bewertungskriterien als Daten), `CLAUDE.md` (Verweis), und der Skill `.claude/skills/projekt-anlegen/SKILL.md` (Logik). Keine Testsuite — Verifikation über Datei-/Frontmatter-Prüfungen, eine Live-Geokodierung und CRM-Lesetests. CRM-Felder sind gegen die API verifiziert.
**Tech Stack:** Markdown-Skill, EspoCRM REST (curl, Key aus `.secrets/espocrm-api.md`), Nominatim-Geokodierung + Haversine (python3), Git (sha256).
**Referenz-Spec:** `docs/superpowers/specs/2026-06-12-projekt-anlegen-erweiterung-design.md`
Arbeitsverzeichnis für alle Befehle: `/home/tlg/mkt/bewerb`.
---
## Task 1: Vorgaben-Datei `vorgaben/rahmenbedingungen.md`
**Files:**
- Create: `vorgaben/rahmenbedingungen.md`
- [ ] **Step 1: Datei schreiben**
```markdown
# Rahmenbedingungen (Misc-Bewertung)
Feste Kriterien für die automatische Bewertung von Rahmenbedingungen (Kategorie Misc)
durch den Skill `projekt-anlegen`. Ändern sich selten.
## Kriterien
| Dimension | Vorgabe | Bewertung |
|---|---|---|
| Verfügbar ab | sofort | Projektstart ≤ 8 Wochen ab heute → ✅; > 8 Wochen → ❌; nicht genannt → ❔ |
| Auslastung | 100 % (Vollzeit) | 75100 % → ✅; < 75 % → ❔; nicht genannt → ❔ |
| Laufzeit | keine Einschränkung | jede Laufzeit → ✅ |
| Einsatzort / Remote | 100 % Remote oder Onsite ≤ 50 km um Sauerlach | siehe Einsatzort-Regel |
## Einsatzort-Regel
- 100 % Remote → ✅ (Ort egal).
- Onsite/Hybrid, Ort ≤ 50 km Luftlinie um Sauerlach → ✅.
- Onsite/Hybrid, Ort 5060 km (grenzwertig) → ❔.
- Onsite/Hybrid, Ort > 60 km → ❌.
- Einsatzort/Remote-Anteil unklar oder nicht genannt → ❌.
Distanz = Luftlinie zwischen Onsite-Ort und Sauerlach (47,9721 N / 11,6528 O),
bestimmt per Geokodierung (Nominatim/OpenStreetMap, nur Deutschland) und Haversine.
Unbekannter/mehrdeutiger Ort → ❔.
Hinweis: „heute" = aktuelles Datum der Session. „8 Wochen" = 56 Tage.
```
- [ ] **Step 2: Verifizieren**
Run: `grep -c "→" vorgaben/rahmenbedingungen.md`
Expected: ≥ 8 (alle Bewertungsregeln vorhanden).
- [ ] **Step 3: Commit**
```bash
git add vorgaben/rahmenbedingungen.md
git commit -m "Vorgaben-Datei rahmenbedingungen.md (Misc-Bewertungskriterien) anlegen"
```
---
## Task 2: `CLAUDE.md` um Rahmenbedingungen ergänzen
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: vorgaben-Zeile ergänzen**
Ersetze die Zeile:
```
- `vorgaben/` — feste Quellen: `Lebenslauf_Dr-Ing_Thomas_Langer.md`, `marketing.md`. Referenz; nicht ohne Auftrag ändern.
```
durch:
```
- `vorgaben/` — feste Quellen: `Lebenslauf_Dr-Ing_Thomas_Langer.md`, `marketing.md`, `rahmenbedingungen.md` (Misc-Kriterien für `projekt-anlegen`). Referenz; nicht ohne Auftrag ändern.
```
- [ ] **Step 2: Verifizieren**
Run: `grep -c "rahmenbedingungen.md" CLAUDE.md`
Expected: `1`
- [ ] **Step 3: Commit**
```bash
git add CLAUDE.md
git commit -m "CLAUDE.md: rahmenbedingungen.md als Vorgabe ergänzen"
```
---
## Task 3: Skill `projekt-anlegen` erweitern
**Files:**
- Modify: `.claude/skills/projekt-anlegen/SKILL.md`
Alle Edits sind exakte Ersetzungen am bestehenden Skill. Reihenfolge einhalten.
- [ ] **Step 1: Pipeline-Zeile und Voraussetzungen erweitern**
Ersetze:
```
Pipeline: Ausschreibung beschaffen → Anforderungen extrahieren → einordnen → gegen Lebenslauf bewerten → Matches berechnen → **Review im Chat (Pflicht)** → als Opportunity ins EspoCRM schreiben (per Shell) → verifizieren.
## Voraussetzungen und feste Pfade
| Zweck | Pfad |
|---|---|
| Lebenslauf (Markdown) | `vorgaben/Lebenslauf_Dr-Ing_Thomas_Langer.md` |
| EspoCRM-Zugangsdaten | `.secrets/espocrm-api.md` |
Beide Dateien mit dem Read-Tool lesen. Ohne Lebenslauf keine Bewertung, ohne Zugangsdaten kein CRM-Eintrag.
```
durch:
```
Pipeline: Ausschreibung beschaffen → Anforderungen + Käufer/Firma/Kontakt extrahieren → einordnen → gegen Lebenslauf bewerten → Rahmenbedingungen gegen Vorgaben bewerten → Matches berechnen → **Review im Chat (Pflicht)** → Firma → Kontakt → Verkaufschance ins EspoCRM schreiben (per Shell) → verifizieren.
## Voraussetzungen und feste Pfade
| Zweck | Pfad |
|---|---|
| Lebenslauf (Markdown) | `vorgaben/Lebenslauf_Dr-Ing_Thomas_Langer.md` |
| Rahmenbedingungen (Misc-Bewertung) | `vorgaben/rahmenbedingungen.md` |
| EspoCRM-Zugangsdaten | `.secrets/espocrm-api.md` |
Alle drei mit dem Read-Tool lesen. Ohne Lebenslauf keine fachliche Bewertung, ohne Rahmenbedingungen keine automatische Misc-Bewertung, ohne Zugangsdaten kein CRM-Eintrag.
```
- [ ] **Step 2: Schritt 1 (Extraktion) erweitern**
Ersetze den Block ab `## Schritt 1: Ausschreibung beschaffen` bis einschließlich der Aufzählung mit `Rahmenbedingungen (Start, Einsatzort/Remote-Anteil, Auslastung, Laufzeit) zählen als Anforderungen der Kategorie Misc.` durch:
```
## Schritt 1: Ausschreibung beschaffen und Eckdaten extrahieren
Die Projektausschreibung kommt auf zwei Wegen:
- **URL:** Nennt der Nutzer eine URL, hole den Seitentext mit dem **WebFetch-Tool**.
- **Text:** Fügt der Nutzer den Ausschreibungstext direkt in den Chat ein, nutze diesen.
Bei Login-geschützten Portalen ist das Einfügen von Text nötig. Ist unklar, welche Quelle gilt, nachfragen.
Daraus entnehmen:
- **Projektname**: der Titel der Ausschreibung, ohne Portal-Zusatz (z. B. ohne „auf www.freelancermap.de").
- **Projekt-URL**: die tatsächliche URL der Seite, falls vorhanden.
- **Beschreibung und Aufgaben**: nur Kontext für die Bewertung, keine Tabellenzeilen.
- **Anforderungen**: jede Anforderung einzeln, im Originalwortlaut (behutsam kürzen ist erlaubt, Bedeutung nie verändern). Rahmenbedingungen (Start, Einsatzort/Remote-Anteil, Auslastung, Laufzeit) zählen als Anforderungen der Kategorie Misc.
- **Käufer-Typ**: Agentur (Wiederverkäufer) ODER Endkunde (Direktauftrag).
- Agentur-Signale: bekannte Personaldienstleister/Vermittler (Hays, GULP/Randstad, SThree/Computer Futures, Aristo …), Formulierungen wie „im Auftrag unseres Kunden", „für unseren Kunden", Vermittler-Kontext des Portals.
- Nicht eindeutig bestimmbar → im Review nachfragen.
- **Firmenname**: Name der Agentur bzw. des Endkunden.
- **Ansprechperson**: vollständiger Name, falls genannt.
```
- [ ] **Step 3: Misc-Bullet in Schritt 3 auf Schritt 3b umlenken**
Ersetze:
```
- Rahmenbedingungen (Misc): Ortsbezug kann der Lebenslauf beantworten (Büro in Unterhaching bei München). Verfügbarkeit, Auslastung und Laufzeit kann er nicht beantworten → ❔ vorschlagen, Thomas setzt im Review ✅ oder ❌.
```
durch:
```
- Rahmenbedingungen (Misc) werden NICHT hier, sondern in Schritt 3b automatisch gegen `vorgaben/rahmenbedingungen.md` bewertet.
```
- [ ] **Step 4: Neuen Schritt 3b vor Schritt 4 einfügen**
Füge direkt vor der Zeile `## Schritt 4: Match-Berechnung` ein:
````
## Schritt 3b: Rahmenbedingungen automatisch bewerten (Misc)
`vorgaben/rahmenbedingungen.md` lesen und jede Misc-Anforderung danach bewerten:
- **Verfügbar ab / Projektstart:** Start ≤ heute + 8 Wochen (56 Tage) → ✅; > 8 Wochen → ❌; nicht genannt → ❔. („heute" = aktuelles Datum.)
- **Auslastung:** 75100 % → ✅; < 75 % → ❔; nicht genannt → ❔.
- **Laufzeit:** jede → ✅.
- **Einsatzort/Remote:** 100 % Remote → ✅. Bei < 100 % Remote (Hybrid/Onsite) Distanz des Onsite-Orts zu Sauerlach bestimmen (siehe unten): ≤ 50 km → ✅; 5060 km → ❔; > 60 km → ❌. Einsatzort/Remote-Anteil unklar oder nicht genannt → ❌.
**Distanzbestimmung (Geokodierung, Pflicht bei Onsite/Hybrid):** Onsite-Ort per Nominatim auflösen und Luftlinie zu Sauerlach (47,9721 N / 11,6528 O) per Haversine rechnen. Bei Geokodierungs-Fehler/Mehrdeutigkeit (Ausgabe `UNBEKANNT`) → ❔. Nominatim-Policy: max. 1 Request/s.
```bash
python3 - "<ONSITE_ORT>" <<'PY'
import sys, json, urllib.request, urllib.parse
from math import radians, sin, cos, asin, sqrt
ort = sys.argv[1]
UA = 'bewerb-projekt-anlegen/1.0 (Thomas.Langer@destengs.com)'
def geo(q):
url = 'https://nominatim.openstreetmap.org/search?' + urllib.parse.urlencode(
{'q': q, 'format': 'json', 'countrycodes': 'de', 'limit': 1})
d = json.load(urllib.request.urlopen(
urllib.request.Request(url, headers={'User-Agent': UA}), timeout=20))
return (float(d[0]['lat']), float(d[0]['lon'])) if d else None
S = (47.9721357, 11.6528398)
g = geo(ort)
if not g:
print('UNBEKANNT')
else:
dlat = radians(g[0]-S[0]); dlon = radians(g[1]-S[1])
h = sin(dlat/2)**2 + cos(radians(S[0]))*cos(radians(g[0]))*sin(dlon/2)**2
print(f'{2*6371*asin(sqrt(h)):.1f} km')
PY
```
````
- [ ] **Step 5: Schritt 6 (Review) ersetzen**
Ersetze den gesamten Block von `## Schritt 6: Review im Chat — immer vor dem CRM-Eintrag` bis unmittelbar vor `## Schritt 7:` durch:
```
## Schritt 6: Review im Chat — immer vor jedem CRM-Schreibvorgang
Niemals ohne explizite Freigabe ins CRM schreiben. Im Chat zeigen:
1. Vorgesehener **Name** der Opportunity (= Projektname) und **Projektlink**.
2. **Käufer-Typ** (Agentur/Direktauftrag) mit Kurzbegründung.
3. **Firma:** Name und `type` (Reseller bei Agentur, Customer bei Direktauftrag); „neu anlegen" oder „bestehende nutzen: <Name/ID>". Bei ähnlichen, nicht identischen Treffern die Kandidatenliste zeigen und entscheiden lassen.
4. **Kontakt:** Name; „neu anlegen" oder „bestehend nutzen: <Name/ID>".
5. **Verknüpfungs-Zuordnung** der Verkaufschance (account vs. cAccount1, Kontakt).
6. Den wörtlichen Beschreibungs-Markdown in einem Codeblock.
7. Kurze Begründungen zu allen ❌- und ❔-Bewertungen (Must/Nice/Misc) außerhalb der Tabelle.
Korrekturen kommen als „Nr. X → ✅/❌/❔" oder als Korrektur zu Firma/Kontakt/Typ. Danach betroffene Matches neu berechnen und geänderte Werte nennen. Erst nach Freigabe weiter zu Schritt 7.
```
- [ ] **Step 6: Schritt 7 (CRM-Eintrag) ersetzen**
Ersetze den gesamten Block von `## Schritt 7: CRM-Eintrag per Shell (curl)` bis unmittelbar vor `## Schritt 8:` durch:
````
## Schritt 7: CRM-Eintrag per Shell — Reihenfolge Firma → Kontakt → Verkaufschance
Diese Umgebung erreicht die CRM-Domain direkt; der Zugriff läuft per `curl`. Key/Base sicher laden (Werte nicht ausgeben):
```bash
KEY=$(grep -oE '[0-9a-f]{32}' .secrets/espocrm-api.md | head -1)
BASE=$(grep -oE 'https://[^ `]+/api/v1' .secrets/espocrm-api.md | head -1)
```
### 7.1 Firma (Account)
Dedup-Suche mit einem Kern-Token des Namens (z. B. „Aristo"):
```bash
curl -s -G "$BASE/Account" -H "X-Api-Key: $KEY" \
--data-urlencode 'where[0][type]=contains' \
--data-urlencode 'where[0][attribute]=name' \
--data-urlencode 'where[0][value]=<KERN>' \
--data-urlencode 'select=name,type' --data-urlencode 'maxSize=50'
```
Treffer gemäß Review-Entscheidung behandeln. Bestehende Firma → deren `id` als ACCOUNT_ID merken. Sonst neu anlegen — Payload mit dem **Write-Tool** nach `/tmp/account.json` (`type`: `Reseller` bei Agentur, `Customer` bei Direktauftrag):
```json
{"name": "<FIRMENNAME>", "type": "<Reseller|Customer>"}
```
```bash
curl -s -i -X POST "$BASE/Account" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/account.json
```
`id` aus der Antwort als ACCOUNT_ID merken.
### 7.2 Kontakt (Contact)
Dedup-Suche per Nachname:
```bash
curl -s -G "$BASE/Contact" -H "X-Api-Key: $KEY" \
--data-urlencode 'where[0][type]=contains' \
--data-urlencode 'where[0][attribute]=name' \
--data-urlencode 'where[0][value]=<NACHNAME>' \
--data-urlencode 'select=name,accountName' --data-urlencode 'maxSize=50'
```
Bestehenden Kontakt → dessen `id` als CONTACT_ID merken. Sonst neu anlegen — Payload nach `/tmp/contact.json`; die **Verknüpfung zur Firma über `accountId` ist beim Neu-Anlegen zwingend**:
```json
{"firstName": "<VORNAME>", "lastName": "<NACHNAME>", "accountId": "<ACCOUNT_ID>"}
```
```bash
curl -s -i -X POST "$BASE/Contact" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/contact.json
```
`id` aus der Antwort als CONTACT_ID merken. Namensaufteilung: letztes Token = `lastName`, der Rest = `firstName`; ungewöhnliche Fälle im Review klären.
### 7.3 Verkaufschance (Opportunity)
Duplikat-Prüfung des Namens (bei Treffer Suffix „ (2)", „ (3)", … anhängen):
```bash
curl -s -G "$BASE/Opportunity" -H "X-Api-Key: $KEY" \
--data-urlencode 'where[0][type]=startsWith' \
--data-urlencode 'where[0][attribute]=name' \
--data-urlencode 'where[0][value]=<PROJEKTNAME>' \
--data-urlencode 'select=name' --data-urlencode 'maxSize=100'
```
Payload nach `/tmp/opp.json`. Verknüpfungen je Käufer-Typ:
- **Agentur:** `cAccount1Id` = ACCOUNT_ID („Über Agentur"); **kein** `accountId`.
- **Direktauftrag:** `accountId` = ACCOUNT_ID; **kein** `cAccount1Id`.
```json
{"name": "<EINDEUTIGER_NAME>", "description": "<BESCHREIBUNG_MARKDOWN>", "cProjektlink": "<PROJEKT_URL>", "contactsIds": ["<CONTACT_ID>"], "cAccount1Id": "<ACCOUNT_ID — nur bei Agentur, sonst Feld weglassen und stattdessen accountId setzen>"}
```
```bash
curl -s -i -X POST "$BASE/Opportunity" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/opp.json
```
Minimaler Payload genügt (Stage/Wahrscheinlichkeit setzt das CRM). Bei HTTP 400 steht die Ursache im `X-Status-Reason`-Header. Nachträgliche Korrekturen: `PUT "$BASE/Opportunity/<id>"`. Danach `/tmp/account.json /tmp/contact.json /tmp/opp.json` löschen.
````
- [ ] **Step 7: Schritt 8 (Verifizieren) um Verknüpfungen ergänzen**
Ersetze:
```
Prüfen: stimmen Name und Projektlink, hat die Beschreibung die Match-Zeile und alle Tabellenzeilen, sind ✅/❌/❔ intakt. Dann im Chat melden: gewählter Name (inkl. evtl. Duplikat-Suffix), beide Match-Zahlen und der Direktlink `https://crm.creature-go.com/#Opportunity/view/<id>`.
```
durch:
```
Prüfen: stimmen Name und Projektlink, hat die Beschreibung die Match-Zeile und alle Tabellenzeilen, sind ✅/❌/❔ intakt; sind die Verknüpfungen korrekt — Agentur: `cAccount1Name` gesetzt und `accountName` leer; Direktauftrag: `accountName` gesetzt; `contactsIds`/`contactId` enthält den Kontakt. Dann im Chat melden: gewählter Name (inkl. evtl. Duplikat-Suffix), beide Match-Zahlen, Firma und Kontakt (jeweils neu/bestehend) und der Direktlink `https://crm.creature-go.com/#Opportunity/view/<id>`.
```
- [ ] **Step 8: Frontmatter- und Inhaltsprüfung**
Run:
```bash
head -2 .claude/skills/projekt-anlegen/SKILL.md
grep -c "Schritt 3b\|cAccount1Id\|rahmenbedingungen.md\|Nominatim\|contactsIds" .claude/skills/projekt-anlegen/SKILL.md
```
Expected: Zeile 2 = `name: projekt-anlegen`; Trefferzahl ≥ 5.
- [ ] **Step 9: Commit**
```bash
git add .claude/skills/projekt-anlegen/SKILL.md
git commit -m "Skill projekt-anlegen erweitern: Rahmenbedingungs-Bewertung, Käufer/Firma/Kontakt-Extraktion, CRM-Ablauf Firma→Kontakt→VC"
```
---
## Task 4: Abschlussverifikation & Push
- [ ] **Step 1: Geokodierung live prüfen (Regressionscheck)**
Run:
```bash
python3 - "Augsburg" <<'PY'
import sys, json, urllib.request, urllib.parse
from math import radians, sin, cos, asin, sqrt
ort = sys.argv[1]
UA = 'bewerb-projekt-anlegen/1.0 (Thomas.Langer@destengs.com)'
def geo(q):
url = 'https://nominatim.openstreetmap.org/search?' + urllib.parse.urlencode(
{'q': q, 'format': 'json', 'countrycodes': 'de', 'limit': 1})
d = json.load(urllib.request.urlopen(
urllib.request.Request(url, headers={'User-Agent': UA}), timeout=20))
return (float(d[0]['lat']), float(d[0]['lon'])) if d else None
S = (47.9721357, 11.6528398)
g = geo(ort)
dlat = radians(g[0]-S[0]); dlon = radians(g[1]-S[1])
h = sin(dlat/2)**2 + cos(radians(S[0]))*cos(radians(g[0]))*sin(dlon/2)**2
print(f'{2*6371*asin(sqrt(h)):.1f} km')
PY
```
Expected: ca. `71 km` (→ Bewertung ❌, da > 60 km). Bestätigt, dass Geokodierung erreichbar ist.
- [ ] **Step 2: Struktur & Secrets**
Run:
```bash
test -f vorgaben/rahmenbedingungen.md \
&& grep -q "rahmenbedingungen.md" CLAUDE.md \
&& grep -q "Schritt 3b" .claude/skills/projekt-anlegen/SKILL.md \
&& grep -q "cAccount1Id" .claude/skills/projekt-anlegen/SKILL.md \
&& echo "ERWEITERUNG VOLLSTÄNDIG"
git check-ignore -q .secrets/espocrm-api.md && echo "secrets ignoriert" || echo "WARNUNG"
```
Expected: `ERWEITERUNG VOLLSTÄNDIG` und `secrets ignoriert`.
- [ ] **Step 3: Arbeitsbaum & Push**
```bash
git status --short
git push origin main
git rev-list --left-right --count HEAD...@{u}
```
Expected: sauberer Arbeitsbaum, Push erfolgreich, Zähler `0 0`.
---
## Selbstreview (gegen die Spec)
- **Spec §3 rahmenbedingungen.md** → Task 1 (identischer Inhalt). ✓
- **Spec §4 Extraktion (Käufer/Firma/Kontakt)** → Task 3 Step 2. ✓
- **Spec §5 Rahmenbedingungs-Bewertung + Geokodierung** → Task 3 Step 3+4 (Schritt 3b mit Nominatim/Haversine). ✓
- **Spec §6 CRM-Ablauf Firma→Kontakt→VC, Dedup, Verknüpfung, 6.2 accountId** → Task 3 Step 6 (7.1/7.2/7.3). ✓
- **Spec §7 Review-Erweiterung** → Task 3 Step 5. ✓
- **Spec §8 geänderte Dateien** → Tasks 13. ✓
- **Spec §2 verifizierte CRM-Fakten** (type=Reseller/Customer, account/cAccount1/contacts) → in Task 3 Steps 6/7 verwendet. ✓
Keine Platzhalter. Bezeichner konsistent: ACCOUNT_ID/CONTACT_ID, `cAccount1Id`, `accountId`, `contactsIds`, `type` ∈ {Reseller, Customer}.