kimal3 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kimal3-plan",
3
- "version": "0.1.0",
4
- "description": "Hält das Board der Projektseite in der App aktuell: Punkt solange die Sitzung arbeitet, Spec-Texte, Erinnerung an Claude · Keeps the board on the app's project page current: dot while the session works, spec texts, reminder to Claude",
3
+ "version": "0.2.0",
4
+ "description": "Hält das Board der Projektseite in der App aktuell: Punkt solange die Sitzung arbeitet, ob Claude gerade arbeitet oder wartet, „Besprechen“ an Claude weitergeben, Spec-Texte, hält Claude vor dem ersten Bau-Schritt an · Keeps the board on the app's project page current: dot while the session works, whether Claude is working or waiting, passes ‘Discuss’ on to Claude, spec texts, stops Claude before the first build step",
5
5
  "author": { "name": "KImal3", "url": "https://kimal3.de" },
6
6
  "types": "./types/index.d.ts"
7
7
  }
@@ -6,11 +6,16 @@ import { relativImOrdner } from './ordner'
6
6
  * Die drei Saetze, die NUR Claude bekommt (Spec 1.3 Aufgabe 3, 1.5, 1.7, S-25, S-26, US-6, US-8; Bauplan R-4).
7
7
  * Der Mensch sieht keinen davon (gemessen M-3, Claude-App). Zwei Wege, je nachdem, ob der Satz eine Runde starten soll:
8
8
  *
9
- * - **Erinnerung ans Melden** haengt am Aendern einer Datei (`tool.call` mit `context`): keine eigene Runde, Claude liest
10
- * ihn direkt nach dem Werkzeug. Nur die Hauptsitzung bekommt ihn: Ein Subagent (`e.agentId`) baut im Auftrag und
11
- * meldet nicht; er wuerde die 30-Minuten-Sperre verbrauchen, ohne dass die Hauptsitzung je erinnert wird (Ruling). Hoechstens einmal je 30 Minuten je Sitzung, nur in einem Projektordner mit
9
+ * - **Sperre vor dem ersten Bau-Schritt** (US-6, Dennis 25.10.: „Ich will das Problem gleich gefixt haben“): Will Claude
10
+ * eine Datei aendern, ohne dass diese Sitzung eine Story „in Arbeit“ hat, lehnt der Mod die Aenderung VORHER ab
11
+ * (`deny`), und Claude liest den Satz als Grund. Bis zum 25.10. kam der Satz erst NACH der Aenderung als `context` --
12
+ * gemessen in einer echten Sitzung: Claude schrieb zuerst die Pruefung und meldete danach. Der zweite Versuch geht
13
+ * durch (30-Minuten-Sperre): Eine Aenderung ohne Story (Statuszettel, neue Spec) haengt so nie fest. Nur die
14
+ * Hauptsitzung: Ein Subagent (`e.agentId`) baut im Auftrag und meldet nicht. Nur in einem Projektordner mit
12
15
  * Ordnerzettel (S-30), und nur, wenn die App zuletzt „nichts in Arbeit“ gesagt hat -- weiss der Mod es nicht (noch
13
- * keine Antwort, App weg), schweigt er.
16
+ * keine Antwort, App weg), haelt er nichts an. 🔴 Scheitert der Hook selbst, ueberspringt ihn die Maschine und die
17
+ * Aenderung laeuft (`tool.call`, Typen: „A guard that fails is skipped and the tool runs“) -- so gewollt: Ein Fehler
18
+ * im Mod darf die Arbeit nie aufhalten.
14
19
  * - **Anstoss „Projekt anlegen“** (US-8) und **„gemerkte Meldungen senden“** (R-4) sind eine Zeile im Gespraech
15
20
  * (`$.session.append`): Claude liest sie sofort und handelt.
16
21
  */
