kimal3 0.8.0 → 0.9.1

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
- {
2
- "name": "kimal3-plan",
3
- "version": "0.1.1",
4
- "description": "Hält das Board der Projektseite in der App aktuell: Punkt solange die Sitzung arbeitet, 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, spec texts, stops Claude before the first build step",
5
- "author": { "name": "KImal3", "url": "https://kimal3.de" },
6
- "types": "./types/index.d.ts"
7
- }
1
+ {
2
+ "name": "kimal3-plan",
3
+ "version": "0.2.1",
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
+ "author": { "name": "KImal3", "url": "https://kimal3.de" },
6
+ "types": "./types/index.d.ts"
7
+ }
@@ -0,0 +1,141 @@
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
+ // US-6 (T-12): Beim Stop nennt Claude Code die laufende Hintergrund-Arbeit. Ein Befehl darin hält „arbeitet“, bis ein
71
+ // späterer Stop (die Runde, die seine Fertig-Meldung verarbeitet) ihn nicht mehr nennt. Subagenten zählen schon über ihre
72
+ // Runden; Monitore zählen nicht, weil einer ohne Ende laufen kann. Nur die Hauptrunde: Ein Subagent endet mit SubagentStop.
73
+ on('classic.Stop', ($, e, next) => {
74
+ aendere($, lauf, () => { lauf.hintergrund = (e.background_tasks ?? []).some(t => t.type === 'shell') })
75
+ return next(e)
76
+ })
77
+ // Nur beobachten: Die Abfrage selbst entscheidet der Mensch, der Haken antwortet nie mit `decision`.
78
+ on('classic.PermissionRequest', ($, e, next) => {
79
+ aendere($, lauf, () => {
80
+ if (lauf.runden.size > 0) lauf.wartet = { agent: e.agent_id, werkzeug: e.tool_name }
81
+ })
82
+ return next(e)
83
+ })
84
+ // Das Fragefenster: Nur dieser Haken umschließt den Aufruf (`next`), alle anderen Ereignisse hier beobachten nur.
85
+ on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
86
+ aendere($, lauf, () => {
87
+ if (lauf.runden.size > 0) lauf.wartet = { agent: e.agentId, werkzeug: e.tool }
88
+ })
89
+ try {
90
+ return await next(e)
91
+ } finally {
92
+ wartenVorbei($, lauf, e.agentId, e.tool)
93
+ }
94
+ })
95
+ // Das Werkzeug, auf das gewartet wurde, ist fertig, gescheitert oder abgelehnt (gleiches Werkzeug, gleiche Schleife). Ein
96
+ // Aufruf eines Subagenten, der nebenher endet, beendet das Warten der Hauptrunde nicht.
97
+ on('classic.PostToolUse', ($, e, next) => {
98
+ wartenVorbei($, lauf, e.agent_id, e.tool_name)
99
+ return next(e)
100
+ })
101
+ on('classic.PostToolUseFailure', ($, e, next) => {
102
+ wartenVorbei($, lauf, e.agent_id, e.tool_name)
103
+ return next(e)
104
+ })
105
+ on('classic.PermissionDenied', ($, e, next) => {
106
+ wartenVorbei($, lauf, e.agent_id, e.tool_name)
107
+ return next(e)
108
+ })
109
+ // R-36: zugestellt ist der Satz erst, wenn die Nachricht wirklich an Claude geht — nicht bei einer Nachricht, die ein
110
+ // anderer Haken anhält (`block`, `preventContinuation`), nicht beim Vorab-Durchgang eines Ereignisses (`poll_event`, die
111
+ // Typen: „before its delivery ack exists“), nicht in einem Subagenten.
112
+ on('classic.UserPromptSubmit', async ($, e, next) => {
113
+ const z = e.agent_id === undefined && e.source !== 'poll_event' ? await zustellbar($, lauf) : null
114
+ const ergebnis = await next(e)
115
+ if (z === null || ergebnis.block !== undefined || ergebnis.preventContinuation === true) return ergebnis
116
+ await update($, zugestellt, m => ({ ...m, [z.projekt]: Math.max(m?.[z.projekt] ?? 0, z.nummer) }))
117
+ if (z.aktiv) {
118
+ lauf.abholen = { projekt: z.projekt, nummer: z.nummer }
119
+ sofort($, lauf)
120
+ }
121
+ return { ...ergebnis, additionalContext: [...(ergebnis.additionalContext ?? []), z.satz] }
122
+ })
123
+ }
124
+
125
+ /** Der Satz, den dieser Chat jetzt zustellen darf, mit Projekt, Nummer und ob es „angeklickt“ ist — oder `null`. */
126
+ async function zustellbar($: EngineInterface, lauf: Lauf): Promise<{ satz: string; projekt: string; nummer: number; aktiv: boolean } | null> {
127
+ // Ohne laufendes Lebenszeichen (kein Projektordner, kein Zettel, beendet) gibt es nichts abzuholen und nichts zu warten.
128
+ if (lauf.takt === null) return null
129
+ await new Promise<void>(fertig => {
130
+ const frist = $.clock.after(FRISCH_MS, fertig)
131
+ naechsteAntwort($, lauf).then(() => {
132
+ frist.cancel()
133
+ fertig()
134
+ })
135
+ })
136
+ const g = lauf.gespraech
137
+ if (g === null || !g.abholbar || g.quelle !== 'dennis' || g.nummer <= (await zugestelltFuer($, g.projekt))) return null
138
+ const texte = TEXTE[lauf.sprache]
139
+ const satz = g.aktiv ? texte.angeklickt(g.titel, g.datei) : texte.beendet(g.titel)
140
+ return { satz, projekt: g.projekt, nummer: g.nummer, aktiv: g.aktiv }
141
+ }
@@ -37,16 +37,60 @@ 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
+ /** Beim letzten Stop der Hauptrunde lief ein Befehl im Hintergrund: Claude arbeitet weiter, auch ohne offene Runde (US-6, T-12). */
43
+ hintergrund: boolean
44
+ /** Worauf Claude gerade wartet (Erlaubnis, Fragefenster) und in welcher Schleife; `null`: auf nichts (E-5). */
45
+ wartet: { agent: string | undefined; werkzeug: string } | null
46
+ /** Das Gespräch aus der letzten Antwort der App; `null`: keins, oder eine App ohne „Im Gespräch“ (R-18). */
47
+ gespraech: (GespraechFuerMod & { projekt: string }) | null
48
+ /** Diese Nummer („angeklickt“) ist Claude zugestellt, die App weiß es noch nicht: Das nächste Lebenszeichen DIESES Projekts sagt `abgeholt`. */
49
+ abholen: { projekt: string; nummer: number } | null
50
+ /** Ein sofortiger Takt fiel aus, weil einer unterwegs war: gleich danach noch einer (der Nachzügler). */
51
+ nachzuegler: boolean
52
+ /** Wer auf die Antwort des nächsten Takts wartet (`naechsteAntwort`); ein Takt nimmt beim Start alle mit. */
53
+ wartende: (() => void)[]
40
54
  /** Was gerade in der Statuszeile steht (`undefined`: nichts; `null`: noch nie gesetzt -- die erste Auswertung nach dem
41
55
  * Laden ruft `$.ui.status` in jedem Fall und raeumt so eine Zeile ab, die ein frueherer Lauf stehen liess). */
