kimal3 0.6.4 → 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 (51) 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/ablage.ts +8 -0
  6. package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
  7. package/claude-plugins/kimal3-logo/hooks/register.tsx +42 -18
  8. package/claude-plugins/kimal3-logo/hooks/update-rechnung.ts +175 -0
  9. package/claude-plugins/kimal3-logo/hooks/update.ts +244 -0
  10. package/claude-plugins/kimal3-logo/types/index.d.ts +4 -0
  11. package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
  12. package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +79 -0
  13. package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
  14. package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
  15. package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
  16. package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
  17. package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
  18. package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
  19. package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
  20. package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
  21. package/coach/de/kimal3-coach.md +33 -0
  22. package/coach/en/kimal3-coach.md +33 -0
  23. package/dist/argumente.js +12 -0
  24. package/dist/atomar.js +22 -0
  25. package/dist/befehle/einrichten.js +4 -4
  26. package/dist/befehle/login.js +2 -2
  27. package/dist/befehle/plan-hoch.js +8 -0
  28. package/dist/befehle/plan-melde.js +105 -0
  29. package/dist/befehle/plan-senden.js +149 -0
  30. package/dist/befehle/plan.js +45 -10
  31. package/dist/befehle/projekt-verbinden.js +141 -0
  32. package/dist/befehle/skill-install.js +2 -2
  33. package/dist/befehle-katalog-en.js +6 -6
  34. package/dist/befehle-katalog.js +8 -7
  35. package/dist/befehlswoerter.js +9 -0
  36. package/dist/einstellungen-schreiben.js +1 -1
  37. package/dist/kimal3.js +22 -0
  38. package/dist/meldungen-speicher.js +189 -0
  39. package/dist/{logo-ablegen.js → mods-ablegen.js} +87 -20
  40. package/dist/plugins-mischen.js +7 -2
  41. package/dist/projekt-ordner.js +189 -0
  42. package/dist/texte/einrichten.js +3 -1
  43. package/dist/texte/meldungen.js +127 -0
  44. package/dist/texte/plan.js +42 -22
  45. package/dist/texte/wache.js +4 -4
  46. package/dist/wache-rechnung.js +3 -1
  47. package/package.json +1 -1
  48. package/skill/commands/k3feierabend.md +20 -0
  49. package/skill/commands/k3plan.md +151 -211
  50. package/skill/en/commands/k3plan.md +157 -219
  51. package/skill/en/commands/k3wrapup.md +19 -0