@@ -30,17 +35,16 @@ export function dateiAus(e: unknown): string | null {
30
35
 
31
36
  export function erinnerung(on: On, lauf: Lauf): void {
32
37
  on('tool.call', { tool: ['Write', 'Edit', 'NotebookEdit'] }, async ($, e, next) => {
33
- const ergebnis = await next(e)
34
- // Abgelehnt oder gescheitert: Es hat sich nichts geaendert. Ein Subagent: nur die Hauptsitzung wird erinnert.
35
- if (ergebnis.deny !== undefined || ergebnis.isError === true || e.agentId !== undefined) return ergebnis
36
- const satz = await satzFuer($, lauf, dateiAus(e))
37
- return satz === null ? ergebnis : { ...ergebnis, context: [...(ergebnis.context ?? []), satz] }
38
+ const satz = e.agentId === undefined ? await satzFuer($, lauf, dateiAus(e)) : null
39
+ return satz === null ? next(e) : { deny: satz }
38
40
  })
39
41
  }
40
42
 
41
43
  async function satzFuer($: EngineInterface, lauf: Lauf, datei: string | null): Promise<string | null> {
42
44
  const lage = lauf.lage
43
45
  if (lauf.inArbeit !== false || lage.art !== 'projekt' || lage.zettel === null || datei === null) return null
46
+ // Markdown ist Planung (Spec, Bauplan, Statuszettel), kein Bau-Schritt: Eine neue Spec entsteht, bevor es eine Story gibt.
47
+ if (datei.toLowerCase().endsWith('.md')) return null
44
48
  if (relativImOrdner(datei, lage.ordner, await $.session.cwd()) === null) return null
45
49
  const jetzt = await $.clock.now()
46
50
  const zuletzt = await read($, letzteErinnerung)
@@ -0,0 +1,134 @@
1
+ import { update, type EngineInterface, type On } from 'claude-code'
2
+ import { TEXTE, zugestelltFuer, zustandVon, type Lauf } from './lauf'
3
+ import { naechsteAntwort, sofort } from './lebenszeichen'
4
+
5
+ /**
6
+ * „Im Gespräch“ (Spec docs/superpowers/specs/2026-10-26-im-gespraech-design.md, E-4, E-5, E-8, US-2, US-3; Bauplan Task 4,
7
+ * R-17). Zwei Aufgaben:
8
+ *
9
+ * 1. **Ob Claude gerade arbeitet** sieht der Mod selbst, ohne dass Claude etwas meldet (E-4): Jede Runde, die beginnt
10
+ * (`turn.start`), kommt in eine Menge, jede, die endet (`turn.complete`, jeder Grund, auch Esc = `aborted`), geht
11
+ * wieder hinaus — Hauptrunde UND Subagenten. Gemessen in der Probe (docs/qa/2026-10-26-im-gespraech-probe.md): Die
12
+ * Hauptrunde kann VOR einem Subagenten im Hintergrund enden; „arbeitet“ gilt deshalb, solange irgendeine Runde offen
13
+ * ist (G-14). „Wartet“ (E-5): ab einer Erlaubnis-Abfrage (`classic.PermissionRequest`, gemessen) oder während Claude mit
14
+ * `AskUserQuestion` fragt — bis dieser Werkzeugaufruf endet (`classic.PostToolUse`, `PostToolUseFailure`,
15
+ * `PermissionDenied`, gleiches Werkzeug, gleiche Schleife) oder die Runde seiner Schleife (R-34, R-35). Diese Ereignisse
16
+ * umschließen nichts; ein `tool.call`-Haken um JEDES Werkzeug läge dagegen um den Start eines Subagenten, und „an agent's
17
+ * start skips the hooks its spawn was nested in“ (Typen, `'turn.start'`). Jeder Wechsel löst sofort ein Lebenszeichen aus
18
+ * (`sofort`); der Zustand reist ohnehin mit jedem Takt, so heilt der nächste eine verlorene Meldung (G-27).
19
+ * 2. **Dennis' Klick an Claude weitergeben** (E-8): Beim Abschicken einer Nachricht (`classic.UserPromptSubmit`, gemessen:
20
+ * `additionalContext` kommt an) bekommt Claude den Satz „angeklickt“ bzw. „beendet“, wenn die App ihn für diesen Chat
21
+ * `abholbar` nennt und die Nummer neu ist. Gewartet wird höchstens 1,5 s auf eine frische Antwort der App, sonst gilt
22
+ * die letzte (QA 11: das Abschicken hängt nie 8 s). Die zugestellte Nummer steht in `$.state` und übersteht so ein
23
+ * Neuladen des Mods; nach „angeklickt“ geht SOFORT ein Lebenszeichen mit `abgeholt` hinaus (R-17), damit ein zweiter
24
+ * Chat den Klick nicht auch zustellt. Für „beendet“ kein `abgeholt` (die App meldet es dem tragenden Chat dauerhaft
25
+ * `abholbar`; allein die Nummer in `$.state` verhindert die Wiederholung).
26
+ */
27
+
28
+ /** Wie lange das Abschicken einer Nachricht höchstens auf eine frische Antwort der App wartet (US-3, QA 11). */
29
+ const FRISCH_MS = 1_500
30
+
31
+ // Geschrieben wird hier, gelesen in `zugestelltFuer` (lauf.ts, auch vom Lebenszeichen, R-33): je Projekt eine Nummer.
32
+ const zugestellt = { plugin: 'kimal3-plan', key: 'zugestellt' } as const
33
+
34
+ /** Ändert `f` den Zustand, erfährt es die App sofort. Kein `await`: Beginn und Ende einer Runde warten auf nichts. */
35
+ function aendere($: EngineInterface, lauf: Lauf, f: () => void): void {
36
+ const vorher = zustandVon(lauf)
37
+ f()
38
+ if (lauf.runden.size === 0) lauf.wartet = null
39
+ if (zustandVon(lauf) !== vorher) sofort($, lauf)
40
+ }
41
+
42
+ /** Das Warten auf `werkzeug` in der Schleife `agent` (Hauptrunde: `undefined`) ist vorbei. */
43
+ function wartenVorbei($: EngineInterface, lauf: Lauf, agent: string | undefined, werkzeug: string): void {
44
+ aendere($, lauf, () => {
45
+ if (lauf.wartet !== null && lauf.wartet.agent === agent && lauf.wartet.werkzeug === werkzeug) lauf.wartet = null
46
+ })
47
+ }
48
+
49
+ export function gespraech(on: On, lauf: Lauf): void {
50
+ // Nach `/clear` ist es ein neuer Chat: Er hat noch nichts zugestellt.
51
+ on('classic.SessionStart', { source: 'clear' }, async ($, e, next) => {
52
+ const ergebnis = await next(e)
53
+ await update($, zugestellt, () => ({}))
54
+ return ergebnis
55
+ })
56
+ on('turn.start', ($, e, next) => {
57
+ aendere($, lauf, () => lauf.runden.add(e.turnId))
58
+ return next(e)
59
+ })
60
+ // Jeder Grund schließt die Runde, auch `aborted` (Esc, G-26) und `error`. Eine Runde, deren Beginn der Mod nie hörte
61
+ // (vor dem Laden begonnen), ändert nichts. Mit ihr endet ein Warten ihrer Schleife (R-34): Endet die Runde, ohne dass
62
+ // das Werkzeug ein Ende meldete (abgelehnt, abgebrochen), wartet dort niemand mehr — auch wenn ein Subagent weiterläuft.
63
+ on('turn.complete', ($, e, next) => {
64
+ aendere($, lauf, () => {
65
+ lauf.runden.delete(e.turnId)
66
+ if (lauf.wartet?.agent === e.agentId) lauf.wartet = null
67
+ })
68
+ return next(e)
69
+ })
70
+ // Nur beobachten: Die Abfrage selbst entscheidet der Mensch, der Haken antwortet nie mit `decision`.
71
+ on('classic.PermissionRequest', ($, e, next) => {
72
+ aendere($, lauf, () => {
73
+ if (lauf.runden.size > 0) lauf.wartet = { agent: e.agent_id, werkzeug: e.tool_name }
74
+ })
75
+ return next(e)
76
+ })
77
+ // Das Fragefenster: Nur dieser Haken umschließt den Aufruf (`next`), alle anderen Ereignisse hier beobachten nur.
78
+ on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
79
+ aendere($, lauf, () => {
80
+ if (lauf.runden.size > 0) lauf.wartet = { agent: e.agentId, werkzeug: e.tool }
81
+ })
82
+ try {
83
+ return await next(e)
84
+ } finally {
85
+ wartenVorbei($, lauf, e.agentId, e.tool)
86
+ }
87
+ })
88
+ // Das Werkzeug, auf das gewartet wurde, ist fertig, gescheitert oder abgelehnt (gleiches Werkzeug, gleiche Schleife). Ein
89
+ // Aufruf eines Subagenten, der nebenher endet, beendet das Warten der Hauptrunde nicht.
90
+ on('classic.PostToolUse', ($, e, next) => {
91
+ wartenVorbei($, lauf, e.agent_id, e.tool_name)
92
+ return next(e)
93
+ })
94
+ on('classic.PostToolUseFailure', ($, e, next) => {
95
+ wartenVorbei($, lauf, e.agent_id, e.tool_name)
96
+ return next(e)
97
+ })
98
+ on('classic.PermissionDenied', ($, e, next) => {
99
+ wartenVorbei($, lauf, e.agent_id, e.tool_name)
100
+ return next(e)
101
+ })
102
+ // R-36: zugestellt ist der Satz erst, wenn die Nachricht wirklich an Claude geht — nicht bei einer Nachricht, die ein
103
+ // anderer Haken anhält (`block`, `preventContinuation`), nicht beim Vorab-Durchgang eines Ereignisses (`poll_event`, die
104
+ // Typen: „before its delivery ack exists“), nicht in einem Subagenten.
105
+ on('classic.UserPromptSubmit', async ($, e, next) => {
106
+ const z = e.agent_id === undefined && e.source !== 'poll_event' ? await zustellbar($, lauf) : null
107
+ const ergebnis = await next(e)
108
+ if (z === null || ergebnis.block !== undefined || ergebnis.preventContinuation === true) return ergebnis
109
+ await update($, zugestellt, m => ({ ...m, [z.projekt]: Math.max(m?.[z.projekt] ?? 0, z.nummer) }))
110
+ if (z.aktiv) {
111
+ lauf.abholen = { projekt: z.projekt, nummer: z.nummer }
112
+ sofort($, lauf)
113
+ }
114
+ return { ...ergebnis, additionalContext: [...(ergebnis.additionalContext ?? []), z.satz] }
115
+ })
116
+ }
117
+
118
+ /** Der Satz, den dieser Chat jetzt zustellen darf, mit Projekt, Nummer und ob es „angeklickt“ ist — oder `null`. */
119
+ async function zustellbar($: EngineInterface, lauf: Lauf): Promise<{ satz: string; projekt: string; nummer: number; aktiv: boolean } | null> {
120
+ // Ohne laufendes Lebenszeichen (kein Projektordner, kein Zettel, beendet) gibt es nichts abzuholen und nichts zu warten.
121
+ if (lauf.takt === null) return null
122
+ await new Promise<void>(fertig => {
123
+ const frist = $.clock.after(FRISCH_MS, fertig)
124
+ naechsteAntwort($, lauf).then(() => {
125
+ frist.cancel()
126
+ fertig()
127
+ })
128
+ })
129
+ const g = lauf.gespraech
130
+ if (g === null || !g.abholbar || g.quelle !== 'dennis' || g.nummer <= (await zugestelltFuer($, g.projekt))) return null
131
+ const texte = TEXTE[lauf.sprache]
132
+ const satz = g.aktiv ? texte.angeklickt(g.titel, g.datei) : texte.beendet(g.titel)
133
+ return { satz, projekt: g.projekt, nummer: g.nummer, aktiv: g.aktiv }
134
+ }
@@ -37,16 +37,58 @@ export type Lauf = {
37
37
  /** Beim naechsten erfolgreichen Lebenszeichen nach gemerkten Meldungen sehen (R-4): beim Start und nach jedem Ausfall. */
38
38
  meldungenPruefen: boolean
39
39
  fehler: Set<Fehler>
40
+ /** Die offenen Runden (`turnId`), Hauptrunde und Subagenten: nicht leer = Claude arbeitet (hooks/gespraech.ts, E-4). */
41
+ runden: Set<string>
42
+ /** Worauf Claude gerade wartet (Erlaubnis, Fragefenster) und in welcher Schleife; `null`: auf nichts (E-5). */
43
+ wartet: { agent: string | undefined; werkzeug: string } | null
44
+ /** Das Gespräch aus der letzten Antwort der App; `null`: keins, oder eine App ohne „Im Gespräch“ (R-18). */
45
+ gespraech: (GespraechFuerMod & { projekt: string }) | null
46
+ /** Diese Nummer („angeklickt“) ist Claude zugestellt, die App weiß es noch nicht: Das nächste Lebenszeichen DIESES Projekts sagt `abgeholt`. */
47
+ abholen: { projekt: string; nummer: number } | null
48
+ /** Ein sofortiger Takt fiel aus, weil einer unterwegs war: gleich danach noch einer (der Nachzügler). */
49
+ nachzuegler: boolean
50
+ /** Wer auf die Antwort des nächsten Takts wartet (`naechsteAntwort`); ein Takt nimmt beim Start alle mit. */
51
+ wartende: (() => void)[]
40
52
  /** Was gerade in der Statuszeile steht (`undefined`: nichts; `null`: noch nie gesetzt -- die erste Auswertung nach dem
41
53
  * Laden ruft `$.ui.status` in jedem Fall und raeumt so eine Zeile ab, die ein frueherer Lauf stehen liess). */
42
54
  zeile: string | undefined | null
43
55
  }
