Decision Sheet
Entscheidungsfragen verlassen die CLI: Agent schreibt ein Sheet, der User beantwortet es im Browser, die Antworten kommen als kompakte JSON zurück.
Drei bewegliche Teile:
| Teil | Wo | Wer |
|---|---|---|
Sheet <slug>.jsonl |
<projekt>/.decisions/ |
Agent schreibt |
Renderer index.html |
im Skill; global gespiegelt nach ~/ai-shared/izg-decision-sheet/ |
User bedient |
Antworten <slug>.answers.json |
<projekt>/.decisions/ |
User exportiert |
assets/index.html nie direkt lesen — reine Renderer-Vorlage fuer render_sheet.py,
ohne Informationswert fuer dich. Bei Renderfehlern die Fehlermeldung von render_sheet.py
lesen, nicht die HTML.
Die Scripts im Skill funktionieren überall — auch auf einem System, auf dem der globale Setup-Schritt nie gelaufen ist. Die Hooks sind Komfort, keine Voraussetzung: sie sparen dir pro Sheet ein paar Tool-Calls, mehr nicht. Du musst nicht wissen, ob sie da sind — der Ablauf ist mit und ohne derselbe.
Scripts im Überblick
| Script | Aufruf | Ausgabe |
|---|---|---|
render_sheet.py |
python3 <skill>/scripts/render_sheet.py .decisions/<slug>.jsonl |
Validiert das Sheet, meldet alle Verstöße auf einmal, baut die HTML und öffnet das Fenster |
fetch_answers.py |
python3 <skill>/scripts/fetch_answers.py [--slug <slug>] |
Holt die neueste .answers.json aus dem Download-Ordner nach .decisions/, ruft resolve_answers.py mit auf und gibt die aufgelöste Entscheidungstabelle aus |
resolve_answers.py |
python3 <skill>/scripts/resolve_answers.py .decisions/<slug>.jsonl .decisions/<slug>.answers.json |
Führt Sheet und Antwort-Datei zusammen, Zeile pro Frage mit Wert und Herkunft |
Quelltext dieser Scripts nur bei einem echten Fehler im Script selbst lesen — für den normalen Ablauf genügt die Tabelle.
Wann dieser Skill statt AskUserQuestion
Ab etwa vier offenen Entscheidungen, oder sobald Fragen voneinander abhängen
(dep). Bei einer oder zwei Fragen ist AskUserQuestion schneller.
Modus 1: Sheet schreiben
Vorab nichts nachsehen — kein ls, kein mkdir: Write legt .decisions/ mit an,
<skill> ist der Pfad aus der Zeile "Base directory for this skill" über dieser SKILL.md.
.decisions/<slug>.jsonl per Write anlegen — ein JSON-Objekt pro Zeile, erste Zeile Header:
{"v":1,"sheet":"ticketsystem-v2","title":"Ticketsystem v2","ctx":"docs/RFC-20260727-001.md"}
{"id":"id-vergabe","q":"IDs per Counter oder Timestamp?","t":"pick","o":["Counter","Timestamp","beides"],"d":"Counter","why":"kollisionsfrei, braucht aber Lockfile"}
{"id":"prefix-registry","q":"Prefix-Registry global lassen?","t":"yn","d":"y"}
{"id":"archiv-name","q":"Wie soll der Archiv-Ordner heissen?","t":"text","d":"archiv"}
{"id":"schreibrechte","q":"Welche Agents dürfen Tickets schreiben?","t":"multi","o":["claude","codex","gemini","vibe"],"d":["claude","codex"]}
{"id":"lock-mechanismus","q":"Lock via flock oder mkdir?","t":"pick","o":["flock","mkdir"],"dep":["id-vergabe","Counter"]}
{"id":"migration-modus","q":"Migrations-Skript synchron oder als Job?","t":"pick","o":["synchron","Job"],"ctx":"Bestand: 40k Zeilen in ticket_legacy, Migration lief 2025 zuletzt bei ticket_archive - synchron dauerte dort 6min und blockte den Import."}
Dateiname und das sheet-Feld im Header müssen identisch sein
(.decisions/ticketsystem-v2.jsonl zu "sheet":"ticketsystem-v2"): render_sheet.py
nimmt den Slug aus head['sheet'], der Dateiname ist nur Fallback, während
fetch_answers.py --slug nach dem Dateinamen der Antwort-Datei sucht — laufen beide
auseinander, findet der Rückweg nichts.
Danach immer diesen einen Befehl — unabhängig davon, ob die Hooks eingerichtet sind, und egal ob du die Datei per Write, Edit oder Bash-Heredoc angelegt hast:
python3 <skill>/scripts/render_sheet.py .decisions/<slug>.jsonl
Das Script validiert das Sheet (JSON pro Zeile, doppelte ids, fehlende Optionen,
kaputte oder zyklische dep-Verweise) und nennt dabei alle Verstöße auf einmal,
nicht nur den ersten. Danach baut es die HTML und öffnet das Fenster — immer, ob
Hooks eingerichtet sind oder nicht. Der Stop-Hook merkt das und hält sich raus.
Bricht es mit einer Fehlermeldung ab: Sheet korrigieren, nicht das Script umgehen. Der Aufruf ist auch mit Hook kein Leerlauf — er ist deine einzige Rückmeldung, dass das Sheet überhaupt valide ist, bevor dein Turn endet.
Liegt der Skill nicht im Projekt, tut es der globale Spiegel:
python3 ~/ai-shared/izg-decision-sheet/render_sheet.py … — identisches Script.
Gilt es doch mal nicht: der Skill im Projekt kam mit deinem Pull, der Spiegel vom
letzten Setup-Lauf auf dieser Maschine. Das Script nimmt deshalb die Kopie neben sich
und meldet eine Zeile, wenn der Spiegel davon abweicht — dann setup_global_conventions.sh
erneut laufen lassen, sonst arbeitet der Hook weiter mit dem alten Stand.
Feldreferenz
| Feld | Pflicht | Bedeutung |
|---|---|---|
id |
ja | Sprechender Kurzname, eindeutig — keine durchlaufende Zahl (Einschub wuerde sonst alle folgenden IDs verschieben) |
q |
ja | Fragetext, eine Zeile |
t |
nein | pick (default) · multi · yn · text |
o |
bei pick/multi |
Optionen. Bei yn implizit ja/nein |
d |
nein | Empfehlung — im Renderer vorausgewählt und als EMPF markiert. Bei yn: "y"/"n". Bei multi: Array |
why |
nein | Eine Zeile Begründung/Trade-off unter der Frage |
ctx |
nein | Längerer Hintergrund zu dieser einen Frage — im Renderer eingeklappt hinter „+ Kontext", nicht standardmäßig sichtbar. Nicht zu verwechseln mit dem Header-ctx (Dokumentverweis fürs ganze Sheet) |
dep |
nein | [id, wert] oder [id, [wert1, wert2]] — Frage wird nur aktiv, wenn die andere so beantwortet ist |
Header: v (Format-Version, aktuell 1), sheet (Slug = Dateiname ohne Endung), title (Überschrift im Renderer), ctx (Pfad zum Dokument mit dem Hintergrund, optional).
Regeln beim Schreiben
- Immer ein
dsetzen, wenn du eine Meinung hast. Das ist der Punkt des Formats: der User bestätigt schweigend und beantwortet nur, wo er abweicht. Nur bei echt offenen Fragen (Namen, Zahlen, Präferenzen)dweglassen — die markiert der Renderer als „offen". whynur wenn es das Trade-off wirklich klärt. Eine Zeile, kein Absatz. Braucht eine Frage mehr Hintergrund als eine Zeile — z.B. weil sie ohne Projekt-Detail (Bestandsgröße, letzter Vorfall, betroffene Komponente) nicht beantwortbar ist — gehört der in das Fragen-ctx, nicht inwhygequetscht. Frage bleibt dadurch kurz, der Kontext ist trotzdem einen Klick entfernt statt inwhyaufgebläht oder ganz weggelassen.- Eine Zeile pro Frage, kein Pretty-Print. Zeilenumbrüche im JSON zerstören den Parser (ein Objekt = eine Zeile ist die einzige Regel des Formats).
- Fragen sortieren: grundlegende zuerst, Folgefragen per
depdahinter. Nie einedep-Frage vor der Frage, an der sie hängt. IDs tragen selbst keine Reihenfolge — ein nachträglicher Einschub ist ein Edit von einer Zeile, nie ein Neuschreiben des ganzen Sheets. - Keine Fragen stellen, die du selbst entscheiden kannst. Ein Sheet mit 20 Trivialitäten ist schlimmer als drei gute Fragen in der CLI.
- Bei
multikeine „keine/reicht so"-Pseudo-Option. Nichts ausgewählt bedeutet dort schon „keine" — eine zusätzliche Verneinungs-Option macht das Ergebnis zweideutig (leeres Array vs. abgewählte Pseudo-Option lesen sich gleich). Wenn „nichts davon" eine echte Antwort ist, gehört die Frage alsyndavor.
Modus 2: Antworten lesen
Der User tippt #answers im Chat. Der UserPromptSubmit-Hook holt die neueste
*.answers.json aus dem Download-Ordner, verschiebt sie nach .decisions/ und
legt den Inhalt in den Kontext. Optional mit Slug: #answers ticketsystem-v2.
Kommt auf #answers nichts zurück, ist der Hook auf diesem System nicht
eingerichtet. Dann holst du die Datei selbst — dasselbe Script, das der Hook aufruft:
python3 <skill>/scripts/fetch_answers.py [--slug <slug>]
Sag dem User in dem Fall einmal, dass er dir nach dem Export kurz Bescheid gibt, statt auf die Automatik zu warten.
Interpretation: nicht selbst rechnen
Die Antwort-Datei ist komprimiert (übernommene Empfehlungen fehlen, dep-gefilterte
Fragen tauchen nie auf) und ohne das Sheet bedeutungslos — beides zusammenführen
tut resolve_answers.py, nicht du:
python3 <skill>/scripts/resolve_answers.py .decisions/<slug>.jsonl .decisions/<slug>.answers.json
fetch_answers.py (und damit auch #answers) ruft es selbst auf, sobald das Sheet
danebenliegt — du bekommst die aufgelöste Tabelle statt der Rohdatei. Jede Zeile nennt
Frage, effektiven Wert und Herkunft (Empfehlung / gewaehlt / offen); mit !
markierte Zeilen und die ACHTUNG-Zeile am Ende sind Fragen ohne belastbare
Entscheidung — die klärst du, bevor du sie umsetzt. Notizen sind verbindlich, nicht
Deko.
Nur wenn das Sheet fehlt, kommt die Rohdatei — dann gilt: Key fehlt = Empfehlung d
übernommen, {"a":wert,"n":…} = Antwort plus Notiz, {"a":null,"n":…} = keine Auswahl,
nur eine Rückfrage, [] bei multi = echtes „nichts davon". Das Sheet liegt in dem Fall
meist trotzdem als .decisions/<slug>.jsonl da — lesen statt raten.
Die Notiz steht bewusst im Objekt und nicht als [wert, notiz]-Tupel: eine
multi-Antwort mit zwei Optionen (["claude","codex"]) wäre sonst nicht von
Antwort-plus-Notiz zu unterscheiden. Antwort-Dateien tragen bewusst keine Fragetexte
mit, damit der Rückweg billig bleibt.
Nach dem Umsetzen: .decisions/ gehört in die .gitignore des Projekts, die Sheets
sind Wegwerf-Artefakte. Was dauerhaft gilt, gehört als ADR (doc-ids) oder in
CONTEXT.md — nicht ins Sheet.
Setup
Pro Projekt: nichts. .decisions/ wird beim ersten Sheet angelegt und gehört in
die .gitignore. Der Skill-Pull bringt alles mit, was der Ablauf braucht —
assets/index.html, scripts/, hooks/.
Global: optional, aber empfohlen. Ein Schritt, einmal pro Maschine:
bash ~/Dokumente/AI/ai-SKILL-set/scripts/setup_global_conventions.sh ~/.claude
Spiegelt Renderer und Scripts nach ~/ai-shared/izg-decision-sheet/ und registriert zwei
Hooks in ~/.claude/settings.json:
| Hook | Event | Wirkung |
|---|---|---|
izg-decision-sheet-open.sh |
Stop |
Sheet, das an render_sheet.py vorbei entstanden ist, geht trotzdem auf |
izg-decision-answers.sh |
UserPromptSubmit |
#answers holt die Antworten zurück |
Was die Hooks bringen: der Hinweg funktioniert auch dann, wenn das Sheet an
render_sheet.py vorbei entstanden ist, und der Rückweg kostet dich keinen Tool-Call.
Ohne sie läuft alles gleich, nur mit zwei bis drei Aufrufen mehr pro Sheet.
Ohne globales Setup — und andere Agents (Codex, Vibe, Gemini)
Die Hooks sind Claude-spezifisch, das Format und die Scripts sind es nicht. Der komplette Ablauf funktioniert aus dem gepullten Skill heraus:
<skill> löst sich agent-abhängig auf: bei Claude ist das der Pfad aus der Zeile
"Base directory for this skill" über dieser SKILL.md. Codex, Vibe und Gemini haben
diese Zeile nicht — dort gilt .claude/skills/izg-decision-sheet/scripts/... im
Projekt, sonst der globale Spiegel ~/ai-shared/izg-decision-sheet/..., wo die
Scripts flach ohne scripts-Unterordner liegen.
- Sheet nach
.decisions/<slug>.jsonlschreiben (identisches Format). python3 <skill>/scripts/render_sheet.py .decisions/<slug>.jsonl— öffnet das Fenster selbst — mit derassets/index.htmlneben sich, der globale Spiegel spielt dabei keine Rolle.- Nach dem Export
python3 <skill>/scripts/fetch_answers.py— holt die Datei aus dem Download-Ordner nach.decisions/und gibt die aufgelöste Entscheidungstabelle aus (resolve_answers.pyläuft mit, ohne Hook und ohne Zutun).
Wenn etwas nicht funktioniert
| Symptom | Ursache |
|---|---|
| Script meldet „gerendert", aber es geht kein Fenster auf | xdg-open fehlt oder hat keinen Browser zugeordnet — der Pfad steht in der Meldung, manuell öffnen |
| Sheet geschrieben, gar nichts passiert | render_sheet.py nicht aufgerufen — genau dafür ist der Aufruf Pflicht, auch mit Hook |
render_sheet.py: keine index.html gefunden |
Skill unvollständig gepullt (assets/ fehlt) und kein globaler Spiegel da |
| „globaler Spiegel weicht vom Skill ab" | Skill-Kopie und ~/ai-shared/izg-decision-sheet/ sind auseinandergelaufen — gerendert wird korrekt mit der Skill-Kopie, aber der Hook nutzt den alten Spiegel: setup_global_conventions.sh ~/.claude erneut laufen lassen |
| Hook meldet „Spiegel unvollständig" | Im Spiegel fehlt eine Datei (typisch nach einem Setup-Lauf vor einer neuen Script-Datei) — dasselbe Kommando behebt es |
| Renderer zeigt Dropzone statt Fragen | Sheet defekt — Fehlermeldung steht in der Box darunter |
#answers bringt nichts |
Hook nicht eingerichtet → fetch_answers.py selbst aufrufen; oder Export noch nicht gespeichert (nur „Kopieren" gedrückt) |
Export ist {"sheet":…,"a":{}} |
Kein Fehler — der User hat alle Empfehlungen übernommen |
fetch_answers.py gibt die Rohdatei statt der Tabelle aus |
Das Sheet liegt nicht als .decisions/<slug>.jsonl daneben (anderer Slug?) — resolve_answers.py mit beiden Pfaden selbst aufrufen |
Letzter Ausweg ganz ohne Scripts: assets/index.html im Browser öffnen, Sheet
reinziehen, exportierte Datei selbst nach .decisions/ legen und den Pfad nennen.