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.
- package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -7
- package/claude-plugins/kimal3-plan/hooks/gespraech.ts +141 -0
- package/claude-plugins/kimal3-plan/hooks/lauf.ts +50 -1
- package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +61 -8
- package/claude-plugins/kimal3-plan/hooks/register.tsx +5 -2
- package/claude-plugins/kimal3-plan/hooks/spec-text.ts +8 -3
- package/claude-plugins/kimal3-plan/types/index.d.ts +4 -0
- package/coach/de/kimal3-coach.md +32 -11
- package/coach/en/kimal3-coach.md +30 -11
- package/dist/anmeldung.js +0 -4
- package/dist/arbeitsplatz-dateien.js +8 -8
- package/dist/befehle/arbeitsplatz.js +11 -91
- package/dist/befehle/firma-daten.js +5 -67
- package/dist/befehle/kontext-gemeinsam.js +7 -40
- package/dist/befehle/name-und-firma.js +81 -0
- package/dist/befehle/skill-install.js +3 -2
- package/dist/befehle/werkbank.js +7 -75
- package/dist/firmen-liste.js +11 -4
- package/dist/meldungen-speicher.js +5 -1
- package/dist/texte/meldungen.js +2 -2
- package/dist/wache-schalter.js +0 -8
- package/package.json +30 -30
- package/skill/commands/k3feierabend.md +2 -2
- package/skill/commands/k3plan.md +14 -8
- package/skill/en/commands/k3plan.md +15 -9
- package/skill/en/commands/k3wrapup.md +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kimal3-plan",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
179
|
-
|
|
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
|
-
|
|
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.
|
|
9
|
-
// (spec-text.ts), Claude ans Melden erinnern (erinnerung.ts)
|
|
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
|
-
|
|
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
|
}
|
package/coach/de/kimal3-coach.md
CHANGED
|
@@ -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
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
package/coach/en/kimal3-coach.md
CHANGED
|
@@ -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
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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`"
|
|
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
|