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.
- package/README.md +10 -4
- package/claude-plugins/.claude-plugin/marketplace.json +2 -1
- package/claude-plugins/README.md +2 -0
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +1 -1
- package/claude-plugins/kimal3-logo/hooks/ablage.ts +8 -0
- package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
- package/claude-plugins/kimal3-logo/hooks/register.tsx +42 -18
- package/claude-plugins/kimal3-logo/hooks/update-rechnung.ts +175 -0
- package/claude-plugins/kimal3-logo/hooks/update.ts +244 -0
- package/claude-plugins/kimal3-logo/types/index.d.ts +4 -0
- package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
- package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +79 -0
- package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
- package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
- package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
- package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
- package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
- package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
- package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
- package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
- package/coach/de/kimal3-coach.md +33 -0
- package/coach/en/kimal3-coach.md +33 -0
- package/dist/argumente.js +12 -0
- package/dist/atomar.js +22 -0
- package/dist/befehle/einrichten.js +4 -4
- package/dist/befehle/login.js +2 -2
- package/dist/befehle/plan-hoch.js +8 -0
- package/dist/befehle/plan-melde.js +105 -0
- package/dist/befehle/plan-senden.js +149 -0
- package/dist/befehle/plan.js +45 -10
- package/dist/befehle/projekt-verbinden.js +141 -0
- package/dist/befehle/skill-install.js +2 -2
- package/dist/befehle-katalog-en.js +6 -6
- package/dist/befehle-katalog.js +8 -7
- package/dist/befehlswoerter.js +9 -0
- package/dist/einstellungen-schreiben.js +1 -1
- package/dist/kimal3.js +22 -0
- package/dist/meldungen-speicher.js +189 -0
- package/dist/{logo-ablegen.js → mods-ablegen.js} +87 -20
- package/dist/plugins-mischen.js +7 -2
- package/dist/projekt-ordner.js +189 -0
- package/dist/texte/einrichten.js +3 -1
- package/dist/texte/meldungen.js +127 -0
- package/dist/texte/plan.js +42 -22
- package/dist/texte/wache.js +4 -4
- package/dist/wache-rechnung.js +3 -1
- package/package.json +1 -1
- package/skill/commands/k3feierabend.md +20 -0
- package/skill/commands/k3plan.md +151 -211
- package/skill/en/commands/k3plan.md +157 -219
- 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
|
+
}
|
package/coach/de/kimal3-coach.md
CHANGED
|
@@ -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.
|
package/coach/en/kimal3-coach.md
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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")
|
package/dist/befehle/login.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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;
|