42
56
  zeile: string | undefined | null
43
57
  }
44
58
 
59
+ export type Zustand = 'still' | 'arbeitet' | 'wartet'
60
+
61
+ /** Was das Lebenszeichen über das Gespräch sagt (`gespraechFuerMod` in `src/lib/planung/gespraech.ts`, Bauplan R-18). */
62
+ export type GespraechFuerMod = { nummer: number; spec: string; titel: string; datei: string | null; aktiv: boolean; quelle: string; abholbar: boolean }
63
+
64
+ /** Arbeitet Claude, wartet es, oder ist es fertig (hooks/gespraech.ts, E-4, E-5)? */
65
+ export function zustandVon(lauf: Lauf): Zustand {
66
+ if (lauf.runden.size === 0) return lauf.hintergrund ? 'arbeitet' : 'still'
67
+ return lauf.wartet === null ? 'arbeitet' : 'wartet'
68
+ }
69
+
70
+ /** `gespraech` aus der Antwort; fehlt es (alte App, alter Rumpf) oder ist es kaputt: `null` — beides gleich (R-18). */
71
+ export function gespraechAus(o: unknown): GespraechFuerMod | null {
72
+ const g = o as Partial<Record<keyof GespraechFuerMod, unknown>> | null | undefined
73
+ if (g === null || typeof g !== 'object') return null
74
+ const { nummer, spec, titel, datei, aktiv, quelle, abholbar } = g
75
+ if (!Number.isSafeInteger(nummer) || (nummer as number) < 1 || typeof spec !== 'string' || typeof titel !== 'string') return null
76
+ if (!(datei === null || typeof datei === 'string') || typeof aktiv !== 'boolean' || typeof quelle !== 'string' || typeof abholbar !== 'boolean') return null
77
+ return { nummer: nummer as number, spec, titel, datei, aktiv, quelle, abholbar }
78
+ }
79
+
80
+ /**
81
+ * Welche Nummer Claude je Projekt schon bekam (R-37): je `projekt_id`, damit ein Chat, der das Projekt wechselt, dort keinen
82
+ * Satz verschluckt. In `$.state`, damit ein Neuladen des Mods denselben Satz nicht noch einmal zustellt (US-3).
83
+ */
84
+ export async function zugestelltFuer($: EngineInterface, projekt: string): Promise<number> {
85
+ const { value } = await $.state.get({ plugin: 'kimal3-plan', key: 'zugestellt' })
86
+ return value?.[projekt] ?? 0
87
+ }
88
+
45
89
  export function neuerLauf(vorgabe: Sprache): Lauf {
46
90
  return {
47
91
  vorgabe, sprache: vorgabe, heim: null, basis: APP, lage: { art: 'kein-projektordner' }, inArbeit: null, meldungen: 0, specDateien: [],
48
92
  takt: null, startet: false, generation: 0, beendet: false, abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true,
49
- fehler: new Set(), zeile: null,
93
+ fehler: new Set(), zeile: null, runden: new Set(), hintergrund: false, wartet: null, gespraech: null, abholen: null, nachzuegler: false, wartende: [],
50
94
  }
51
95
  }
