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