44
56
 
57
+ export type Zustand = 'still' | 'arbeitet' | 'wartet'
58
+
59
+ /** Was das Lebenszeichen über das Gespräch sagt (`gespraechFuerMod` in `src/lib/planung/gespraech.ts`, Bauplan R-18). */
60
+ export type GespraechFuerMod = { nummer: number; spec: string; titel: string; datei: string | null; aktiv: boolean; quelle: string; abholbar: boolean }
61
+
62
+ /** Arbeitet Claude, wartet es, oder ist es fertig (hooks/gespraech.ts, E-4, E-5)? */
63
+ export function zustandVon(lauf: Lauf): Zustand {
64
+ if (lauf.runden.size === 0) return 'still'
65
+ return lauf.wartet === null ? 'arbeitet' : 'wartet'
66
+ }
67
+
68
+ /** `gespraech` aus der Antwort; fehlt es (alte App, alter Rumpf) oder ist es kaputt: `null` — beides gleich (R-18). */
69
+ export function gespraechAus(o: unknown): GespraechFuerMod | null {
70
+ const g = o as Partial<Record<keyof GespraechFuerMod, unknown>> | null | undefined
71
+ if (g === null || typeof g !== 'object') return null
72
+ const { nummer, spec, titel, datei, aktiv, quelle, abholbar } = g
73
+ if (!Number.isSafeInteger(nummer) || (nummer as number) < 1 || typeof spec !== 'string' || typeof titel !== 'string') return null
74
+ if (!(datei === null || typeof datei === 'string') || typeof aktiv !== 'boolean' || typeof quelle !== 'string' || typeof abholbar !== 'boolean') return null
75
+ return { nummer: nummer as number, spec, titel, datei, aktiv, quelle, abholbar }
76
+ }
77
+
78
+ /**
79
+ * Welche Nummer Claude je Projekt schon bekam (R-37): je `projekt_id`, damit ein Chat, der das Projekt wechselt, dort keinen
80
+ * Satz verschluckt. In `$.state`, damit ein Neuladen des Mods denselben Satz nicht noch einmal zustellt (US-3).
81
+ */
82
+ export async function zugestelltFuer($: EngineInterface, projekt: string): Promise<number> {
83
+ const { value } = await $.state.get({ plugin: 'kimal3-plan', key: 'zugestellt' })
84
+ return value?.[projekt] ?? 0
85
+ }
86
+
45
87
  export function neuerLauf(vorgabe: Sprache): Lauf {
46
88
  return {
47
89
  vorgabe, sprache: vorgabe, heim: null, basis: APP, lage: { art: 'kein-projektordner' }, inArbeit: null, meldungen: 0, specDateien: [],
48
90
  takt: null, startet: false, generation: 0, beendet: false, abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true,
49
- fehler: new Set(), zeile: null,
91
+ fehler: new Set(), zeile: null, runden: new Set(), wartet: null, gespraech: null, abholen: null, nachzuegler: false, wartende: [],
50
92
  }
51
93
  }
52
94
 
@@ -56,9 +98,12 @@ const APP = 'https://app.kimal3.de'
56
98
  // Englische Befehlswoerter wie in `cli/src/befehlswoerter.ts` (`project connect`, `plan send`).