@@ -0,0 +1,197 @@
1
+ import type { EngineInterface, On } from 'claude-code'
2
+ import { anstossOhneZettel, gemerkteMeldungenAnstossen, sitzungNeu } from './erinnerung'
3
+ import { fehlerSind } from './fehlerzeile'
4
+ import { anApp, liesAusweis, umgebungLesen, type Lauf } from './lauf'
5
+ import { lageJetzt } from './ordner'
6
+
7
+ /**
8
+ * Den Punkt am Leben halten (Spec 1.3 Aufgabe 2, E-4, US-2; Bauplan R-5, Review Focus 5). Solange die Sitzung in einem
9
+ * Projektordner mit Ordnerzettel offen ist, meldet sich der Mod alle 10 Sekunden bei der App (`POST
10
+ * /api/mod/lebenszeichen`, gemessen M-2: der Zeitgeber laeuft auch, waehrend Claude arbeitet) und merkt sich, ob diese
11
+ * Sitzung eine Story „in Arbeit“ hat. Beim Ende der Sitzung meldet er sich ab, damit der Punkt in unter 1 Sekunde weg
12
+ * ist; ein abgeschossener Prozess meldet sich nie ab, dafuer gibt es die 30-Sekunden-Regel der App.
13
+ *
14
+ * Jeder Takt liest Ordner, Zettel und Ausweis neu: So kommt ein Zettel, den Claude nach dem Anstoss anlegt (US-8), und ein
15
+ * `kimal3 login` beim naechsten Takt an, ohne Neustart.
16
+ */
17
+ const TAKT_MS = 10_000
18
+ const MELDUNG = /\bplan\s+(melde|report|senden|send)\b/
19
+
20
+ /**
21
+ * Die Nummer dieser Sitzung fuer `kimal3 plan melde` (Ruling Final, Important 2): Ob `CLAUDE_CODE_SESSION_ID` nach
22
+ * `/clear` neu ist, ist ungemessen -- der Mod setzt deshalb bei jedem Start, auch nach `/clear`, `KIMAL3_SITZUNG`, und
23
+ * das Programm nimmt sie vor Claudes eigener Variable (die bleibt unberuehrt). Gemessen am 23.10.: die Variable kommt
24
+ * bei einem Bash-Aufruf an (docs/qa/2026-10-23-brett-echtzeit-messungen.md, „Final — Sitzungskennung“). Scheitert das
25
+ * Setzen, laeuft der Mod weiter; das Programm faellt dann auf `CLAUDE_CODE_SESSION_ID` zurueck.
26
+ */
27
+ async function sitzungFuerProgramm($: EngineInterface): Promise<void> {
28
+ await $.env.set('KIMAL3_SITZUNG', await $.session.id()).catch(() => undefined)
29
+ }
30
+
31
+ export function lebenszeichen(on: On, lauf: Lauf): void {
32
+ on('session.start', async ($, e, next) => {
33
+ const ergebnis = await next(e)
34
+ await sitzungFuerProgramm($)
35
+ await starte($, lauf)
36
+ return ergebnis
37
+ })
38
+ // Haengt sich eine Oberflaeche an, ohne dass es lief (etwa nach einem frueheren Fehlstart): noch einmal versuchen.
39
+ on('session.attach', async ($, e, next) => {
40
+ const ergebnis = await next(e)
41
+ await starte($, lauf)
42
+ return ergebnis
43
+ })
44
+ // `/clear` beendet die Sitzung (abgemeldet unten), der Prozess laeuft unter einer neuen Kennung weiter -- ohne
45
+ // `session.start`. Die neue Sitzung meldet sich deshalb hier wieder an.
46
+ on('classic.SessionStart', async ($, e, next) => {
47
+ const ergebnis = await next(e)
48
+ if (e.source === 'clear') {
49
+ Object.assign(lauf, { beendet: false, inArbeit: null, specDateien: [], abgelehnt: null, gemeldetFuer: null, meldungenPruefen: true })
50
+ await sitzungNeu($)
51
+ await sitzungFuerProgramm($)
52
+ await starte($, lauf)
53
+ }
54
+ return ergebnis
55
+ })
56
+ // Claude hat gerade gemeldet (`kimal3 plan melde`/`plan senden`): Was der Mod ueber „in Arbeit“ weiss, ist ab jetzt
57
+ // veraltet -- bis zu 10 s lang kaeme sonst die Erinnerung, obwohl die Meldung schon raus ist (Final-Review, Important 1).
58
+ // Also gilt es als unbekannt (keine Erinnerung), und ein Lebenszeichen holt sofort den neuen Stand. Ein Fehlalarm des
59
+ // Musters kostet nur ein Lebenszeichen mehr.
60
+ on('tool.call', { tool: ['Bash', 'PowerShell'] }, async ($, e, next) => {
61
+ const ergebnis = await next(e)
62
+ if (!MELDUNG.test(e.command)) return ergebnis
63
+ lauf.inArbeit = null
64
+ lauf.meldungen++
65
+ // Nur, wenn der Zeitgeber laeuft (Projektordner, nicht beendet): kein zweiter Zeitgeber, und `takt` faellt aus, wenn
66
+ // schon eines unterwegs ist -- dessen Antwort zaehlt dann nicht (`meldungen`), bis der naechste Takt fragt.
67
+ if (lauf.takt !== null) $.clock.after(0, () => void takt($, lauf))
68
+ return ergebnis
69
+ })
70
+ on('session.end', async ($, e, next) => {
71
+ // 🔴 ZUERST den Zeitgeber anhalten, DANN abmelden (Review Focus 5): sonst kann ein letzter Takt nach dem Abmelden
72
+ // ankommen. Die App lehnt ihn zwar ab (`410`), aber `beendet` haelt auch einen Takt an, der schon unterwegs ist.
73
+ lauf.beendet = true
74
+ lauf.generation++
75
+ lauf.takt?.cancel()
76
+ lauf.takt = null
77
+ lauf.inArbeit = null
78
+ // Die ganze Kette von `session.end` hat ~1,5 s (gemessen M-2); der Abruf endet vorher, damit Claude Code nicht
79
+ // mitten in ihm abschneidet und die Hooks nach uns ihre Zeit behalten.
80
+ const grenze = next.budget.remainingMs - 100
81
+ if (lauf.gemeldetFuer !== null) await abmelden($, lauf, lauf.gemeldetFuer, e.sessionId, grenze)
82
+ lauf.gemeldetFuer = null
83
+ return next(e)
84
+ })
85
+ }
86
+
87
+ async function starte($: EngineInterface, lauf: Lauf): Promise<void> {
88
+ // 🔴 `startet` steht VOR dem ersten `await`: `session.start` und `session.attach` kommen fast gleichzeitig, und
89
+ // beide bestanden sonst die Pruefung auf `takt` -- zwei Zeitgeber, einer davon nie angehalten (Befund der Durchsicht).
90
+ if (lauf.takt !== null || lauf.startet || lauf.beendet) return
91
+ lauf.startet = true
92
+ try {
93
+ await umgebungLesen($, lauf)
94
+ lauf.lage = await lageJetzt($)
95
+ // Kein Projektordner (Werkbank, Beispielordner, ausserhalb): der Mod tut nichts (S-30).
96
+ if (lauf.lage.art !== 'projekt') return
97
+ await anstossOhneZettel($, lauf)
98
+ lauf.takt = $.clock.every(TAKT_MS, () => void takt($, lauf))
99
+ // Das erste Lebenszeichen sofort (Punkt in unter 1 s, E-3), aber ohne den Sitzungsstart darauf warten zu lassen.
100
+ $.clock.after(0, () => void takt($, lauf))
101
+ } finally {
102
+ lauf.startet = false
103
+ }
104
+ }
105
+
106
+ /** Die Sitzung (`generation`), fuer die gerade ein Lebenszeichen unterwegs ist -- je Sitzung hoechstens eines. */
107
+ let unterwegs: number | null = null
108
+
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.
111
+ const generation = lauf.generation
112
+ if (lauf.beendet || unterwegs === generation) return
113
+ unterwegs = generation
114
+ try {
115
+ await einTakt($, lauf, generation)
116
+ } finally {
117
+ if (unterwegs === generation) unterwegs = null
118
+ }
119
+ }
120
+
121
+ async function einTakt($: EngineInterface, lauf: Lauf, generation: number): Promise<void> {
122
+ // Nach jedem `await`: Ist die Sitzung, fuer die dieser Takt lief, inzwischen vorbei (Ende, `/clear`), wirkt er nicht
123
+ // mehr -- sonst haelt ein spaetes `410` der alten Sitzung den Zeitgeber der neuen an.
124
+ const vorbei = () => lauf.beendet || lauf.generation !== generation
125
+ const lage = await lageJetzt($)
126
+ if (vorbei()) return
127
+ lauf.lage = lage
128
+ if (lage.art !== 'projekt') return
129
+ const ausweis = await liesAusweis($, lauf)
130
+ if (vorbei()) return
131
+ if ('fehler' in ausweis) {
132
+ lauf.inArbeit = null
133
+ return fehlerSind($, lauf, [ausweis.fehler])
134
+ }
135
+ const zettel = lage.zettel
136
+ // Ohne Zettel gibt es noch kein Projekt (den Anstoss bekam Claude beim Start); nach `403` fragt der Mod nicht mehr.
137
+ if (zettel === null || zettel.projekt_id === lauf.abgelehnt) return fehlerSind($, lauf, [])
138
+
139
+ const sitzung = await $.session.id()
140
+ if (vorbei()) return
141
+ // Kam waehrend des Abrufs eine Meldung, ist seine Antwort zu „in Arbeit“ womoeglich von davor: sie zaehlt nicht.
142
+ const meldungen = lauf.meldungen
143
+ lauf.gemeldetFuer = zettel.projekt_id
144
+ const antwort = await anApp($, lauf, '/api/mod/lebenszeichen', ausweis.schluessel, { projektId: zettel.projekt_id, sitzung })
145
+ if (vorbei()) return
146
+
147
+ if (antwort?.status === 200) {
148
+ const gelesen = antwortAus(antwort.text)
149
+ // Kann der Mod nicht feststellen, ob es geklappt hat, gilt es als Fehler (US-4).
150
+ if (gelesen === null) return fehlerSind($, lauf, ['nicht-erreichbar'])
151
+ if (lauf.meldungen === meldungen) lauf.inArbeit = gelesen.inArbeit
152
+ lauf.specDateien = gelesen.specDateien
153
+ fehlerSind($, lauf, [])
154
+ if (lauf.meldungenPruefen) {
155
+ lauf.meldungenPruefen = false
156
+ await gemerkteMeldungenAnstossen($, lauf)
157
+ }
158
+ return
159
+ }
160
+ if (antwort?.status === 401) return fehlerSind($, lauf, ['nicht-angemeldet'])
161
+ // `403`: das Projekt gehoert diesem Konto nicht (mehr) -- etwa ein Kollege mit demselben Repo, oder in der App
162
+ // geloescht (das klaert `kimal3 projekt verbinden`, S-31). Kein Fehler fuer den Menschen, kein weiterer Versuch.
163
+ if (antwort?.status === 403) {
164
+ lauf.abgelehnt = zettel.projekt_id
165
+ lauf.inArbeit = null
166
+ return fehlerSind($, lauf, [])
167
+ }
168
+ // `410`: diese Sitzung ist schon abgemeldet -- nie wiederbeleben, sonst kaeme der Punkt zurueck (Review Focus 5).
169
+ if (antwort?.status === 410) {
170
+ lauf.takt?.cancel()
171
+ lauf.takt = null
172
+ lauf.inArbeit = null
173
+ return fehlerSind($, lauf, [])
174
+ }
175
+ fehlerSind($, lauf, ['nicht-erreichbar'])
176
+ }
177
+
178
+ /** `{ inArbeit, specDateien }` aus der Antwort; ohne gueltiges `inArbeit` `null`. Fehlt `specDateien` (aeltere App): leer. */
179
+ function antwortAus(text: string): { inArbeit: boolean; specDateien: string[] } | null {
180
+ try {
181
+ const o: unknown = JSON.parse(text)
182
+ const inArbeit = (o as { inArbeit?: unknown } | null)?.inArbeit
183
+ const dateien = (o as { specDateien?: unknown } | null)?.specDateien
184
+ if (typeof inArbeit !== 'boolean') return null
185
+ return { inArbeit, specDateien: Array.isArray(dateien) ? dateien.filter((d): d is string => typeof d === 'string') : [] }
186
+ } catch {
187
+ return null
188
+ }
189
+ }
190
+
191
+ async function abmelden($: EngineInterface, lauf: Lauf, projektId: string, sitzung: string, grenzeMs: number): Promise<void> {
192
+ const ausweis = await liesAusweis($, lauf)
193
+ if ('fehler' in ausweis) return
194
+ // Der Abruf brauchte gemessen 4 ms (M-2). Reicht die Zeit nicht, kostet das hoechstens den Punkt fuer 30 Sekunden.
195
+ const antwort = await anApp($, lauf, '/api/mod/abmelden', ausweis.schluessel, { projektId, sitzung }, grenzeMs)
196
+ if (antwort?.status !== 204) $.ui.log(`kimal3-plan: abmelden ${antwort === null ? 'nicht erreichbar' : antwort.status}`, { to: 'debug' })
197
+ }
@@ -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;