kimal3 0.1.5 β†’ 0.2.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.
@@ -0,0 +1,254 @@
1
+ import { zerlege } from "../argumente.js";
2
+ import { ruf } from "../api.js";
3
+ import { heuteAus } from "../nachricht.js";
4
+ import { istUnsichererPfad } from "../arbeitsplatz-dateien.js";
5
+ import { LEER, VERWALTETE_DATEIEN, alsMerkzettel, sicherungsOrdner, sicherungsZiel, lageFuerDatei, ratNachUnberuehrt, } from "../kontext-stand.js";
6
+ import { leseEineDatei, leseMerkzettelDatei, schreibeMerkzettelDatei, schreibeVerwalteteDatei, sichereDatei, } from "../kontext-dateien.js";
7
+ import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js";
8
+ /**
9
+ * Holt den Firmenkontext von der App und legt ihn in den verwalteten Dateien
10
+ * des gefundenen Arbeitsplatzes ab -- `CLAUDE.md` und die fuenf Dateien in
11
+ * `kontext/`.
12
+ *
13
+ * πŸ”΄ **`kontext/kurzprofil.md` ist seit Ruling K-24 (Task 7) dabei.** Sie wurde
14
+ * beim ERSTEN Ausliefern gefuellt und danach nie wieder aufgefrischt -- waehrend
15
+ * sie selbst dem Kunden zusicherte, sie werde β€žzentral in der App gepflegt".
16
+ *
17
+ * πŸ”΄ **R2 (Spec Β§14): Dieser Befehl fragt NIE `git status`.** Er entscheidet
18
+ * ausschliesslich ueber den Merkzettel (`.kimal3-stand.json`), egal ob der
19
+ * Ordner ein Git-Repo ist oder nicht -- sonst gaebe es zwei Verhalten fuer
20
+ * denselben Befehl, und das schwerer erklaerbare traefe ausgerechnet den, der
21
+ * Git benutzt.
22
+ *
23
+ * β›” **Unterhalb von `werkbank-<name>/` wird nichts geschrieben** (Spec Β§4).
24
+ * Dieser Befehl kennt genau die Pfade aus `VERWALTETE_DATEIEN`, und in keinem
25
+ * davon steht ein Personenname.
26
+ *
27
+ * πŸ” **Ruling T9-2:** Die Gegenrichtung, `kontextHoch`, wohnt seit dem Split
28
+ * von `kontext.ts` in `kontext-hoch.ts`. Beide teilen sich Fundregel,
29
+ * Firmenwahl-Absage und die `--firma`/`--firma-id`-Optionen ueber
30
+ * `kontext-gemeinsam.ts`.
31
+ */
32
+ const HILFE = `
33
+ kimal3 kontext runter [--firma "Name" | --firma-id "…"]
34
+
35
+ Holt den Firmenkontext (CLAUDE.md + die fuenf Dateien in kontext/) und legt
36
+ ihn in deinem KI-Arbeitsplatz ab. Was du selbst geaendert hast, fasst der
37
+ Befehl nicht an -- er sagt dir, wie du es einbringst.
38
+
39
+ --firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
40
+ Die Absage nennt dir die Namen als fertige Befehle.
41
+
42
+ --firma-id "…" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
43
+ Auch diese Absage nennt dir fertige Befehle.
44
+ `;
45
+ export async function kontextRunter(argv) {
46
+ if (argv.includes("--help") || argv.includes("-h")) {
47
+ console.log(HILFE);
48
+ return 0;
49
+ }
50
+ const zerlegt = zerlege(argv, ERLAUBT);
51
+ if (!zerlegt.ok) {
52
+ console.error(zerlegt.meldung);
53
+ return 1;
54
+ }
55
+ // πŸ“Œ Seit Task 7 gemeinsam mit `kontext hoch` und `kontext fragen`
56
+ // (`arbeitsplatzOderAbsage`). Hier standen dieselben vier Zeilen abgetippt;
57
+ // bei drei Aufrufern ist das keine Wiederholung mehr, sondern eine Falle --
58
+ // wer eine Zeile schaerft, schaerft eine von dreien.
59
+ const ordner = arbeitsplatzOderAbsage();
60
+ if (ordner === null)
61
+ return 1;
62
+ const wunsch = zerlegt.werte.optionen.firma;
63
+ const wunschId = zerlegt.werte.optionen["firma-id"];
64
+ const suche = new URLSearchParams();
65
+ if (wunsch !== undefined)
66
+ suche.set("firma", wunsch);
67
+ if (wunschId !== undefined)
68
+ suche.set("firma-id", wunschId);
69
+ const pfad = suche.toString() ? `/api/cli/kontext?${suche.toString()}` : "/api/cli/kontext";
70
+ const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true });
71
+ if (antwort.status !== 200) {
72
+ return absage(antwort, "kontext runter");
73
+ }
74
+ // πŸ”΄ Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dasselbe Muster wie
75
+ // in `firma-daten.ts`. `ruf` verschluckt Parse-Fehler zu `null`.
76
+ if (antwort.koerper === null || typeof antwort.koerper !== "object") {
77
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
78
+ return 3;
79
+ }
80
+ const daten = alsKontextAntwort(antwort.koerper);
81
+ if (daten === null) {
82
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
83
+ return 3;
84
+ }
85
+ const lokalerMerkzettel = leseMerkzettelDatei(ordner);
86
+ const heute = heuteAus(new Date());
87
+ const geschrieben = [];
88
+ const gesicherte = [];
89
+ const unberuehrt = [];
90
+ for (const verwalteterPfad of VERWALTETE_DATEIEN) {
91
+ const serverDatei = daten.dateien.find((d) => d.pfad === verwalteterPfad);
92
+ if (!serverDatei) {
93
+ console.error(`Die Antwort enthaelt keine Datei β€ž${verwalteterPfad}". Bitte melden.`);
94
+ return 3;
95
+ }
96
+ // Zweites Netz -- s. Kommentar an `schreibeVerwalteteDatei`: `pfad` kommt
97
+ // hier aus der eigenen Iteration, nicht roh vom Server, trotzdem geprueft.
98
+ if (istUnsichererPfad(serverDatei.pfad)) {
99
+ console.error(`Die Antwort enthaelt einen unsicheren Pfad: β€ž${serverDatei.pfad}". Bitte melden.`);
100
+ return 3;
101
+ }
102
+ const lokalGelesen = leseEineDatei(ordner, verwalteterPfad);
103
+ if (!lokalGelesen.ok) {
104
+ console.error(lokalGelesen.meldung);
105
+ return 1;
106
+ }
107
+ const eintrag = lokalerMerkzettel.dateien[verwalteterPfad] ?? null;
108
+ const serverStand = daten.merkzettel.dateien[verwalteterPfad]?.stand ?? LEER;
109
+ const lage = lageFuerDatei({
110
+ lokalerInhalt: lokalGelesen.inhalt,
111
+ serverInhalt: serverDatei.inhalt,
112
+ eintrag,
113
+ serverStand,
114
+ });
115
+ if (lage === "beides") {
116
+ // `lokalGelesen.inhalt` ist hier garantiert eine Zeichenkette:
117
+ // `lageFuerDatei` liefert "beides" nie, wenn `lokalerInhalt === null`.
118
+ const sicherung = sichereDatei(ordner, verwalteterPfad, lokalGelesen.inhalt, heute);
119
+ if (!sicherung.ok) {
120
+ console.error(sicherung.meldung);
121
+ return 1;
122
+ }
123
+ // Ruling T8-2: `sichereDatei` ueberschreibt nie und ueberspringt nie --
124
+ // `nebenVorhandener` sagt nur, ob HEUTE schon eine andere Sicherung
125
+ // dieser Datei existiert. Beide Faelle sind echte Erfolge und werden
126
+ // unten bewusst UNTERSCHIEDLICH gemeldet (Β§8).
127
+ gesicherte.push({
128
+ pfad: verwalteterPfad,
129
+ ziel: sicherung.ziel,
130
+ nebenVorhandener: sicherung.nebenVorhandener,
131
+ });
132
+ }
133
+ if (lage === "server-neuer" || lage === "beides") {
134
+ const geschriebenErg = schreibeVerwalteteDatei(ordner, verwalteterPfad, serverDatei.inhalt);
135
+ if (!geschriebenErg.ok) {
136
+ console.error(geschriebenErg.meldung);
137
+ return 1;
138
+ }
139
+ geschrieben.push(verwalteterPfad);
140
+ }
141
+ if (lage === "lokal-geaendert") {
142
+ unberuehrt.push(verwalteterPfad);
143
+ }
144
+ }
145
+ /*
146
+ πŸ”΄ Der Merkzettel des SERVERS wird geschrieben -- unabhaengig davon, ob in
147
+ diesem Lauf ueberhaupt etwas geaendert wurde. Er traegt fuer JEDE der fuenf
148
+ Dateien den Stand, der zum jetzt ausgelieferten Inhalt passt (Β§7b), auch
149
+ fuer die, die "schon aktuell" waren oder lokal geaendert blieben -- ein
150
+ lokal geaenderter Eintrag bleibt so beim naechsten Lauf ERKENNBAR lokal
151
+ geaendert, statt durch einen fehlenden Merkzettel-Eintrag neu "sicher" zu
152
+ werden.
153
+
154
+ πŸ”΄ Task 10a: `firma_id` kommt ausdruecklich aus `daten.firma.id` -- der
155
+ Firma, die diese Antwort tatsaechlich beantwortet hat (`waehleFirma` in
156
+ der Route), nicht aus dem Merkzettel-Feld selbst. Wer zu MEHREREN Firmen
157
+ gehoert, soll im Ordner von Firma A niemals lesen "B hat sich geaendert".
158
+ */
159
+ const merkzettelZumSchreiben = { firma_id: daten.firma.id, dateien: daten.merkzettel.dateien };
160
+ const merkzettelSchreiben = schreibeMerkzettelDatei(ordner, merkzettelZumSchreiben);
161
+ if (!merkzettelSchreiben.ok) {
162
+ console.error(merkzettelSchreiben.meldung);
163
+ return 1;
164
+ }
165
+ // ---- Ausgabe --------------------------------------------------------
166
+ // πŸ”΄ Ruling T8-2 (Fix-Runde 1): ZWEI ehrlich verschiedene Faelle, keine
167
+ // Pauschalmeldung mehr. "Frisch gesichert" und "zusaetzlich neben einer
168
+ // schon vorhandenen Sicherung von heute" sind beide echte Erfolge -- aber
169
+ // verschiedene, und der Mensch muss wissen, WAS mit seinem Text passiert
170
+ // ist. Nie ein Satz, der eine Sicherung behauptet, wo keine (mehr)
171
+ // entstanden ist (der urspruengliche Befund).
172
+ //
173
+ // US-7 bleibt erfuellt: Die Meldung steht VOR der "aktualisiert"-Zeile
174
+ // (vor dem Ergebnis) und nennt den Ordner. Die eigentliche Reihenfolge
175
+ // (erst sichern, dann ueberschreiben) erzwingt schon der Code oben.
176
+ if (gesicherte.length > 0) {
177
+ console.log(`Ich sichere deine lokale Fassung, bevor ich sie ersetze -- Ordner: ${sicherungsOrdner(heute)}`);
178
+ for (const g of gesicherte) {
179
+ if (g.nebenVorhandener) {
180
+ const bisherige = sicherungsZiel(heute, g.pfad).split("/").pop();
181
+ const neue = g.ziel.split("/").pop();
182
+ console.log(` β€ž${g.pfad}": Von heute gibt es schon eine Sicherung (${bisherige}) -- die bleibt ` +
183
+ `unveraendert stehen. Deine neuere Fassung sichere ich zusaetzlich als ${neue}, ` +
184
+ "bevor ich die Datei im Ordner ersetze.");
185
+ }
186
+ else {
187
+ console.log(` β€ž${g.pfad}" -> ${g.ziel.split("/").pop()}`);
188
+ }
189
+ }
190
+ }
191
+ if (geschrieben.length > 0) {
192
+ console.log(`βœ“ aktualisiert: ${geschrieben.join(", ")}`);
193
+ }
194
+ // Β§8: Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall --
195
+ // "nichts geschrieben" heisst hier DREI verschiedene Dinge, und die Ausgabe
196
+ // muss sie unterscheiden, nicht nur die Rueckgabe (alle drei geben 0).
197
+ if (unberuehrt.length > 0) {
198
+ console.log(`Ich fasse nichts an -- du hast lokal geaendert: ${unberuehrt.join(", ")}`);
199
+ // πŸ”΄ Der Rat haengt an der DATEI, nicht nur an der Rolle -- s.
200
+ // `ratNachUnberuehrt` (kontext-stand.ts), Review-Befund F-3.
201
+ for (const zeile of ratNachUnberuehrt(unberuehrt, daten.rolle, daten.adminName)) {
202
+ console.log(zeile);
203
+ }
204
+ }
205
+ if (geschrieben.length === 0 && unberuehrt.length === 0) {
206
+ const alleLeer = VERWALTETE_DATEIEN.every((p) => (daten.merkzettel.dateien[p]?.stand ?? LEER) === LEER);
207
+ if (alleLeer) {
208
+ console.log("Deine Firma hat noch nichts hinterlegt. Bis dahin bleiben die Vordrucke stehen.");
209
+ }
210
+ else {
211
+ console.log("Dein Firmenkontext ist aktuell.");
212
+ }
213
+ }
214
+ return 0;
215
+ }
216
+ /** Prueft die Antwort-Form, ohne `any` -- eine falsche Form ist unlesbar, keine Ausnahme. */
217
+ function alsKontextAntwort(koerper) {
218
+ if (!koerper || typeof koerper !== "object")
219
+ return null;
220
+ const k = koerper;
221
+ // πŸ”΄ Task 10a: `firma.id` ist die Quelle, aus der `kontextRunter` gleich
222
+ // den lokalen Merkzettel stempelt -- s. Kommentar an der Schreibstelle.
223
+ const firmaRoh = k.firma;
224
+ if (!firmaRoh || typeof firmaRoh !== "object")
225
+ return null;
226
+ const firmaId = firmaRoh.id;
227
+ if (typeof firmaId !== "string" || firmaId.trim() === "")
228
+ return null;
229
+ const rolle = k.rolle;
230
+ if (rolle !== "admin" && rolle !== "member")
231
+ return null;
232
+ const adminName = k.admin_name;
233
+ if (adminName !== null && typeof adminName !== "string")
234
+ return null;
235
+ const dateienRoh = k.dateien;
236
+ if (!Array.isArray(dateienRoh))
237
+ return null;
238
+ const dateien = [];
239
+ for (const eintrag of dateienRoh) {
240
+ if (!eintrag || typeof eintrag !== "object")
241
+ return null;
242
+ const p = eintrag.pfad;
243
+ const i = eintrag.inhalt;
244
+ if (typeof p !== "string" || p.trim() === "")
245
+ return null;
246
+ if (typeof i !== "string")
247
+ return null;
248
+ dateien.push({ pfad: p, inhalt: i });
249
+ }
250
+ const merkzettel = alsMerkzettel(k.merkzettel);
251
+ if (merkzettel === null)
252
+ return null;
253
+ return { firma: { id: firmaId }, rolle, adminName, dateien, merkzettel };
254
+ }
@@ -1,7 +1,9 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { join } from "node:path";
5
+ import { STEMPEL_MARKE } from "../nachricht.js";
6
+ import { eigeneVersion } from "../version.js";
5
7
  /**
6
8
  * Namen, die frueher einmal ausgeliefert wurden und heute nicht mehr.
7
9
  *
@@ -25,6 +27,62 @@ const VERALTETE_BEFEHLE = [];
25
27
  * wird sie NICHT ueberschrieben, sondern uebersprungen und gemeldet.
26
28
  */