57
99
  export const TEXTE = {
58
100
  de: {
59
- erinnerung: (titel: string) => `Du arbeitest im Projekt ${titel}, aber diese Sitzung hat keine Story ‚in Arbeit‘ gemeldet. Welche ist dran? Gehört die Änderung zu keiner Spec, notiere sie als Thema oder arbeite weiter.`,
101
+ erinnerung: (titel: string) => `Halt, die Datei ist noch nicht geändert: Du arbeitest im Projekt ${titel}, aber diese Sitzung hat keine Story ‚in Arbeit‘ gemeldet. Melde zuerst die Story, an der du arbeitest, und ändere die Datei danach. Gehört die Änderung zu keiner Story, notiere sie als Thema oder versuch es einfach noch einmal.`,
60
102
  anstoss: 'Für diesen Projektordner gibt es noch kein Projekt in der App — leg es mit `kimal3 projekt verbinden` an.',
61
103
  gemerkt: 'Gemerkte Meldungen warten — führe `kimal3 plan senden` aus.',
104
+ // Im Gespräch, E-8 (Wortlaute der Spec ohne Namen: Der Mod kennt den Vornamen des Eigentümers nicht).
105
+ angeklickt: (titel: string, datei: string | null) => `Auf dem Board wurde ‚${titel}‘${datei === null ? '' : ` (${datei})`} zum Besprechen angeklickt — darüber wird jetzt gesprochen.`,
106
+ beendet: (titel: string) => `Auf dem Board wurde das Gespräch über ‚${titel}‘ beendet.`,
62
107
  fehler: {
63
108
  'nicht-erreichbar': 'Die Projektseite ist gerade nicht erreichbar — ich versuche es gleich wieder.',
64
109
  'nicht-angemeldet': 'Du bist bei kimal3 nicht mehr angemeldet — `kimal3 login`.',
@@ -66,9 +111,11 @@ export const TEXTE = {
66
111
  },
67
112
  },
68
113
  en: {
69
- erinnerung: (titel: string) => `You are working in the project ${titel}, but this session hasn't reported a story as ‘in progress’. Which one is it? If the change belongs to no spec, note it as a topic or keep working.`,
114
+ erinnerung: (titel: string) => `Stop, the file has not been changed: You are working in the project ${titel}, but this session hasn't reported a story as ‘in progress’. Report the story you're working on first, then change the file. If the change belongs to no story, note it as a topic or simply try again.`,
70
115
  anstoss: 'There is no project in the app for this project folder yet — create it with `kimal3 project connect`.',
71
116
  gemerkt: 'Saved reports are waiting — run `kimal3 plan send`.',
117
+ angeklickt: (titel: string, datei: string | null) => `'${titel}'${datei === null ? '' : ` (${datei})`} was selected on the board to discuss — that's what this conversation is about now.`,
118
+ beendet: (titel: string) => `The discussion of '${titel}' was ended on the board.`,
72
119
  fehler: {
73
120
  'nicht-erreichbar': "The project page can't be reached right now — I'll try again shortly.",
74
121
  'nicht-angemeldet': "You're no longer signed in to kimal3 — `kimal3 login`.",
@@ -1,7 +1,7 @@
1
1
  import type { EngineInterface, On } from 'claude-code'
2
2
  import { anstossOhneZettel, gemerkteMeldungenAnstossen, sitzungNeu } from './erinnerung'
3
3
  import { fehlerSind } from './fehlerzeile'
4
- import { anApp, liesAusweis, umgebungLesen, type Lauf } from './lauf'
4
+ import { anApp, gespraechAus, liesAusweis, umgebungLesen, zugestelltFuer, zustandVon, type GespraechFuerMod, type Lauf } from './lauf'
5
5
  import { lageJetzt } from './ordner'
6
6
 
7
7
  /**
@@ -13,6 +13,11 @@ import { lageJetzt } from './ordner'
13
13
  *
14
14
  * Jeder Takt liest Ordner, Zettel und Ausweis neu: So kommt ein Zettel, den Claude nach dem Anstoss anlegt (US-8), und ein
15
15
  * `kimal3 login` beim naechsten Takt an, ohne Neustart.
16
+ *
17
+ * „Im Gespraech“ (Spec 2026-10-26-im-gespraech-design.md, US-2, US-3; hooks/gespraech.ts): Jedes Lebenszeichen traegt den
18
+ * Zustand (`still`/`arbeitet`/`wartet`), `faehig: true` und nach einer Zustellung `abgeholt`; die Antwort `gespraech` merkt
19
+ * sich der Mod. Ein Wechsel des Zustands loest einen sofortigen Takt aus (`sofort`); ist gerade einer unterwegs, kommt
20
+ * gleich danach ein Nachzuegler -- sonst fiele die Meldung bis zum naechsten 10-Sekunden-Takt aus.
16
21
  */
17
22
  const TAKT_MS = 10_000
18
23
  const MELDUNG = /\bplan\s+(melde|report|senden|send)\b/
@@ -47,6 +52,7 @@ export function lebenszeichen(on: On, lauf: Lauf): void {
47
52
  const ergebnis = await next(e)
48
53
  if (e.source === 'clear') {
49
54
  Object.assign(lauf, { beendet: false, inArbeit: null, specDateien: [], abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true })
55
+ Object.assign(lauf, { runden: new Set(), wartet: null, gespraech: null, abholen: null, nachzuegler: false })
50
56
  await sitzungNeu($)
51
57
  await sitzungFuerProgramm($)
52
58
  await starte($, lauf)
@@ -75,6 +81,7 @@ export function lebenszeichen(on: On, lauf: Lauf): void {
75
81
  lauf.takt?.cancel()
76
82
  lauf.takt = null
77
83
  lauf.inArbeit = null
84
+ Object.assign(lauf, { runden: new Set(), wartet: null, gespraech: null, abholen: null })
78
85
  // Die ganze Kette von `session.end` hat ~1,5 s (gemessen M-2); der Abruf endet vorher, damit Claude Code nicht
79
86
  // mitten in ihm abschneidet und die Hooks nach uns ihre Zeit behalten.
80
87
  const grenze = next.budget.remainingMs - 100
@@ -84,6 +91,20 @@ export function lebenszeichen(on: On, lauf: Lauf): void {
84
91
  })
85
92
  }
86
93
 
94
+ /** Ein Lebenszeichen sofort (hooks/gespraech.ts) -- nur, wenn der Zeitgeber laeuft (Projektordner, nicht beendet). */
95
+ export function sofort($: EngineInterface, lauf: Lauf): void {
96
+ if (lauf.takt !== null) $.clock.after(0, () => void takt($, lauf, true))
97
+ }
98
+
99
+ /** Erfuellt sich, sobald ein Takt, der ab jetzt beginnt, seine Antwort hat (oder gleich, wenn keiner laeuft). */
100
+ export function naechsteAntwort($: EngineInterface, lauf: Lauf): Promise<void> {
101
+ if (lauf.takt === null) return Promise.resolve()
102
+ return new Promise(fertig => {
103
+ lauf.wartende.push(fertig)
104
+ sofort($, lauf)
105
+ })
106
+ }
107
+
87
108
  async function starte($: EngineInterface, lauf: Lauf): Promise<void> {
88
109
  // 🔴 `startet` steht VOR dem ersten `await`: `session.start` und `session.attach` kommen fast gleichzeitig, und
89
110
  // beide bestanden sonst die Pruefung auf `takt` -- zwei Zeitgeber, einer davon nie angehalten (Befund der Durchsicht).
@@ -106,15 +127,27 @@ async function starte($: EngineInterface, lauf: Lauf): Promise<void> {
106
127
  /** Die Sitzung (`generation`), fuer die gerade ein Lebenszeichen unterwegs ist -- je Sitzung hoechstens eines. */
107
128
  let unterwegs: number | null = null
108
129
 
109
- async function takt($: EngineInterface, lauf: Lauf): Promise<void> {
110
- // Ein Takt, dessen Vorgaenger noch auf die App wartet (langsames Netz), faellt aus, statt sich zu stapeln.
130
+ async function takt($: EngineInterface, lauf: Lauf, sofort = false): Promise<void> {
131
+ // Ein Takt, dessen Vorgaenger noch auf die App wartet (langsames Netz), faellt aus, statt sich zu stapeln. Ein
132
+ // sofortiger (Zustand gewechselt, Satz zugestellt) merkt sich, dass gleich danach einer kommen muss -- hoechstens einer.
111
133
  const generation = lauf.generation
112
- if (lauf.beendet || unterwegs === generation) return
134
+ if (lauf.beendet) return
135
+ if (unterwegs === generation) {
136
+ if (sofort) lauf.nachzuegler = true
137
+ return
138
+ }
113
139
  unterwegs = generation
140
+ // Wer jetzt wartet, bekommt die Antwort DIESES Takts; wer erst waehrend des Abrufs dazukommt, die des Nachzueglers.
141
+ const wartende = lauf.wartende.splice(0)
114
142
  try {
115
143
  await einTakt($, lauf, generation)
116
144
  } finally {
117
145
  if (unterwegs === generation) unterwegs = null
146
+ for (const fertig of wartende) fertig()
147
+ if (lauf.nachzuegler && !lauf.beendet && lauf.generation === generation) {
148
+ lauf.nachzuegler = false
149
+ $.clock.after(0, () => void takt($, lauf, true))
150
+ }
118
151
  }
119
152
  }
120
153
 
@@ -141,7 +174,11 @@ async function einTakt($: EngineInterface, lauf: Lauf, generation: number): Prom
141
174
  // Kam waehrend des Abrufs eine Meldung, ist seine Antwort zu „in Arbeit“ womoeglich von davor: sie zaehlt nicht.
142
175
  const meldungen = lauf.meldungen
143
176
  lauf.gemeldetFuer = zettel.projekt_id
144
- const antwort = await anApp($, lauf, '/api/mod/lebenszeichen', ausweis.schluessel, { projektId: zettel.projekt_id, sitzung })
177
+ // Der Zustand im Augenblick des Abschickens; aendert er sich waehrend des Abrufs, kommt der Nachzuegler (`takt`).
178
+ // `abgeholt` nur an das Projekt, dessen Klick zugestellt wurde (R-37: der Chat kann den Ordner gewechselt haben).
179
+ const abgeholt = lauf.abholen?.projekt === zettel.projekt_id ? lauf.abholen.nummer : null
180
+ const rumpf = { projektId: zettel.projekt_id, sitzung, zustand: zustandVon(lauf), faehig: true, ...(abgeholt === null ? {} : { abgeholt }) }
181
+ const antwort = await anApp($, lauf, '/api/mod/lebenszeichen', ausweis.schluessel, rumpf)
145
182
  if (vorbei()) return
146
183
 
147
184
  if (antwort?.status === 200) {
@@ -150,6 +187,16 @@ async function einTakt($: EngineInterface, lauf: Lauf, generation: number): Prom
150
187
  if (gelesen === null) return fehlerSind($, lauf, ['nicht-erreichbar'])
151
188
  if (lauf.meldungen === meldungen) lauf.inArbeit = gelesen.inArbeit
152
189
  lauf.specDateien = gelesen.specDateien
190
+ const g = gelesen.gespraech
191
+ lauf.gespraech = g === null ? null : { ...g, projekt: zettel.projekt_id }
192
+ if (abgeholt !== null && lauf.abholen?.nummer === abgeholt) lauf.abholen = null
193
+ // R-33: Nennt die App einen Klick, den dieser Chat schon zugestellt hat, noch `abholbar`, hat das Eintragen nicht
194
+ // geklappt (oder die Antwort war von davor) -- das naechste Lebenszeichen sagt `abgeholt` noch einmal. Doppeltes
195
+ // Abholen ist harmlos (wer zuerst kommt, `abholen` in src/lib/planung/gespraech.ts); nie fuer „beendet“ (R-17).
196
+ if (g !== null && g.aktiv && g.abholbar && g.quelle === 'dennis' && g.nummer === (await zugestelltFuer($, zettel.projekt_id))) {
197
+ if (vorbei()) return
198
+ lauf.abholen = { projekt: zettel.projekt_id, nummer: g.nummer }
199
+ }
153
200
  fehlerSind($, lauf, [])
154
201
  if (lauf.meldungenPruefen) {
155
202
  lauf.meldungenPruefen = false
@@ -163,6 +210,7 @@ async function einTakt($: EngineInterface, lauf: Lauf, generation: number): Prom
163
210
  if (antwort?.status === 403) {
164
211
  lauf.abgelehnt = zettel.projekt_id
165
212
  lauf.inArbeit = null
213
+ lauf.gespraech = null
166
214
  return fehlerSind($, lauf, [])
167
215
  }
168
216
  // `410`: diese Sitzung ist schon abgemeldet -- nie wiederbeleben, sonst kaeme der Punkt zurueck (Review Focus 5).
@@ -170,19 +218,24 @@ async function einTakt($: EngineInterface, lauf: Lauf, generation: number): Prom
170
218
  lauf.takt?.cancel()
171
219
  lauf.takt = null
172
220
  lauf.inArbeit = null
221
+ lauf.gespraech = null
173
222
  return fehlerSind($, lauf, [])
174
223
  }
175
224
  fehlerSind($, lauf, ['nicht-erreichbar'])
176
225
  }
177
226
 
178
- /** `{ inArbeit, specDateien }` aus der Antwort; ohne gueltiges `inArbeit` `null`. Fehlt `specDateien` (aeltere App): leer. */
179
- function antwortAus(text: string): { inArbeit: boolean; specDateien: string[] } | null {
227
+ /**
228
+ * `{ inArbeit, specDateien, gespraech }` aus der Antwort; ohne gueltiges `inArbeit` `null`. Fehlt `specDateien` (aeltere
229
+ * App): leer. Fehlt `gespraech` oder ist es `null` (App vor „Im Gespraech“): kein Gespraech -- das bricht nichts.
230
+ */
231
+ function antwortAus(text: string): { inArbeit: boolean; specDateien: string[]; gespraech: GespraechFuerMod | null } | null {
180
232
  try {
181
233
  const o: unknown = JSON.parse(text)
182
234
  const inArbeit = (o as { inArbeit?: unknown } | null)?.inArbeit
183
235
  const dateien = (o as { specDateien?: unknown } | null)?.specDateien
184
236
  if (typeof inArbeit !== 'boolean') return null
185
- return { inArbeit, specDateien: Array.isArray(dateien) ? dateien.filter((d): d is string => typeof d === 'string') : [] }
237
+ const gespraech = gespraechAus((o as { gespraech?: unknown }).gespraech)
238
+ return { inArbeit, specDateien: Array.isArray(dateien) ? dateien.filter((d): d is string => typeof d === 'string') : [], gespraech }
186
239
  } catch {
187
240
  return null
188
241
  }
@@ -1,12 +1,14 @@
1
1
  import type { Register } from 'claude-code'
2
2
  import { erinnerung } from './erinnerung'
3
+ import { gespraech } from './gespraech'
3
4
  import { neuerLauf } from './lauf'
4
5
  import { lebenszeichen } from './lebenszeichen'
5
6
  import { specText } from './spec-text'
6
7
 
7
8
  // Der Mod `kimal3-plan` (Spec docs/superpowers/specs/2026-10-23-brett-in-echtzeit-design.md, 1.3): haelt das Board der
8
- // Projektseite aktuell. Drei Aufgaben, je eine Datei: Punkt am Leben halten (lebenszeichen.ts), Spec-Texte hochladen
9
- // (spec-text.ts), Claude ans Melden erinnern (erinnerung.ts). Dem Menschen sagt er nur etwas, wenn etwas nicht klappt --
9
+ // Projektseite aktuell. Vier Aufgaben, je eine Datei: Punkt am Leben halten (lebenszeichen.ts), Spec-Texte hochladen
10
+ // (spec-text.ts), Claude ans Melden erinnern (erinnerung.ts), „Im Gespraech“ -- ob Claude arbeitet, und Dennis' Klick auf
11
+ // dem Board an Claude weitergeben (gespraech.ts, Spec 2026-10-26-im-gespraech-design.md). Dem Menschen sagt er nur etwas, wenn etwas nicht klappt --
10
12
  // eine Zeile in der Statuszeile (fehlerzeile.ts). Er startet kein `kimal3` (`$.process` gibt es in der Claude-App nicht)
11
13
  // und spricht selbst mit der App, mit dem Geraeteschluessel aus `~/.kimal3/anmeldung.json`.
12
14
 
@@ -17,6 +19,7 @@ const SPRACHE: 'de' | 'en' = 'de'
17
19
  export const register: Register = on => {
18
20
  const lauf = neuerLauf(SPRACHE)
19
21
  lebenszeichen(on, lauf)
22
+ gespraech(on, lauf)
20
23
  erinnerung(on, lauf)
21
24
  specText(on, lauf)
22
25
  }
@@ -14,6 +14,10 @@ import { liesDatei, normiert, relativImOrdner } from './ordner'
14
14
  *
15
15
  * Hochgeladen wird nach dem Werkzeug und ohne Claude darauf warten zu lassen (`$.clock.after(0, …)`). Was per Skript
16
16
  * entsteht (Bash, Python), sieht der Mod nicht; das kommt mit `/k3plan` (S-19).
17
+ *
18
+ * „Im Gespraech“ (Spec 2026-10-26-im-gespraech-design.md, E-2 Rang 3, G-8, G-13): Schreibt die HAUPTSITZUNG die Spec,
19
+ * geht ihre Kennung mit (`sitzung`) -- dann kommt die Spec von selbst ins Gespraech, wenn gerade keins laeuft. Ein Subagent
20
+ * (`e.agentId`) laedt den Text hoch, aber ohne Kennung: Er setzt nie ein Gespraech.
17
21
  */
18
22
  export function specText(on: On, lauf: Lauf): void {
19
23
  on('tool.call', { tool: ['Write', 'Edit'] }, async ($, e, next) => {
@@ -25,17 +29,18 @@ export function specText(on: On, lauf: Lauf): void {
25
29
  const pfad = relativImOrdner(datei, lage.ordner, await $.session.cwd())
26
30
  if (pfad === null || !lauf.specDateien.includes(pfad)) return ergebnis
27
31
  const projektId = lage.zettel.projekt_id
28
- $.clock.after(0, () => void hochladen($, lauf, `${normiert(lage.ordner)}/${pfad}`, pfad, projektId))
32
+ const sitzung = e.agentId === undefined ? await $.session.id() : null
33
+ $.clock.after(0, () => void hochladen($, lauf, `${normiert(lage.ordner)}/${pfad}`, pfad, projektId, sitzung))
29
34
  return ergebnis
30
35
  })
31
36
  }
32
37
 
33
- async function hochladen($: EngineInterface, lauf: Lauf, datei: string, pfad: string, projektId: string): Promise<void> {
38
+ async function hochladen($: EngineInterface, lauf: Lauf, datei: string, pfad: string, projektId: string, sitzung: string | null): Promise<void> {
34
39
  const text = await liesDatei($, datei)
35
40
  const ausweis = await liesAusweis($, lauf)
36
41
  // Ohne Ausweis meldet sich die Statuszeile schon ueber das Lebenszeichen (S-11, S-22).
37
42
  if (text === null || 'fehler' in ausweis) return
38
- const antwort = await anApp($, lauf, '/api/mod/spec-text', ausweis.schluessel, { projektId, pfad, text })
43
+ const antwort = await anApp($, lauf, '/api/mod/spec-text', ausweis.schluessel, { projektId, pfad, text, ...(sitzung === null ? {} : { sitzung }) })
39
44
  if (antwort?.status === 200 || antwort?.status === 204) return fehlerSetzen($, lauf, { 'nicht-angemeldet': false, 'nicht-erreichbar': false })
40
45
  if (antwort?.status === 401) return fehlerSetzen($, lauf, { 'nicht-angemeldet': true })
41
46
  if (antwort === null || antwort.status >= 500) return fehlerSetzen($, lauf, { 'nicht-erreichbar': true })
@@ -5,6 +5,10 @@ declare module 'claude-code' {
5
5
  letzteErinnerung: number | null
6
6
  // Der Anstoss „Projekt anlegen“ ging in dieser Sitzung schon an Claude (hooks/erinnerung.ts, US-8).
7
7
  anstossGegeben: boolean
8
+ // Je Projekt (`projekt_id`) die hoechste Nummer eines Gespraechs, deren Satz („angeklickt“/„beendet“) Claude in dieser
9
+ // Sitzung bekam; fehlt das Projekt: noch keine. In `$.state`, damit ein Neuladen des Mods denselben Satz nicht noch
10
+ // einmal zustellt (hooks/gespraech.ts, US-3, R-37).
11
+ zugestellt: Partial<Record<string, number>>
8
12
  }
9
13
  }
10
14
  }
@@ -88,7 +88,12 @@ nutze sie, statt vorschnell loszulegen.
88
88
 
89
89
  ### 3.3 Planen, jetzt konkret
90
90
  - Was genau gebaut wird, in welchen Schritten, woran jeder Schritt geprüft wird. Bei Größerem schriftlich: eine Spec,
91
- also ein Plan mit Nutzergeschichten („Als … will ich … damit …“) und einer Gegenprobe je Kriterium.
91
+ also ein Plan mit Nutzergeschichten. Jede Story steht in vier Kisten, immer in dieser Reihenfolge:
92
+ **User Story** („Als … will ich … damit …“; der Titel nennt das Ziel des Menschen, nicht das Ergebnis) ·
93
+ **Akzeptanzkriterium** (vier Zeilen: *X hat …* · *X tut …* · *Dann muss …* · *Gegenprobe: …* — ein Kriterium, das nicht
94
+ scheitern kann, ist keins) · **So testest du es** (nummerierte Schritte für jemanden ohne Technik: wo, was tun, was dann
95
+ zu sehen ist; darunter die Zeile „Passt nicht, wenn: …“; ein Befehl nur mit Ort, in Backticks) · **Technische Notiz**
96
+ (wo die Sache wohnt, was sie trägt — der Mensch muss sie nicht lesen). Echte Zahlen, echter Text, ein Name statt „der Nutzer“.
92
97
  - **Ein abgesegneter Plan rechtfertigt nichts, was frische Messungen widerlegen.** Nie „der Plan sagt, das ist
93
98
  hinnehmbar" als Begründung — sondern neu beantworten: *Was ist mit dem, was ich JETZT weiß, das Beste?* Weicht es
94
99
  ab: laut sagen, Plan nachziehen.
@@ -143,20 +148,41 @@ Das Board in der App zeigt nur, was du meldest: Wer nicht meldet, lässt es stun
143
148
  Legt es ein Projekt an, gibst du den **einen** Satz weiter, den es ausgibt („Ich habe das Projekt ‚…‘ in der App angelegt.“).
144
149
  Gibt es zwei gleichnamige Projekte oder wurde es in der App gelöscht, fragt es: Frag den Menschen und wähle nie selbst; `--neu`
145
150
  gibt es nur nach seinem Ja. Fehlt das Erfolgsprofil, sag den Satz einmal, arbeite weiter und melde nichts.
146
- - **Du meldest zu sechs Zeitpunkten**, jedes Mal mit `kimal3 plan melde` (Eingabe: eine Meldung als JSON oder eine Liste,
151
+ - **Du meldest zu sieben Zeitpunkten**, jedes Mal mit `kimal3 plan melde` (Eingabe: eine Meldung als JSON oder eine Liste,
147
152
  z. B. `echo '[…]' | kimal3 plan melde`; Zeit und Sitzung setzt das Programm). Die Schlüssel von Spec und Story liest du mit
148
153
  `kimal3 plan` aus der App — erfinde keine.
149
- 1. **Spec angelegt oder ihre Stories geändert:**
150
- `{"art":"spec","schluessel":"2026-10-24-testmonat","titel":"Testmonat","datei":"docs/superpowers/specs/2026-10-24-testmonat-design.md","stories":[{"schluessel":"US-1","titel":"Der Testmonat startet"}]}`
151
- 2. **Der Bau einer Spec beginnt:** die Stories, die jetzt gebaut werden, auf „in Arbeit“ — **bevor** du den ersten Bau-Schritt oder
154
+ 1. **Eine Spec entsteht oder ihre Stories ändern sich:** Eine neue Spec meldest du, **sobald der Mensch sagt, dass eine Spec
155
+ entstehen soll** (oder deinem Vorschlag dazu zustimmt) — vor der ersten Rückfrage, vor dem ersten geschriebenen Satz, auch
156
+ ohne Stories. Schlüssel ist der Dateiname, den sie bekommt, ohne `.md`. Legst du sie dem Menschen zum Lesen vor (nach deinen
157
+ eigenen Prüfrunden), meldest du sie mit ihren Stories und `"wartet_auf_lesen":true` — an der Karte steht dann „Wartet auf
158
+ dein Lesen“. Sagt er „Passt“, meldest du sie mit `"wartet_auf_lesen":false`; sagt er, was nicht passt, bleibt das Etikett,
159
+ bis du überarbeitet hast und er „Passt“ sagt. Überarbeitest du sie nach „Passt“ so, dass er sie neu lesen muss, setzt du es
160
+ wieder. 🔑 **Eine Spec ist eine Karte:** keine Nebenkarten wie „Spec schreiben: …“ oder „Bauplan: …“, weder als
161
+ Spec noch als Thema.
162
+ `{"art":"spec","schluessel":"2026-10-24-testmonat-design","titel":"Testmonat","datei":"docs/superpowers/specs/2026-10-24-testmonat-design.md","stories":[]}`
163
+ `{"art":"spec","schluessel":"2026-10-24-testmonat-design","titel":"Testmonat","datei":"docs/superpowers/specs/2026-10-24-testmonat-design.md","stories":[{"schluessel":"US-1","titel":"Der Testmonat startet"}],"wartet_auf_lesen":true}`
164
+ 2. **Das Thema im Chat ist eine Spec** (der Mensch spricht über eine, oder du legst eine an): wann immer das Thema zu einer
165
+ anderen Spec wechselt, nicht bei jeder Nachricht; im Zweifel melden — doppelt schadet nicht. An der Karte steht dann „Im
166
+ Gespräch“. Bei einer neuen Spec beides in einer Liste, die Spec zuerst. Sagt der Mensch im Chat, er sei damit fertig,
167
+ meldest du das Ende mit `"spec":null` und nennst in `"war"` immer die Spec, über die ihr gesprochen habt — hat der Mensch
168
+ inzwischen eine andere Karte angeklickt, beendet deine Meldung dann nicht deren Gespräch. Immer mit Sitzung, also ohne
169
+ `--ohne-sitzung`. Sagt die App „Über diese Karte spricht gerade ein anderer Chat.“: nichts tun, das Gespräch trägt ein
170
+ anderer Chat. Sagt sie „… braucht den Chat, aus dem sie kommt.“: mit Sitzung noch einmal melden, nicht mit
171
+ `--ohne-sitzung`. Sagt sie „Dieser Chat hat den alten Board-Mod.“: dem Menschen sagen, er soll `/reload-plugins` tippen
172
+ oder Claude neu starten, danach noch einmal melden. Sagt sie „Dieser Chat ist nicht mehr offen“ (eine gemerkte Meldung kam
173
+ erst später an): nichts tun — gilt das Thema noch, meldest du es aus dem Chat, in dem ihr gerade sprecht.
174
+ `{"art":"gespraech","spec":"2026-10-24-testmonat-design"}` · `{"art":"gespraech","spec":null,"war":"2026-10-24-testmonat-design"}`
175
+ 3. **Der Bau einer Spec beginnt:** die Stories, die jetzt gebaut werden, auf „in Arbeit“ — **bevor** du den ersten Bau-Schritt oder
152
176
  einen Subagenten losschickst:
153
- `{"art":"story","spec":"2026-10-24-testmonat","story":"US-1","stand":"in_arbeit"}`
154
- 3. **Eine Story ist gebaut und ihre Gegenproben sind gelaufen:** „gebaut“ mit Kriterien und dem `testen`-Satz. Der Satz ist eine Anleitung für jemanden ohne Technik: wo man es findet, was man tut, was dann zu sehen ist, woran man merkt, dass es nicht stimmt. Ein Befehl nur, wenn es keinen anderen Weg gibt — dann sagst du, wo man ihn eingibt (in Claude Code im Projektordner, mit `!` davor), und setzt ihn in Backticks. Ohne Befehl: „Öffne in der App ‚Kennzahlen‘ und klick auf den Beitrag vom 14.10. — dort steht ‚4.210 gesehen‘. Steht dort eine leere Zelle, sag ‚Passt nicht‘.“ Mit Befehl: „Tippe in Claude Code im Projektordner `! node wortzaehler.js beispiel.txt` — es erscheinen fünf Zeilen, oben ‚der (4)‘. Kommt eine Fehlermeldung, sag ‚Passt nicht‘.“
177
+ `{"art":"story","spec":"2026-10-24-testmonat-design","story":"US-1","stand":"in_arbeit"}`
178
+ 4. **Eine Story ist gebaut und ihre Gegenproben sind gelaufen:** „gebaut“ mit Kriterien und dem `testen`-Text. Den Text nimmst du aus der Kiste „So testest du es“ der Story, wörtlich. Er ist eine Anleitung für jemanden ohne Technik: **eine Zeile je Schritt** (wo man es findet, was man tut, was dann zu sehen ist), **ohne** Kistenüberschrift, und **zuletzt** die Zeile „Passt nicht, wenn: …“. Die Zeilen trennst du im JSON mit `\n` — ein echter Zeilenumbruch im Text bricht das JSON, und `kimal3 plan melde` liest die Meldung nicht. Höchstens 1000 Zeichen: ist er länger, kürze die Schritte, nie mitten im Satz abschneiden. Ein Befehl nur, wenn es keinen anderen Weg gibt — dann sagst du, wo man ihn eingibt (in Claude Code im Projektordner, mit `!` davor), und setzt ihn in Backticks.
155
179
  Beispiel:
156
- `{"art":"story","spec":"2026-10-24-testmonat","story":"US-1","stand":"gebaut","kriterien":[{"text":"Startet Sabine am 1.11., endet er am 30.11.","gegenprobe":"bestanden"}],"testen":"Öffne in der App ‚Testmonat‘ und klick „Starten“ — darunter steht ‚Endet am 30.11.‘. Steht dort ein anderes Datum, sag ‚Passt nicht‘."}`
157
- 4. **Etwas fällt auf, das keine Spec verspricht:** `{"art":"thema","schluessel":"T-4","text":"Der Kalender könnte Feiertage zeigen"}`
158
- 5. **`/k3plan`:** Specs gegen den Code prüfen und jede Abweichung einzeln melden — dort steht, wie.
159
- 6. **Feierabend:** den Tag abgleichen — dort steht, wie.
180
+ `{"art":"story","spec":"2026-10-24-testmonat-design","story":"US-1","stand":"gebaut","kriterien":[{"text":"Startet Sabine am 1.11., endet er am 30.11.","gegenprobe":"bestanden"}],"testen":"1. Öffne in der App ‚Testmonat‘.\n2. Klick „Starten“.\n3. Darunter steht ‚Endet am 30.11.‘.\nPasst nicht, wenn: dort ein anderes Datum steht."}`
181
+ 5. **Etwas fällt auf, das keine Spec verspricht:** `{"art":"thema","schluessel":"T-4","text":"Der Kalender könnte Feiertage zeigen"}`
182
+ 6. **`/k3plan`:** Specs gegen den Code prüfen und jede Abweichung einzeln melden — dort steht, wie. **Prüfen ist Arbeit:** Eine Story mit offenen Gegenproben (Spalte „Claude-Test offen“), die du gegen den Code prüfst, meldest du vorher als „in Arbeit“, mit `"pruefen":true` — so kommt die Karte dabei nicht ins Gespräch. Stories in „Dein Test“ oder „Fertig“ meldest du beim Abgleich nicht neu.
183
+ 7. **Feierabend:** den Tag abgleichen — dort steht, wie.
184
+
185
+ 🔑 **Bleibt eine Story unfertig, wenn du aufhörst** (am Ende von `/k3plan` oder im Feierabend), meldest du sie mit `--ohne-sitzung` (`kimal3 plan melde --ohne-sitzung`): Ihr Plan-Stand bleibt `in_arbeit`, aber der Punkt am Projekt leuchtet nicht weiter, obwohl das Fenster offen bleibt — auf dem Board steht sie dann in „Offen“ als angefangen.
160
186
 
161
187
  „Gebaut“ meldest du erst nach einer Gegenprobe, die in dieser Sitzung gelaufen ist, nie aus dem Gedächtnis.
162
188
  - **Subagenten melden nie.** Der Mod erinnert nur dich, nicht sie: Du meldest „in Arbeit“ **vor** dem Auftrag an einen Subagenten, der
@@ -167,7 +193,9 @@ Das Board in der App zeigt nur, was du meldest: Wer nicht meldet, lässt es stun
167
193
  und geht beim nächsten Befehl mit, schick sie nicht noch einmal. `2` heißt nicht angemeldet: Sag ihm `kimal3 login`.
168
194
  - **Sätze, die nur du bekommst** (vom Mod `kimal3-plan`, der Mensch sieht sie nicht): die Erinnerung, eine Story als „in Arbeit“
169
195
  zu melden oder die Änderung als Thema zu notieren · „leg das Projekt mit `kimal3 projekt verbinden` an“ · „gemerkte Meldungen
170
- warten — `kimal3 plan senden`“. Handle danach, ohne dem Menschen davon zu erzählen.
196
+ warten — `kimal3 plan senden`“ · der Mensch hat auf dem Board eine Karte zum Besprechen angeklickt oder das Gespräch beendet
197
+ (dann sprichst du über diese Spec bzw. nicht mehr; ein `gespraech` meldest du dafür nicht). Handle danach, ohne dem Menschen
198
+ davon zu erzählen.
171
199
 
172
200
  ## 4 · Erlaubnis und Sicherheit
173
201
  - **Bevor eine Aktion bestätigt werden muss**, erkläre in Alltagssprache: **was** passiert (die Wirkung, nicht der