kimal3 0.6.5 → 0.7.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.
Files changed (45) hide show
  1. package/README.md +10 -4
  2. package/claude-plugins/.claude-plugin/marketplace.json +2 -1
  3. package/claude-plugins/README.md +2 -0
  4. package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +1 -1
  5. package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
  6. package/claude-plugins/kimal3-logo/hooks/register.tsx +1 -1
  7. package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
  8. package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +79 -0
  9. package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
  10. package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
  11. package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
  12. package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
  13. package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
  14. package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
  15. package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
  16. package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
  17. package/coach/de/kimal3-coach.md +33 -0
  18. package/coach/en/kimal3-coach.md +33 -0
  19. package/dist/argumente.js +12 -0
  20. package/dist/atomar.js +22 -0
  21. package/dist/befehle/einrichten.js +4 -4
  22. package/dist/befehle/login.js +2 -2
  23. package/dist/befehle/plan-hoch.js +8 -0
  24. package/dist/befehle/plan-melde.js +105 -0
  25. package/dist/befehle/plan-senden.js +149 -0
  26. package/dist/befehle/plan.js +45 -10
  27. package/dist/befehle/projekt-verbinden.js +141 -0
  28. package/dist/befehle/skill-install.js +2 -2
  29. package/dist/befehle-katalog-en.js +6 -6
  30. package/dist/befehle-katalog.js +8 -7
  31. package/dist/befehlswoerter.js +9 -0
  32. package/dist/einstellungen-schreiben.js +1 -1
  33. package/dist/kimal3.js +22 -0
  34. package/dist/meldungen-speicher.js +189 -0
  35. package/dist/{logo-ablegen.js → mods-ablegen.js} +29 -18
  36. package/dist/plugins-mischen.js +7 -2
  37. package/dist/projekt-ordner.js +189 -0
  38. package/dist/texte/einrichten.js +3 -1
  39. package/dist/texte/meldungen.js +127 -0
  40. package/dist/texte/plan.js +42 -22
  41. package/package.json +1 -1
  42. package/skill/commands/k3feierabend.md +20 -0
  43. package/skill/commands/k3plan.md +151 -211
  44. package/skill/en/commands/k3plan.md +157 -219
  45. package/skill/en/commands/k3wrapup.md +19 -0