52
96
 
@@ -59,6 +103,9 @@ export const TEXTE = {
59
103
  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
104
  anstoss: 'Für diesen Projektordner gibt es noch kein Projekt in der App — leg es mit `kimal3 projekt verbinden` an.',
61
105
  gemerkt: 'Gemerkte Meldungen warten — führe `kimal3 plan senden` aus.',
106
+ // Im Gespräch, E-8 (Wortlaute der Spec ohne Namen: Der Mod kennt den Vornamen des Eigentümers nicht).
107
+ angeklickt: (titel: string, datei: string | null) => `Auf dem Board wurde ‚${titel}‘${datei === null ? '' : ` (${datei})`} zum Besprechen angeklickt — darüber wird jetzt gesprochen.`,
108
+ beendet: (titel: string) => `Auf dem Board wurde das Gespräch über ‚${titel}‘ beendet.`,
62
109
  fehler: {
63
110
  'nicht-erreichbar': 'Die Projektseite ist gerade nicht erreichbar — ich versuche es gleich wieder.',
64
111
  'nicht-angemeldet': 'Du bist bei kimal3 nicht mehr angemeldet — `kimal3 login`.',
@@ -69,6 +116,8 @@ export const TEXTE = {
69
116
  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
117
  anstoss: 'There is no project in the app for this project folder yet — create it with `kimal3 project connect`.',
71
118
  gemerkt: 'Saved reports are waiting — run `kimal3 plan send`.',
119
+ 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.`,
120
+ beendet: (titel: string) => `The discussion of '${titel}' was ended on the board.`,
72
121
  fehler: {
73
122
  'nicht-erreichbar': "The project page can't be reached right now — I'll try again shortly.",
74
123
  '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
  }
@@ -148,20 +148,39 @@ Das Board in der App zeigt nur, was du meldest: Wer nicht meldet, lässt es stun
148
148
  Legt es ein Projekt an, gibst du den **einen** Satz weiter, den es ausgibt („Ich habe das Projekt ‚…‘ in der App angelegt.“).
149
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`
150
150
  gibt es nur nach seinem Ja. Fehlt das Erfolgsprofil, sag den Satz einmal, arbeite weiter und melde nichts.
151
- - **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,
152
152
  z. B. `echo '[…]' | kimal3 plan melde`; Zeit und Sitzung setzt das Programm). Die Schlüssel von Spec und Story liest du mit
153
153
  `kimal3 plan` aus der App — erfinde keine.
154
- 1. **Spec angelegt oder ihre Stories geändert:**
155
- `{"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"}]}`
156
- 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
157
176
  einen Subagenten losschickst:
158
- `{"art":"story","spec":"2026-10-24-testmonat","story":"US-1","stand":"in_arbeit"}`
159
- 3. **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.
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.
160
179
  Beispiel:
161
- `{"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":"1. Öffne in der App ‚Testmonat‘.\n2. Klick „Starten“.\n3. Darunter steht ‚Endet am 30.11.‘.\nPasst nicht, wenn: dort ein anderes Datum steht."}`
162
- 4. **Etwas fällt auf, das keine Spec verspricht:** `{"art":"thema","schluessel":"T-4","text":"Der Kalender könnte Feiertage zeigen"}`
163
- 5. **`/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“. Stories in „Dein Test“ oder „Fertig“ meldest du beim Abgleich nicht neu.
164
- 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.
165
184
 
166
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.
167
186
 
@@ -174,7 +193,9 @@ Das Board in der App zeigt nur, was du meldest: Wer nicht meldet, lässt es stun
174
193
  und geht beim nächsten Befehl mit, schick sie nicht noch einmal. `2` heißt nicht angemeldet: Sag ihm `kimal3 login`.
175
194
  - **Sätze, die nur du bekommst** (vom Mod `kimal3-plan`, der Mensch sieht sie nicht): die Erinnerung, eine Story als „in Arbeit“
176
195
  zu melden oder die Änderung als Thema zu notieren · „leg das Projekt mit `kimal3 projekt verbinden` an“ · „gemerkte Meldungen
177
- 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.
178
199
 
179
200
  ## 4 · Erlaubnis und Sicherheit
180
201
  - **Bevor eine Aktion bestätigt werden muss**, erkläre in Alltagssprache: **was** passiert (die Wirkung, nicht der
@@ -148,20 +148,38 @@ The board in the app shows only what you report: if you don't, it shows "Open" f
148
148
  If it creates a project, you pass on the **one** sentence it prints ("I created the project '…' in the app.").
149
149
  If two projects have the same name, or it was deleted in the app, it asks: ask the person and never choose yourself; `--new`
150
150
  only after their yes. If the success profile is missing, say the sentence once, keep working and report nothing.
151
- - **You report at six moments**, each time with `kimal3 plan report` (input: one report as JSON or a list,
151
+ - **You report at seven moments**, each time with `kimal3 plan report` (input: one report as JSON or a list,
152
152
  e.g. `echo '[…]' | kimal3 plan report`; the program sets time and session). You read the keys of spec and story from the app
153
153
  with `kimal3 plan` — don't invent them.
154
- 1. **A spec is created or its stories change:**
155
- `{"art":"spec","schluessel":"2026-10-24-test-month","titel":"Test month","datei":"docs/superpowers/specs/2026-10-24-test-month-design.md","stories":[{"schluessel":"US-1","titel":"The test month starts"}]}`
156
- 2. **The build of a spec begins:** the stories being built now go to "in progress" — **before** you take the first build step or
154
+ 1. **A spec comes into being or its stories change:** you report a new spec **as soon as the person says a spec should be
155
+ written** (or agrees to your suggestion of one) — before the first question back, before the first written sentence, even
156
+ without stories. The key is the file name it will get, without `.md`. When you hand it to the person to read (after your own
157
+ review rounds), you report it with its stories and `"wartet_auf_lesen":true` — the card then shows "Waiting for you to
158
+ read". If they say it works, you report it with `"wartet_auf_lesen":false`; if they say what doesn't work, the label stays
159
+ until you have revised it and they say it works. If you revise it after "works" so that they have to read it again, you set
160
+ it again. 🔑 **A spec is one card:** no side cards like "Write spec: …" or "Build plan: …", neither as a spec nor as a topic.
161
+ `{"art":"spec","schluessel":"2026-10-24-test-month-design","titel":"Test month","datei":"docs/superpowers/specs/2026-10-24-test-month-design.md","stories":[]}`
162
+ `{"art":"spec","schluessel":"2026-10-24-test-month-design","titel":"Test month","datei":"docs/superpowers/specs/2026-10-24-test-month-design.md","stories":[{"schluessel":"US-1","titel":"The test month starts"}],"wartet_auf_lesen":true}`
163
+ 2. **The topic in the chat is a spec** (the person talks about one, or you are creating one): whenever the topic switches to
164
+ another spec, not with every message; when in doubt, report — sending twice does no harm. The card then shows "In
165
+ discussion". For a new spec, both in one list, the spec first. If the person says in the chat that they are done with it,
166
+ you report the end with `"spec":null` and always name the spec you were discussing in `"war"` — if the person has clicked
167
+ another card in the meantime, your report then doesn't end that card's discussion. Always with the session, so without
168
+ `--no-session`. If the app says "Another chat is discussing this card right now.": do nothing, another chat carries the
169
+ discussion. If it says "… needs the chat it comes from.": report again with the session, not with `--no-session`. If it
170
+ says "This chat has the old board mod.": tell the person to type `/reload-plugins` or restart Claude, then report again.
171
+ If it says "This chat is no longer open" (a stored report arrived later): do nothing — if the topic still applies,
172
+ report it from the chat you are talking in now.
173
+ `{"art":"gespraech","spec":"2026-10-24-test-month-design"}` · `{"art":"gespraech","spec":null,"war":"2026-10-24-test-month-design"}`
174
+ 3. **The build of a spec begins:** the stories being built now go to "in progress" — **before** you take the first build step or
157
175
  send off a subagent:
158
- `{"art":"story","spec":"2026-10-24-test-month","story":"US-1","stand":"in_arbeit"}`
159
- 3. **A story is built and its counter-checks have run:** "built" with criteria and the `testen` text. You take the text from the story's box "How you test it", word for word. It is a guide for someone without technical knowledge: **one line per step** (where to find it, what to do, what must appear), **without** the box heading, and **last** the line "Doesn't work if: …". In the JSON you separate the lines with `\n` — a real line break inside the text breaks the JSON, and `kimal3 plan report` cannot read the report. At most 1000 characters: if it is longer, shorten the steps, never cut off in the middle of a sentence. A command only if there is no other way - then say where to type it (in Claude Code in the project folder, with `!` in front) and put it in backticks.
176
+ `{"art":"story","spec":"2026-10-24-test-month-design","story":"US-1","stand":"in_arbeit"}`
177
+ 4. **A story is built and its counter-checks have run:** "built" with criteria and the `testen` text. You take the text from the story's box "How you test it", word for word. It is a guide for someone without technical knowledge: **one line per step** (where to find it, what to do, what must appear), **without** the box heading, and **last** the line "Doesn't work if: …". In the JSON you separate the lines with `\n` — a real line break inside the text breaks the JSON, and `kimal3 plan report` cannot read the report. At most 1000 characters: if it is longer, shorten the steps, never cut off in the middle of a sentence. A command only if there is no other way - then say where to type it (in Claude Code in the project folder, with `!` in front) and put it in backticks.
160
178
  Example:
161
- `{"art":"story","spec":"2026-10-24-test-month","story":"US-1","stand":"gebaut","kriterien":[{"text":"If Sabine starts on 1 Nov, it ends on 30 Nov.","gegenprobe":"bestanden"}],"testen":"1. In the app open 'Test month'.\n2. Click Start.\n3. Below it says 'Ends on 30 Nov'.\nDoesn't work if: it shows another date."}`
162
- 4. **Something comes up that no spec promises:** `{"art":"thema","schluessel":"T-4","text":"The calendar could show public holidays"}`
163
- 5. **`/k3plan`:** check the specs against the code and report every deviation individually — it says how. **Checking is work:** a story with pending counter-checks (column "Claude test pending") that you check against the code, you report as "in progress" beforehand. Stories under "Your test" or "Done" you do not report again during the reconciliation.
164
- 6. **Wrap-up:** reconcile the day — it says how.
179
+ `{"art":"story","spec":"2026-10-24-test-month-design","story":"US-1","stand":"gebaut","kriterien":[{"text":"If Sabine starts on 1 Nov, it ends on 30 Nov.","gegenprobe":"bestanden"}],"testen":"1. In the app open 'Test month'.\n2. Click Start.\n3. Below it says 'Ends on 30 Nov'.\nDoesn't work if: it shows another date."}`
180
+ 5. **Something comes up that no spec promises:** `{"art":"thema","schluessel":"T-4","text":"The calendar could show public holidays"}`
181
+ 6. **`/k3plan`:** check the specs against the code and report every deviation individually — it says how. **Checking is work:** a story with pending counter-checks (column "Claude test pending") that you check against the code, you report as "in progress" beforehand, with `"pruefen":true` — so the card doesn't come into discussion meanwhile. Stories under "Your test" or "Done" you do not report again during the reconciliation.
182
+ 7. **Wrap-up:** reconcile the day — it says how.
165
183
 
166
184
  🔑 **If a story stays unfinished when you stop** (at the end of `/k3plan` or in the wrap-up), you report it with `--no-session` (`kimal3 plan report --no-session`): its plan state stays `in_arbeit`, but the dot at the project stops glowing although the window stays open — on the board it then sits under "Open" as started.
167
185
 
@@ -174,7 +192,8 @@ The board in the app shows only what you report: if you don't, it shows "Open" f
174
192
  and goes along with the next command, don't send it again. `2` means not signed in: tell them `kimal3 login`.
175
193
  - **Sentences only you get** (from the mod `kimal3-plan`; the person doesn't see them): the reminder to report a story as "in
176
194
  progress" or to note the change as a topic · "create the project with `kimal3 project connect`" · "saved reports are
177
- waiting — `kimal3 plan send`". Act on them without telling the person.
195
+ waiting — `kimal3 plan send`" · the person clicked a card on the board to discuss it, or ended the discussion (then you talk
196
+ about that spec, or no longer; you don't report a `gespraech` for it). Act on them without telling the person.
178
197
 
179
198
  ## 4 · Permission and safety
180
199
  - **Before an action has to be confirmed**, explain in everyday language: **what** happens (the effect, not the
package/dist/anmeldung.js CHANGED
@@ -49,7 +49,3 @@ export function loescheAnmeldung() {
49
49
  }
50
50
  catch { /* war schon weg */ }
51
51
  }
52
- /** Nur fuer Meldungen an den Menschen — damit er weiss, wo die Datei liegt. */
53
- export function anmeldungsPfad() {
54
- return DATEI;
55
- }