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.
- 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/app-oeffnen.ts +37 -9
- package/claude-plugins/kimal3-logo/hooks/register.tsx +1 -1
- 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} +29 -18
- 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/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,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;
|
|
@@ -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
|
+
}
|