@@ -0,0 +1,141 @@
1
+ import type { EngineInterface } from 'claude-code'
2
+
3
+ /**
4
+ * Zu welchem Projekt in der App gehoert der Ordner, in dem Claude laeuft? (Spec 2026-10-23-brett-in-echtzeit-design.md,
5
+ * 1.4, 1.7, S-27 bis S-31). DIESELBE Regel wie `cli/src/projekt-ordner.ts` -- der Mod kann `cli/src` nicht importieren,
6
+ * deshalb steht sie hier ein zweites Mal. Den Gleichlauf haelt die gemeinsame Tabelle `scripts/projektordner-faelle.mts`:
7
+ * `check:cli` prueft die CLI, `check:plan-mod` diesen Mod gegen dieselben Faelle. Aendert sich die Regel, an BEIDEN
8
+ * Stellen und in der Tabelle.
9
+ *
10
+ * Einziger Unterschied: Die Platte fragt der Mod ueber `$.fs`, also asynchron -- `gibt` und `lies` sind hier Promises.
11
+ */
12
+
13
+ export type Ordnerzettel = { format: 1; projekt_id: string; titel: string }
14
+ export type Ordnerlage =
15
+ | { art: 'projekt'; ordner: string; name: string; zettel: Ordnerzettel | null; ausStandJson: boolean }
16
+ | { art: 'kein-projektordner' }
17
+ type Gibt = (pfad: string) => Promise<boolean>
18
+ type Lies = (pfad: string) => Promise<string | null>
19
+
20
+ const ZETTEL_PFAD = 'plan/projekt.json'
21
+ const STAND_PFAD = 'plan/stand.json'
22
+ const BEISPIELE: readonly string[] = ['beispiel-rechnungen-erfassen', 'example-capture-invoices']
23
+ const PROJEKTORDNER: readonly string[] = ['projekte', 'projects']
24
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
25
+
26
+ export const normiert = (pfad: string) => pfad.replace(/\\/g, '/').replace(/\/+$/, '')
27
+ const nachOben = (pfad: string) => {
28
+ const teile = normiert(pfad).split('/')
29
+ return teile.map((_, i) => teile.slice(0, teile.length - i).join('/')).filter(p => p !== '')
30
+ }
31
+ const istWerkbank = (ordner: string) => ordner.startsWith('werkbank-') || ordner.startsWith('workbench-')
32
+
33
+ /** `werkbank-x/projekte/<name>` irgendwo im Pfad -- der aeusserste Treffer, damit `.claude/worktrees/x` nicht zaehlt. */
34
+ function projektordnerIn(pfad: string): { ordner: string; name: string } | null {
35
+ const teile = normiert(pfad).split('/')
36
+ for (let i = 0; i + 2 < teile.length; i++) {
37
+ if (!istWerkbank(teile[i] ?? '') || !PROJEKTORDNER.includes(teile[i + 1] ?? '')) continue
38
+ const name = teile[i + 2] ?? ''
39
+ return name === '' || BEISPIELE.includes(name) ? null : { ordner: teile.slice(0, i + 3).join('/'), name }
40
+ }
41
+ return null
42
+ }
43
+
44
+ function objektAus(text: string | null): Record<string, unknown> | null {
45
+ if (text === null) return null
46
+ try {
47
+ const j: unknown = JSON.parse(text.replace(/^\uFEFF/, ''))
48
+ return j !== null && typeof j === 'object' && !Array.isArray(j) ? (j as Record<string, unknown>) : null
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ /** Der Zettel, wenn er gueltig ist -- sonst `null` (kein JSON, andere Formatnummer, keine Projektnummer, kein Titel). */
55
+ export function zettelAus(text: string | null): Ordnerzettel | null {
56
+ const o = objektAus(text)
57
+ if (o === null || o.format !== 1 || typeof o.projekt_id !== 'string' || !UUID.test(o.projekt_id)) return null
58
+ if (typeof o.titel !== 'string' || o.titel.trim() === '') return null
59
+ return { format: 1, projekt_id: o.projekt_id, titel: o.titel }
60
+ }
61
+
62
+ /** Aus einer alten `plan/stand.json` nur die Nummer (und der Titel, falls er dasteht). */
63
+ function zettelAusStand(text: string | null, name: string): Ordnerzettel | null {
64
+ const o = objektAus(text)
65
+ if (o === null || typeof o.projekt_id !== 'string' || !UUID.test(o.projekt_id)) return null
66
+ const titel = typeof o.projekt === 'string' && o.projekt.trim() !== '' ? o.projekt : name
67
+ return { format: 1, projekt_id: o.projekt_id, titel }
68
+ }
69
+
70
+ /** `.` und `..` in einem Pfad mit `/` aufloesen, ohne die Platte zu fragen (die Wurzel bleibt stehen). */
71
+ function aufgeloest(pfad: string): string {
72
+ const teile: string[] = []
73
+ for (const t of pfad.split('/')) {
74
+ if (t === '.' || (t === '' && teile.length > 0)) continue
75
+ if (t === '..' && teile.length > 1) teile.pop()
76
+ else if (t !== '..') teile.push(t)
77
+ }
78
+ return teile.join('/')
79
+ }
80
+
81
+ async function naechsteGitWurzel(cwd: string, gibt: Gibt): Promise<string | undefined> {
82
+ for (const o of nachOben(cwd)) if (await gibt(`${o}/.git`)) return o
83
+ return undefined
84
+ }
85
+
86
+ /** Der Hauptordner einer Git-Arbeitskopie (die naechste `.git` ueber `cwd` ist eine Datei `gitdir: …/.git/worktrees/x`). */
87
+ export async function hauptordnerFuer(cwd: string, gibt: Gibt, lies: Lies): Promise<string | null> {
88
+ const wurzel = await naechsteGitWurzel(cwd, gibt)
89
+ if (wurzel === undefined) return null
90
+ const text = await lies(`${wurzel}/.git`)
91
+ const treffer = text === null ? null : /^gitdir:\s*(.+?)[\\/]\.git[\\/]worktrees[\\/][^\\/]+\s*$/.exec(text.trim())
92
+ if (treffer === null) return null
93
+ const ziel = normiert(treffer[1] ?? '')
94
+ return /^([A-Za-z]:)?\//.test(ziel) ? ziel : aufgeloest(`${wurzel}/${ziel}`)
95
+ }
96
+
97
+ export async function ordnerlage(cwd: string, gibt: Gibt, lies: Lies, hauptordner: string | null): Promise<Ordnerlage> {
98
+ // In einer Arbeitskopie zaehlt das Projekt des Hauptordners; der Zettel gehoert in ihre eigene Wurzel.
99
+ const projekt = projektordnerIn(hauptordner ?? cwd)
100
+ if (projekt === null) return { art: 'kein-projektordner' }
101
+ const ordner = hauptordner === null ? projekt.ordner : (await naechsteGitWurzel(cwd, gibt)) ?? projekt.ordner
102
+ const orte = ordner === projekt.ordner ? [ordner] : [ordner, projekt.ordner]
103
+ for (const o of orte) {
104
+ const zettel = zettelAus(await lies(`${o}/${ZETTEL_PFAD}`))
105
+ if (zettel !== null) return { art: 'projekt', ordner, name: projekt.name, zettel, ausStandJson: false }
106
+ }
107
+ for (const o of orte) {
108
+ const zettel = zettelAusStand(await lies(`${o}/${STAND_PFAD}`), projekt.name)
109
+ if (zettel !== null) return { art: 'projekt', ordner, name: projekt.name, zettel, ausStandJson: true }
110
+ }
111
+ return { art: 'projekt', ordner, name: projekt.name, zettel: null, ausStandJson: false }
112
+ }
113
+
114
+ // ── Platte über `$.fs` ──────────────────────────────────────────────────────────────────────────────────────────────
115
+
116
+ /** Liest eine Datei; `null`, wenn sie fehlt oder ein Ordner ist (`.git` eines normalen Projekts). */
117
+ export async function liesDatei($: EngineInterface, pfad: string): Promise<string | null> {
118
+ try {
119
+ return await $.fs.read(pfad)
120
+ } catch {
121
+ return null
122
+ }
123
+ }
124
+
125
+ /** Die Lage des Ordners, in dem die Sitzung laeuft. */
126
+ export async function lageJetzt($: EngineInterface): Promise<Ordnerlage> {
127
+ const cwd = normiert(await $.session.cwd())
128
+ const gibt: Gibt = p => $.fs.exists(p).catch(() => false)
129
+ const lies: Lies = p => liesDatei($, p)
130
+ return ordnerlage(cwd, gibt, lies, await hauptordnerFuer(cwd, gibt, lies))
131
+ }
132
+
133
+ /**
134
+ * Der Pfad einer Datei relativ zum Projektordner (`docs/x.md`), oder `null`, wenn sie nicht darin liegt.
135
+ * Gross- und Kleinschreibung zaehlt nicht (Windows); ein relativer Pfad gilt ab dem Startordner der Sitzung.
136
+ */
137
+ export function relativImOrdner(datei: string, ordner: string, cwd: string): string | null {
138
+ const absolut = /^([A-Za-z]:)?[\\/]/.test(datei) ? normiert(datei) : aufgeloest(`${normiert(cwd)}/${normiert(datei)}`)
139
+ const basis = `${normiert(ordner)}/`
140
+ return absolut.toLowerCase().startsWith(basis.toLowerCase()) && absolut.length > basis.length ? absolut.slice(basis.length) : null
141
+ }
@@ -0,0 +1,22 @@
1
+ import type { Register } from 'claude-code'
2
+ import { erinnerung } from './erinnerung'
3
+ import { neuerLauf } from './lauf'
4
+ import { lebenszeichen } from './lebenszeichen'
5
+ import { specText } from './spec-text'
6
+
7
+ // Der Mod `kimal3-plan` (Spec docs/superpowers/specs/2026-10-23-brett-in-echtzeit-design.md, 1.3): haelt das Board der
8
+ // Projektseite aktuell. Drei Aufgaben, je eine Datei: Punkt am Leben halten (lebenszeichen.ts), Spec-Texte hochladen
9
+ // (spec-text.ts), Claude ans Melden erinnern (erinnerung.ts). Dem Menschen sagt er nur etwas, wenn etwas nicht klappt --
10
+ // eine Zeile in der Statuszeile (fehlerzeile.ts). Er startet kein `kimal3` (`$.process` gibt es in der Claude-App nicht)
11
+ // und spricht selbst mit der App, mit dem Geraeteschluessel aus `~/.kimal3/anmeldung.json`.
12
+
13
+ // Die Sprache, wenn `~/.kimal3/sprache.json` fehlt (S-23). `kimal3 einrichten` setzt diese Zeile beim Ablegen in
14
+ // `~/.kimal3/claude-plugins` (`cli/src/mods-ablegen.ts`, `mitSprache`).
15
+ const SPRACHE: 'de' | 'en' = 'de'
16
+
17
+ export const register: Register = on => {
18
+ const lauf = neuerLauf(SPRACHE)
19
+ lebenszeichen(on, lauf)
20
+ erinnerung(on, lauf)
21
+ specText(on, lauf)
22
+ }
@@ -0,0 +1,45 @@
1
+ import type { EngineInterface, On } from 'claude-code'
2
+ import { dateiAus } from './erinnerung'
3
+ import { fehlerSetzen } from './fehlerzeile'
4
+ import { anApp, liesAusweis, type Lauf } from './lauf'
5
+ import { liesDatei, normiert, relativImOrdner } from './ordner'
6
+
7
+ /**
8
+ * Spec-Texte hochladen (Spec 1.3 Aufgabe 1, S-18): Schreibt oder aendert Claude eine `.md`-Datei im Projektordner, laedt
9
+ * der Mod ihren Text hoch (`POST /api/mod/spec-text`), damit die Lesefassung in der App stimmt, ohne dass jemand tippt.
10
+ * 🔑 Hochgeladen wird NUR eine Datei, die die App als Spec des Projekts kennt: Das Lebenszeichen nennt die Liste
11
+ * (`specDateien`, wie `datei` im Stand). Jede andere `.md` -- Notizen, README, Entwuerfe -- verlaesst den Rechner nicht
12
+ * (Datensparsamkeit, Ruling der Fixrunde). Eine brandneue Spec kommt mit ihrer Spec-Meldung (`kimal3 plan melde`).
13
+ * Sagt die App trotzdem `204` (keine Spec), ist das Erfolg ohne Wirkung. Der Pfad geht RELATIV zum Projektordner mit.
14
+ *
15
+ * Hochgeladen wird nach dem Werkzeug und ohne Claude darauf warten zu lassen (`$.clock.after(0, …)`). Was per Skript
16
+ * entsteht (Bash, Python), sieht der Mod nicht; das kommt mit `/k3plan` (S-19).
17
+ */
18
+ export function specText(on: On, lauf: Lauf): void {
19
+ on('tool.call', { tool: ['Write', 'Edit'] }, async ($, e, next) => {
20
+ const ergebnis = await next(e)
21
+ if (ergebnis.deny !== undefined || ergebnis.isError === true) return ergebnis
22
+ const datei = dateiAus(e)
23
+ const lage = lauf.lage
24
+ if (datei === null || !datei.toLowerCase().endsWith('.md') || lage.art !== 'projekt' || lage.zettel === null) return ergebnis
25
+ const pfad = relativImOrdner(datei, lage.ordner, await $.session.cwd())
26
+ if (pfad === null || !lauf.specDateien.includes(pfad)) return ergebnis
27
+ const projektId = lage.zettel.projekt_id
28
+ $.clock.after(0, () => void hochladen($, lauf, `${normiert(lage.ordner)}/${pfad}`, pfad, projektId))
29
+ return ergebnis
30
+ })
31
+ }
32
+
33
+ async function hochladen($: EngineInterface, lauf: Lauf, datei: string, pfad: string, projektId: string): Promise<void> {
34
+ const text = await liesDatei($, datei)
35
+ const ausweis = await liesAusweis($, lauf)
36
+ // Ohne Ausweis meldet sich die Statuszeile schon ueber das Lebenszeichen (S-11, S-22).
37
+ if (text === null || 'fehler' in ausweis) return
38
+ const antwort = await anApp($, lauf, '/api/mod/spec-text', ausweis.schluessel, { projektId, pfad, text })
39
+ if (antwort?.status === 200 || antwort?.status === 204) return fehlerSetzen($, lauf, { 'nicht-angemeldet': false, 'nicht-erreichbar': false })
40
+ if (antwort?.status === 401) return fehlerSetzen($, lauf, { 'nicht-angemeldet': true })
41
+ if (antwort === null || antwort.status >= 500) return fehlerSetzen($, lauf, { 'nicht-erreichbar': true })
42
+ // `413` (zu gross), `403`, `400`: Die App war da und hat abgelehnt -- kein Fall fuer die Statuszeile, der Text kommt
43
+ // mit `/k3plan`. Nur ins Debug-Protokoll.
44
+ $.ui.log(`kimal3-plan: Spec-Text ${pfad} nicht hochgeladen (${antwort.status})`, { to: 'debug' })
45
+ }
@@ -0,0 +1,10 @@
1
+ declare module 'claude-code' {
2
+ interface PluginState {
3
+ 'kimal3-plan': {
4
+ // Wann Claude zuletzt ans Melden erinnert wurde (`$.clock.now()`), `null`: noch nie in dieser Sitzung (hooks/erinnerung.ts, S-26).
5
+ letzteErinnerung: number | null
6
+ // Der Anstoss „Projekt anlegen“ ging in dieser Sitzung schon an Claude (hooks/erinnerung.ts, US-8).
7
+ anstossGegeben: boolean
8
+ }
9
+ }
10
+ }
@@ -136,6 +136,39 @@ nutze sie, statt vorschnell loszulegen.
136
136
  - **Sag von selbst, welche Schritte du ausgelassen hast** — auch einen, den ein Skill vorschreibt. Es wird danach
137
137
  gefragt, und aufgeflogen wiegt es schwerer als gesagt.
138
138
 
139
+ ### 3.6 Das Board — wann du der Projektseite meldest
140
+ Gilt in jedem Projektordner (ein Ordner direkt in `werkbank-…/projekte/`). Anderswo meldest du nichts und fragst nichts.
141
+ Das Board in der App zeigt nur, was du meldest: Wer nicht meldet, lässt es stundenlang „Offen“ zeigen, während gebaut wird.
142
+ - **Das Projekt gibt es einmal je Ordner:** `kimal3 projekt verbinden`. Es schweigt (`0`), wenn der Ordner schon verbunden ist.
143
+ Legt es ein Projekt an, gibst du den **einen** Satz weiter, den es ausgibt („Ich habe das Projekt ‚…‘ in der App angelegt.“).
144
+ 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
+ gibt es nur nach seinem Ja. Fehlt das Erfolgsprofil, sag den Satz einmal, arbeite weiter und melde nichts.
146
+ - **Du meldest zu sechs Zeitpunkten**, jedes Mal mit `kimal3 plan melde` (Eingabe: eine Meldung als JSON oder eine Liste,
147
+ z. B. `echo '[…]' | kimal3 plan melde`; Zeit und Sitzung setzt das Programm). Die Schlüssel von Spec und Story liest du mit
148
+ `kimal3 plan` aus der App — erfinde keine.
149
+ 1. **Spec angelegt oder ihre Stories geändert:**
150
+ `{"art":"spec","schluessel":"2026-10-24-testmonat","titel":"Testmonat","datei":"docs/superpowers/specs/2026-10-24-testmonat-design.md","stories":[{"schluessel":"US-1","titel":"Der Testmonat startet"}]}`
151
+ 2. **Der Bau einer Spec beginnt:** die Stories, die jetzt gebaut werden, auf „in Arbeit“ — **bevor** du den ersten Bau-Schritt oder
152
+ einen Subagenten losschickst:
153
+ `{"art":"story","spec":"2026-10-24-testmonat","story":"US-1","stand":"in_arbeit"}`
154
+ 3. **Eine Story ist gebaut und ihre Gegenproben sind gelaufen:** „gebaut“ mit Kriterien und dem `testen`-Satz. Der Satz ist eine Anleitung für jemanden ohne Technik: wo man es findet, was man tut, was dann zu sehen ist, woran man merkt, dass es nicht stimmt. Ein Befehl nur, wenn es keinen anderen Weg gibt — dann sagst du, wo man ihn eingibt (in Claude Code im Projektordner, mit `!` davor), und setzt ihn in Backticks. Ohne Befehl: „Öffne in der App ‚Kennzahlen‘ und klick auf den Beitrag vom 14.10. — dort steht ‚4.210 gesehen‘. Steht dort eine leere Zelle, sag ‚Passt nicht‘.“ Mit Befehl: „Tippe in Claude Code im Projektordner `! node wortzaehler.js beispiel.txt` — es erscheinen fünf Zeilen, oben ‚der (4)‘. Kommt eine Fehlermeldung, sag ‚Passt nicht‘.“
155
+ Beispiel:
156
+ `{"art":"story","spec":"2026-10-24-testmonat","story":"US-1","stand":"gebaut","kriterien":[{"text":"Startet Sabine am 1.11., endet er am 30.11.","gegenprobe":"bestanden"}],"testen":"Öffne in der App ‚Testmonat‘ und klick „Starten“ — darunter steht ‚Endet am 30.11.‘. Steht dort ein anderes Datum, sag ‚Passt nicht‘."}`
157
+ 4. **Etwas fällt auf, das keine Spec verspricht:** `{"art":"thema","schluessel":"T-4","text":"Der Kalender könnte Feiertage zeigen"}`
158
+ 5. **`/k3plan`:** Specs gegen den Code prüfen und jede Abweichung einzeln melden — dort steht, wie.
159
+ 6. **Feierabend:** den Tag abgleichen — dort steht, wie.
160
+
161
+ „Gebaut“ meldest du erst nach einer Gegenprobe, die in dieser Sitzung gelaufen ist, nie aus dem Gedächtnis.
162
+ - **Subagenten melden nie.** Der Mod erinnert nur dich, nicht sie: Du meldest „in Arbeit“ **vor** dem Auftrag an einen Subagenten, der
163
+ baut, und „gebaut“ **nachdem** du sein Ergebnis geprüft hast.
164
+ - **Bei Erfolg schweigst du.** Der Mensch bekommt keine Zeile darüber, dass du gemeldet hast — ständige Einblendungen stören ihn.
165
+ Sagen musst du nur den Satz von `projekt verbinden` und Probleme: Rückgabewert `1` heißt, die App hat abgesagt — lies den
166
+ Satz, behebe es, und gib weiter, was den Menschen betrifft. `3` heißt, die App ist nicht erreichbar: Die Meldung ist gemerkt
167
+ und geht beim nächsten Befehl mit, schick sie nicht noch einmal. `2` heißt nicht angemeldet: Sag ihm `kimal3 login`.
168
+ - **Sätze, die nur du bekommst** (vom Mod `kimal3-plan`, der Mensch sieht sie nicht): die Erinnerung, eine Story als „in Arbeit“
169
+ zu melden oder die Änderung als Thema zu notieren · „leg das Projekt mit `kimal3 projekt verbinden` an“ · „gemerkte Meldungen
170
+ warten — `kimal3 plan senden`“. Handle danach, ohne dem Menschen davon zu erzählen.
171
+
139
172
  ## 4 · Erlaubnis und Sicherheit
140
173
  - **Bevor eine Aktion bestätigt werden muss**, erkläre in Alltagssprache: **was** passiert (die Wirkung, nicht der
141
174
  Befehl), **warum** jetzt, **was es bedeutet** (umkehrbar? folgenreich?). Ruhig und klar, nicht alarmierend.
@@ -136,6 +136,39 @@ use them instead of starting off too quickly.
136
136
  - **Say on your own which steps you skipped** — even one a skill prescribes. They will ask about it,
137
137
  and being found out weighs more than saying it.
138
138
 
139
+ ### 3.6 The board — when you report to the project page
140
+ Applies in every project folder (a folder directly inside `workbench-…/projects/`). Elsewhere you report nothing and ask nothing.
141
+ The board in the app shows only what you report: if you don't, it shows "Open" for hours while you build.
142
+ - **There is one project per folder:** `kimal3 project connect`. It stays silent (`0`) if the folder is already connected.
143
+ If it creates a project, you pass on the **one** sentence it prints ("I created the project '…' in the app.").
144
+ If two projects have the same name, or it was deleted in the app, it asks: ask the person and never choose yourself; `--new`
145
+ only after their yes. If the success profile is missing, say the sentence once, keep working and report nothing.
146
+ - **You report at six moments**, each time with `kimal3 plan report` (input: one report as JSON or a list,
147
+ e.g. `echo '[…]' | kimal3 plan report`; the program sets time and session). You read the keys of spec and story from the app
148
+ with `kimal3 plan` — don't invent them.
149
+ 1. **A spec is created or its stories change:**
150
+ `{"art":"spec","schluessel":"2026-10-24-test-month","titel":"Test month","datei":"docs/superpowers/specs/2026-10-24-test-month-design.md","stories":[{"schluessel":"US-1","titel":"The test month starts"}]}`
151
+ 2. **The build of a spec begins:** the stories being built now go to "in progress" — **before** you take the first build step or
152
+ send off a subagent:
153
+ `{"art":"story","spec":"2026-10-24-test-month","story":"US-1","stand":"in_arbeit"}`
154
+ 3. **A story is built and its counter-checks have run:** "built" with criteria and the `testen` sentence. The sentence is a guide for someone without technical knowledge: where to find it, what to do, what must appear, how to notice it is wrong. 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. Without a command: "In the app open 'Metrics' and click the post from 14 Oct - it says '4,210 seen'. If a cell is empty, say 'Doesn't work'." With a command: "In Claude Code in the project folder type `! node wortzaehler.js beispiel.txt` - five lines appear, at the top 'der (4)'. If an error message appears, say 'Doesn't work'."
155
+ Example:
156
+ `{"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":"In the app open 'Test month' and click Start - below it says 'Ends on 30 Nov'. If it shows another date, say 'Doesn't work'."}`
157
+ 4. **Something comes up that no spec promises:** `{"art":"thema","schluessel":"T-4","text":"The calendar could show public holidays"}`
158
+ 5. **`/k3plan`:** check the specs against the code and report every deviation individually — it says how.
159
+ 6. **Wrap-up:** reconcile the day — it says how.
160
+
161
+ You report "built" only after a counter-check that ran in this session, never from memory.
162
+ - **Subagents never report.** The mod reminds only you, not them: you report "in progress" **before** the job to a subagent that
163
+ builds, and "built" **after** you have checked its result.
164
+ - **On success you stay silent.** The person gets no line saying that you reported — constant pop-ups bother them.
165
+ You only say the sentence from `project connect` and problems: exit code `1` means the app refused — read the
166
+ sentence, fix it, and pass on what concerns the person. `3` means the app is not reachable: the report is kept
167
+ and goes along with the next command, don't send it again. `2` means not signed in: tell them `kimal3 login`.
168
+ - **Sentences only you get** (from the mod `kimal3-plan`; the person doesn't see them): the reminder to report a story as "in
169
+ progress" or to note the change as a topic · "create the project with `kimal3 project connect`" · "saved reports are
170
+ waiting — `kimal3 plan send`". Act on them without telling the person.
171
+
139
172
  ## 4 · Permission and safety
140
173
  - **Before an action has to be confirmed**, explain in everyday language: **what** happens (the effect, not the
141
174
  command), **why** now, **what it means** (reversible? consequential?). Calm and clear, not alarming.
package/dist/argumente.js CHANGED
@@ -143,3 +143,15 @@ export function schalterOhneWert(argv, deutsch) {
143
143
  const namen = [deutsch, ...Object.keys(SCHALTER_EN).filter((e) => SCHALTER_EN[e] === deutsch)].map((n) => `--${n}`);
144
144
  return { gesetzt: argv.some((a) => namen.includes(a)), rest: argv.filter((a) => !namen.includes(a)) };
145
145
  }
146
+ /**
147
+ * Was `kimal3 plan melde` / `plan report` hinter dem Befehlswort annimmt: nichts, `--help`/`-h` oder genau
148
+ * `--ohne-sitzung` (`--no-session`). Alles andere -- auch ein Tippfehler wie `--ohne-sitzungg` -- bleibt „unbekannt"
149
+ * wie heute (Spec 2026-10-24 E-8). Die Meldung selbst kommt von der Eingabe, nie aus Argumenten.
150
+ */
151
+ export function planMeldeAngenommen(rest) {
152
+ if (rest.length === 0)
153
+ return true;
154
+ if (rest.length > 1)
155
+ return false;
156
+ return rest[0] === "--help" || rest[0] === "-h" || schalterOhneWert(rest, "ohne-sitzung").gesetzt;
157
+ }
package/dist/atomar.js ADDED
@@ -0,0 +1,22 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { renameSync, rmSync, writeFileSync } from "node:fs";
3
+ /**
4
+ * Schreibt eine Datei so, dass sie nie halb dasteht: erst eine Nebendatei, dann umbenennen (Board in Echtzeit, Task 7,
5
+ * Fixrunde 1). Ein Absturz oder voller Datentraeger mitten im Schreiben laesst die alte Datei (oder gar keine) zurueck,
6
+ * nie eine abgeschnittene -- eine abgeschnittene gemerkte Meldung oder ein halber Ordnerzettel waere unlesbar.
7
+ * Wirft wie `writeFileSync`; die Nebendatei raeumt sie dabei weg.
8
+ */
9
+ export function schreibeAtomar(pfad, text) {
10
+ const neben = `${pfad}.${randomBytes(3).toString("hex")}.tmp`; // kein-anzeigetext: Dateiname
11
+ try {
12
+ writeFileSync(neben, text, "utf8");
13
+ renameSync(neben, pfad);
14
+ }
15
+ catch (f) {
16
+ try {
17
+ rmSync(neben, { force: true });
18
+ }
19
+ catch { /* schon weg */ }
20
+ throw f;
21
+ }
22
+ }
@@ -6,7 +6,7 @@ import { leseAnmeldung } from "../anmeldung.js";
6
6
  import { alterKursCoachAblage, alterKursCoachPfad, coachDateiLiegt, legeCoachAb, meldeCoach } from "../coach-ablegen.js";
7
7
  import { istAlterKursCoach } from "../coach-rechnung.js";
8
8
  import { tragePluginsEin } from "../einstellungen-schreiben.js";
9
- import { logoAblegen } from "../logo-ablegen.js";
9
+ import { modsAblegen } from "../mods-ablegen.js";
10
10
  import { pluginStand } from "../plugins-mischen.js";
11
11
  import { ruf } from "../api.js";
12
12
  import { FIRMEN_OPTIONEN, fehlendeDateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
@@ -310,7 +310,7 @@ async function neuAnlegen(pfad, wunsch, email, gespeichert) {
310
310
  function pluginsEinrichten(git) {
311
311
  const s = sprache();
312
312
  const t = EINRICHTEN_TEXTE[s];
313
- const ablage = logoAblegen(s);
313
+ const ablage = modsAblegen(s);
314
314
  // Ohne Logo-Ordner kommt superpowers trotzdem (Durchsicht 2, Fund 1): eine Zeile zum Logo, dann weiter.
315
315
  if (!ablage.ok)
316
316
  console.log(t.logoNotWritten(ablage.detail));
@@ -328,8 +328,8 @@ function pluginsEinrichten(git) {
328
328
  return;
329
329
  }
330
330
  const stand = pluginStand(eintrag.inhalt);
331
- const namen = { superpowers: "superpowers", logo: t.logoName };
332
- const mit = (wert) => ["superpowers", "logo"].filter((p) => stand[p] === wert).map((p) => namen[p]);
331
+ const namen = { superpowers: "superpowers", logo: t.logoName, plan: t.planName };
332
+ const mit = (wert) => ["superpowers", "logo", "plan"].filter((p) => stand[p] === wert).map((p) => namen[p]);
333
333
  console.log(t.pluginsLine(mit("an"), mit("aus"), eintrag.art === "eingetragen"));
334
334
  // Auch wenn superpowers schon von früher drinsteht: ohne Git lädt es nicht (Durchsicht 4, Fund 3).
335
335
  if (!git && stand.superpowers !== "aus")
@@ -6,7 +6,7 @@ import { anmeldeAdresse, oeffneImBrowser } from "../browser.js";
6
6
  import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
7
7
  import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
8
8
  import { frischeCoachAuf } from "../coach-ablegen.js";
9
- import { frischeLogoAuf } from "../logo-ablegen.js";
9
+ import { frischeModsAuf } from "../mods-ablegen.js";
10
10
  import { stelleSkillUm } from "./skill-tausch.js";
11
11
  const warte = (ms) => new Promise((f) => setTimeout(f, ms));
12
12
  // 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
@@ -36,7 +36,7 @@ export async function login() {
36
36
  // 2026-10-18, L-1, L-8): `einrichten` legt es nach der Anmeldung ohnehin selbst ab.
37
37
  if (code === 0) {
38
38
  frischeCoachAuf(sprache());
39
- frischeLogoAuf(sprache());
39
+ frischeModsAuf(sprache());
40
40
  }
41
41
  return code;
42
42
  }
@@ -50,6 +50,14 @@ export async function planHoch(argv) {
50
50
  console.error(zerlegt.meldung);
51
51
  return 1;
52
52
  }
53
+ // 🔴 Seit 0.7.0 (Ruling Task 7, Board in Echtzeit 1.1): Claude meldet jede Aenderung einzeln (`plan melde`). Die ganze
54
+ // Datei geht nur noch mit --ueberschreiben hoch -- fuer eine Planung, die sich anders nicht mehr lesen laesst (die App
55
+ // nimmt das nur an, wenn seit `basis` keine 0.7-Meldung kam, R-6). Ohne den Schalter wuerde eine alte Datei Meldungen
56
+ // anderer Sitzungen wegwischen.
57
+ if (!ueber.gesetzt) {
58
+ console.error(PLAN_TEXTE[s].uploadNotNeeded);
59
+ return 1;
60
+ }
53
61
  const datei = planDateiOderAbsage(s);
54
62
  if (datei === null)
55
63
  return 1;
@@ -0,0 +1,105 @@
1
+ import { schalterOhneWert } from "../argumente.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { MELDUNGEN_TEXTE } from "../texte/meldungen.js";
4
+ import { merke, meldungFehler, sitzungFuerMeldung } from "../meldungen-speicher.js";
5
+ import { ladeSpecText, sendeUmschlaege } from "./plan-senden.js";
6
+ import { projektDesOrdners } from "./projekt-verbinden.js";
7
+ /*
8
+ `kimal3 plan melde` -- Claude meldet der Projektseite EINE Aenderung (oder eine Liste), statt die ganze Datei
9
+ hochzuladen (Board in Echtzeit, Spec 1.1, 1.2, E-6, S-10, US-1, US-4).
10
+
11
+ Das Projekt kommt vom Ordnerzettel, nie aus einer Wahl (S-12). `entstanden` ist JETZT, `sitzung` die Nummer der
12
+ Claude-Sitzung: Beides setzt das Programm, nicht Claude -- eine von Hand gesetzte Zeit koennte eine fremde,
13
+ neuere Meldung verdraengen (E-2, E-6).
14
+
15
+ Nicht erreichbar -> gemerkt, Rueckgabewert 3; beim naechsten Befehl geht sie mit. Erfolg ist still (US-4).
16
+ */
17
+ async function leseEingabe() {
18
+ if (process.stdin.isTTY)
19
+ return null;
20
+ const teile = [];
21
+ for await (const teil of process.stdin)
22
+ teile.push(Buffer.from(teil));
23
+ const text = Buffer.concat(teile).toString("utf8").replace(/^/, "");
24
+ return text.trim() === "" ? null : text;
25
+ }
26
+ export async function planMelde(argv) {
27
+ const t = MELDUNGEN_TEXTE[sprache()];
28
+ if (argv.includes("--help") || argv.includes("-h")) {
29
+ console.log(t.reportHelp);
30
+ return 0;
31
+ }
32
+ const projekt = projektDesOrdners();
33
+ if (projekt === null)
34
+ return 1;
35
+ const text = await leseEingabe();
36
+ if (text === null) {
37
+ console.error(t.noInput);
38
+ return 1;
39
+ }
40
+ let roh;
41
+ try {
42
+ roh = JSON.parse(text);
43
+ }
44
+ catch {
45
+ console.error(t.inputNoJson);
46
+ return 1;
47
+ }
48
+ const liste = Array.isArray(roh) ? roh : [roh];
49
+ if (liste.length === 0) {
50
+ console.error(t.noInput);
51
+ return 1;
52
+ }
53
+ for (const [i, m] of liste.entries()) {
54
+ const feld = meldungFehler(m);
55
+ if (feld !== null) {
56
+ console.error(t.reportUnreadable(i + 1, feld));
57
+ return 1;
58
+ }
59
+ }
60
+ const entstanden = new Date().toISOString();
61
+ // Prüfen ist nicht Arbeiten (Spec 2026-10-24 E-8): `--ohne-sitzung` schickt keine Sitzung mit, also leuchtet kein Punkt.
62
+ const sitzung = schalterOhneWert(argv, "ohne-sitzung").gesetzt ? null : sitzungFuerMeldung(process.env);
63
+ const umschlaege = liste.map((m) => ({ meldung: m, entstanden, sitzung }));
64
+ const sendung = await sendeUmschlaege(projekt.projektId, umschlaege);
65
+ const u = MELDUNGEN_TEXTE[sprache()];
66
+ // Erst die Absagen (je Satz einmal), dann was mit dem Rest geschieht.
67
+ for (const satz of new Set(sendung.je.flatMap((e) => (e.art === "abgesagt" ? [e.meldung] : []))))
68
+ console.error(satz);
69
+ const offen = umschlaege.filter((_, i) => sendung.je[i].art === "offen");
70
+ const p = sendung.problem;
71
+ if (offen.length > 0 && p !== null && p.art !== "absage") {
72
+ // Jede einzeln merken und zaehlen: Scheitert das Merken mittendrin, muss der Satz sagen, wie viele gemerkt sind.
73
+ const gemerkt = offen.filter((x) => merke(projekt.projektId, projekt.ordner, x)).length;
74
+ const satz = gemerkt === offen.length ? (p.art === "anmelden" ? u.storedSignIn : u.stored)
75
+ : gemerkt > 0 ? u.storedPartly(gemerkt, offen.length)
76
+ : p.art === "anmelden" ? p.meldung : u.notStored;
77
+ if (p.art === "anmelden" && gemerkt < offen.length && gemerkt > 0)
78
+ console.error(p.meldung);
79
+ console.error(satz);
80
+ return p.art === "anmelden" ? 2 : 3;
81
+ }
82
+ // Ruling Task 7: Eine Spec-Datei, die VOR ihrer Meldung geschrieben wurde, hat die App noch nicht -- jetzt mitnehmen.
83
+ // Fuer jede ANGENOMMENE Spec-Meldung, auch wenn eine andere der Liste abgesagt wurde.
84
+ let code = sendung.je.some((e) => e.art === "abgesagt") ? 1 : 0;
85
+ for (const [i, x] of umschlaege.entries()) {
86
+ const m = x.meldung;
87
+ if (sendung.je[i].art !== "angenommen" || m.art !== "spec" || m.datei === null)
88
+ continue;
89
+ const text = await ladeSpecText(projekt.projektId, projekt.ordner, m.schluessel, m.datei);
90
+ if (text === "anmelden") {
91
+ console.error(u.specTextSignIn(m.datei));
92
+ return 2;
93
+ }
94
+ if (text === "fehler") {
95
+ console.error(u.specTextFailed(m.datei));
96
+ code = 3;
97
+ }
98
+ else if (text === "zu-gross") {
99
+ console.error(u.specTextTooBig(m.datei));
100
+ if (code === 0)
101
+ code = 1;
102
+ }
103
+ }
104
+ return code;
105
+ }