27
29
  const MARKE = "kimal3:erzeugt";
30
+ /**
31
+ * Wohin das Skill installiert wird -- an EINER Stelle.
32
+ *
33
+ * πŸ”‘ WARUM EXPORTIERT (Befund der vierten Review-Runde): `kimal3.ts` baute
34
+ * denselben Pfad ein zweites Mal zusammen, um dort den Stempel zu lesen. Zoege
35
+ * das Ziel je um, faende `existsSync` dort nichts, `skillZustand` antwortete
36
+ * "still" -- und Weg 2 waere lautlos tot. Kein Fehler, keine Meldung, nichts
37
+ * zum Bemerken.
38
+ *
39
+ * Dieselbe Lehre wie bei `version.ts`, nur eine Stunde spaeter: *Zwei
40
+ * Wahrheiten ueber denselben Ort driften, und die zweite verrottet, ohne
41
+ * dass jemand hinsieht.*
42
+ */
43
+ export function skillOrdner() {
44
+ return join(homedir(), ".claude", "skills", "kimal3");
45
+ }
46
+ export function skillDatei() {
47
+ return join(skillOrdner(), "SKILL.md");
48
+ }
49
+ /**
50
+ * Haengt die eigene Versionsnummer ans ENDE des Skill-Textes.
51
+ *
52
+ * πŸ”΄ DER STEMPEL BESCHREIBT DIE SKILL.md -- NICHT DEN GANZEN LAUF.
53
+ * Diese Zuordnung hat drei Review-Runden gekostet und wurde zweimal falsch
54
+ * entschieden; sie steht hier ausfuehrlich, damit es kein viertes Mal gibt:
55
+ *
56
+ * Runde 2 verschob den Stempel ans Ende des Befehls, damit ein halber Lauf
57
+ * nicht "alles aktuell" behauptet. Runde 3 fand eine Luecke darin, Runde 4
58
+ * flickte den Rueckgabewert -- und Runde 5 zeigte, dass die ganze Richtung
59
+ * falsch war: Bei JEDEM stabilen Fehlschlag danach (fremde k3*.md im
60
+ * Ordner, schreibgeschuetzter Befehlsordner) ist die SKILL.md geschrieben,
61
+ * aber ungestempelt. `skillZustand` antwortet dann "veraltet", die CLI
62
+ * meldet TAEGLICH "deine Skilldateien sind aelter, tippe kimal3 skill
63
+ * install" -- und genau dieser Befehl stellt denselben Zustand wieder her.
64
+ *
65
+ * *`skillZustand` liest die SKILL.md. Also muss der Stempel ueber die SKILL.md
66
+ * sprechen und ueber nichts sonst.* Ist sie geschrieben, ist sie aktuell --
67
+ * unabhaengig davon, was mit den Slash-Befehlen passiert ist. Deren Zustand
68
+ * meldet der Befehl mit Text und Rueckgabewert, sofort und einmalig; das ist
69
+ * der richtige Kanal dafuer, nicht ein taeglicher Hinweis, den niemand
70
+ * abstellen kann.
71
+ *
72
+ * πŸ”‘ Und er entsteht in DERSELBEN Schreiboperation wie die Datei. Kein zweites
73
+ * Lesen, kein zweites Schreiben, kein Zustand dazwischen, in dem die Datei
74
+ * ohne ihren Stempel dasteht.
75
+ *
76
+ * ⚠️ Ohne eigene Nummer GAR KEIN Stempel. Ein erfundener Ersatzwert waere
77
+ * schlimmer als keiner: Die CLI vergliche spaeter gegen ihre eigene Erfindung.
78
+ * Ohne Stempel antwortet `skillZustand` "veraltet" -- die harmlose Seite.
79
+ */
80
+ function mitStempel(inhalt) {
81
+ const eigene = eigeneVersion();
82
+ if (eigene === null)
83
+ return inhalt;
84
+ return `${inhalt.trimEnd()}\n\n<!-- ${STEMPEL_MARKE}${eigene} -->\n`;
85
+ }
28
86
  /**
29
87
  * Legt das Skill UND die Slash-Befehle GLOBAL ab, damit Claude dieses Werkzeug
30
88
  * ueberall kennt -- und damit der Mensch es auch selbst ausloesen kann.
@@ -63,14 +121,46 @@ export async function skillInstall() {
63
121
  // Erfolgsprofil ein" gilt in JEDEM Ordner, nicht in einem. Wer projektlokal
64
122
  // installiert, hat das Werkzeug im naechsten Projekt nicht -- und merkt es
65
123
  // erst, wenn er dort vergeblich "fertig" sagt.
66
- const zielOrdner = join(homedir(), ".claude", "skills", "kimal3");
67
- const ziel = join(zielOrdner, "SKILL.md");
124
+ const zielOrdner = skillOrdner();
125
+ const ziel = skillDatei();
68
126
  // Vorher merken: "angelegt" und "aktualisiert" sind zwei verschiedene
69
127
  // Wirklichkeiten und bekommen deshalb zwei verschiedene Meldungen.
70
128
  const gabEsSchon = existsSync(ziel);
71
129
  try {
72
130
  mkdirSync(zielOrdner, { recursive: true });
73
- copyFileSync(quelle, ziel);
131
+ /*
132
+ πŸ”΄ DER STEMPEL GEHOERT ANS ENDE, NIEMALS AN DEN ANFANG. SKILL.md beginnt
133
+ in Zeile 1 mit YAML-Frontmatter (`---`, dann `name:`). Steht davor
134
+ irgendetwas, ist es kein Frontmatter mehr -- der Skill wird dann gar
135
+ nicht geladen, und zwar OHNE Fehlermeldung. Ein Update-Hinweis, der den
136
+ Skill abschaltet, den er aktuell halten soll, ist der teuerste denkbare
137
+ Fehler in dieser Scheibe.
138
+
139
+ Deshalb auch lesen-anhaengen-schreiben statt copyFileSync: Der Stempel
140
+ entsteht beim KOPIEREN, er steht nicht in der Quelle -- sonst truege das
141
+ Paket seine Versionsnummer an zwei Orten, und die zweite verrottet.
142
+
143
+ πŸ”΄ OHNE eigene Versionsnummer wird GAR KEIN Stempel geschrieben, sondern
144
+ unveraendert kopiert. `eigeneVersion()` liefert `null`, wenn
145
+ `cli/package.json` fehlt oder Unsinn enthaelt. Ein Stempel mit einem
146
+ erfundenen Ersatzwert waere hier schlimmer als keiner: Die CLI vergliche
147
+ spaeter gegen ihre eigene Erfindung und meldete entweder ein Update, das
148
+ es nicht gibt, oder schwiege zu einem, das faellig ist. Ohne Stempel
149
+ antwortet `skillZustand` dagegen "veraltet" -- die harmlose Seite, denn
150
+ ein zweites `kimal3 skill install` kostet nichts.
151
+
152
+ ⚠️ Die Marke `kimal3:erzeugt` (Konstante MARKE oben) steckt in
153
+ `STEMPEL_MARKE` und bleibt damit erhalten. Der Stempel ist eine
154
+ ERWEITERUNG dieser Marke, kein Ersatz -- `raeumeVeralteteAuf` und die
155
+ Ueberschreib-Pruefung erkennen unsere eigenen Dateien genau daran.
156
+
157
+ πŸ“Œ Er entsteht HIER, in derselben Schreiboperation wie die Datei -- und
158
+ zwar wieder, nachdem er drei Runden lang am Ende des Befehls stand.
159
+ Warum die Ruecknahme richtig ist, steht ausfuehrlich bei `mitStempel`:
160
+ Der Stempel spricht ueber die SKILL.md, nicht ueber den ganzen Lauf.
161
+ */
162
+ const inhalt = readFileSync(quelle, "utf8");
163
+ writeFileSync(ziel, mitStempel(inhalt), "utf8");
74
164
  }
75
165
  catch (fehler) {
76
166
  console.error(`Das Skill liess sich nicht schreiben: ${String(fehler)}`);
@@ -142,6 +232,22 @@ export async function skillInstall() {
142
232
  console.error(" Die oben genannten stehen; ein zweiter Lauf holt den Rest.");
143
233
  return 3;
144
234
  }
235
+ /*
236
+ πŸ”΄ Eine nicht lesbare Datei ist ein UNVOLLSTAENDIGER Lauf und bekommt
237
+ deshalb `3`, genau wie `gestoert` (Befund der vierten Review-Runde).
238
+ `3` heisst laut SKILL.md "einmal wiederholen", und das passt zur Lage:
239
+ Eine Datei in Benutzung ist gleich wieder frei.
240
+
241
+ πŸ“Œ Der STEMPEL haengt hier nicht mehr dran -- er entsteht beim Schreiben
242
+ der SKILL.md und beschreibt nur sie (ausfuehrliche Begruendung bei
243
+ `mitStempel` oben). Bis zur fuenften Review-Runde stand er an dieser
244
+ Stelle, und jeder stabile Fehlschlag danach erzeugte einen taeglichen
245
+ Hinweis, den kein Befolgen aufloesen konnte.
246
+ */
247
+ if (unlesbar.length > 0) {
248
+ console.error(" Der Lauf ist damit unvollstaendig -- ein zweiter Versuch holt den Rest.");
249
+ return 3;
250
+ }
145
251
  /*
146
252
  ⚠️ **Nicht auf einen Befehl verweisen, den wir gerade NICHT installiert
147
253
  haben** (Review-Runde 3, 2026-08-18, gemessen). Mit einer fremden