/* ===================================================================== arbeitsschloss.mjs — macht sichtbar, wer gerade in diesem Verzeichnis arbeitet. WARUM ES DAS GIBT (01.10.2026, nach zwei knappen Fällen an einem Tag) An diesem Tag arbeiteten zwei Claude-Sitzungen gleichzeitig in DogiHompage, ohne voneinander zu wissen. Keine hat etwas falsch gemacht — sie konnten es nicht wissen. Zweimal ist es nur gut gegangen: * Beide haben `git add -A` benutzt. Hätte die eine unfestgeschriebene Arbeit der anderen im Baum gehabt, wäre sie mitcommittet worden. Nachgesehen: diesmal war nichts dabei. * Beide haben `tools/workspace-stempel.mjs` laufen lassen. Der schreibt 45 Dateien um. Wer dort gerade eine offen hatte, bekam sie unter den Händen weg geändert. Dasselbe hat in RunOne am 03.09.2026 sieben Minuten Ausfall gekostet, und dort gibt es seitdem `arbeitsschloss.sh`. Diese Fassung übernimmt dessen Lehren und löst zusätzlich das Problem, an dem die dortige am meisten hängt — siehe „WER BIN ICH". --------------------------------------------------------------------- WAS DIESES SCHLOSS IST UND WAS NICHT Es ist kein Riegel. Es kann niemanden aussperren, und das soll es auch nicht — wer wirklich muss, kommt vorbei. Es beantwortet eine einzige Frage, die heute niemand beantworten konnte: „arbeitet hier gerade sonst jemand?" Ein Schloss, das das eigene Arbeiten blockiert, wird abgeschafft. Deshalb drei Eigenschaften, alle drei teuer gelernt: 1. ES VERFÄLLT (nach zwei Stunden). Ein Schloss, das man vergessen kann, blockiert irgendwann dauerhaft — und wird beim ersten Ärger umgangen. Ab da ist es wertlos, obwohl es noch da ist. Ein verfallenes Schloss gilt als frei, und dass da jemand war, wird ausdrücklich gesagt. 2. DIE FRAGE IST „HÄLT ES JEMAND ANDERS", NICHT „IST ABGESCHLOSSEN". RunOnes erste Fassung fragte das Zweite, und das war falsch: Wer während seiner Arbeit ordentlich abschließt — also genau das erwünschte Verhalten — hätte danach nie mehr ausliefern können. Eine Sicherung, die das richtige Verhalten bestraft, wird abgeschafft. Dafür gibt es `fremd`. 3. WER ES NICHT LESEN KANN, WIRD NICHT GEBLOCKT. Ein kaputtes Schloss darf nicht die Arbeit anhalten. Es meldet dann den dritten Ausgang — „konnte nicht nachsehen" — und lässt weiterlaufen. Eine Warnung, die bei jeder Störung die Arbeit stoppt, wird als Erstes entfernt. --------------------------------------------------------------------- WER BIN ICH — UND WARUM DAS DER SCHWIERIGE TEIL IST RunOnes Fassung nimmt im Zweifel den Systembenutzer. Am 04.09.2026 stand dort dazu die bittere Notiz: Zwei Claudian-Sitzungen laufen BEIDE als `claudian`, und ohne gesetztes `ARBEITER` sah das fremde Schloss aus wie das eigene — die Sicherung griff ausgerechnet zwischen den zwei Fällen nicht, für die sie gebaut wurde. Deshalb ist dort `export ARBEITER=…` Pflicht. Pflicht heißt: man vergisst es. Hier gibt es etwas Besseres: `CLAUDE_CODE_SESSION_ID` steht in jeder Sitzung und ist je Sitzung verschieden (nachgesehen, 36 Zeichen UUID). Zwei Sitzungen auf demselben Windows-Benutzer unterscheiden sich damit von selbst, ohne dass jemand etwas tun muss. Die Reihenfolge ist deshalb: 1. `ARBEITER` — wenn jemand ausdrücklich einen Namen will 2. Sitzungskennung — der Normalfall, ohne Zutun 3. Benutzername + WARNUNG — der schlechte Fall, und er sagt es --------------------------------------------------------------------- AUFRUFEN node tools/arbeitsschloss.mjs nehmen ["was ich tue"] node tools/arbeitsschloss.mjs freigeben node tools/arbeitsschloss.mjs ansehen 0 = frei/verfallen, 1 = belegt node tools/arbeitsschloss.mjs fremd 0 = frei oder MEINES, 1 = fremd node tools/arbeitsschloss.mjs einrichten setzt den git-Haken Die Stempelwerkzeuge nehmen es SELBST und geben es selbst wieder frei — wie `bauen.sh` im Shop. Ein Schloss, das man von Hand nehmen muss, wird vergessen; dann wird es umgangen und ist ab da nichts mehr wert. ===================================================================== */ import { readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { hostname, userInfo } from "node:os"; import { execFileSync } from "node:child_process"; const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); const DATEI = join(WURZEL, ".arbeitsschloss"); const HAKEN_ORDNER = "tools/git-haken"; /** Nach wie vielen Minuten ein Schloss als vergessen gilt. */ const VERFALL_MINUTEN = Number(process.env.SCHLOSS_VERFALL || 120); const rot = (t) => `\x1b[31m${t}\x1b[0m`; const gelb = (t) => `\x1b[33m${t}\x1b[0m`; /** Wer bin ich? Siehe „WER BIN ICH" im Kopf. */ export function werIch() { if (process.env.ARBEITER) return { name: process.env.ARBEITER, woher: "ARBEITER" }; const s = process.env.CLAUDE_CODE_SESSION_ID; if (s && s.length >= 8) return { name: `claude-${s.slice(0, 8)}`, woher: "Sitzungskennung" }; return { name: userInfo().username, woher: "Benutzername", schwach: true }; } /** Was steht im Schloss? `null` = frei, `undefined` = konnte nicht nachsehen. */ function lesen() { if (!existsSync(DATEI)) return null; try { const roh = readFileSync(DATEI, "utf8"); const feld = (n) => (roh.match(new RegExp(`^${n}=(.*)$`, "m")) || [])[1]?.trim() || ""; const wer = feld("wer"); if (!wer) return null; const seit = feld("seit"); const ms = Date.parse(seit); /* EIN UNLESBARER ZEITSTEMPEL IST KEIN JUNGES SCHLOSS. Stünde hier 0, wäre das Schloss „gerade erst genommen" und würde ewig blocken. Unlesbar heißt: als verfallen behandeln. */ const alterMin = Number.isNaN(ms) ? 99999 : Math.floor((Date.now() - ms) / 60000); return { wer, seit, was: feld("was"), rechner: feld("rechner"), alterMin }; } catch (f) { console.error(gelb(`[Schloss] Konnte nicht nachsehen: ${f?.message}`)); return undefined; } } function schreiben(wer, was) { writeFileSync(DATEI, [ `wer=${wer}`, `seit=${new Date().toISOString()}`, `was=${was || ""}`, `rechner=${hostname()}`, "", ].join("\n"), "utf8"); } const verfallen = (s) => s.alterMin >= VERFALL_MINUTEN; /* --------------------------------------------------------------------- Der git-Haken Hier liegt das Stück, das heute wirklich gefehlt hat: Beide Sitzungen haben `git add -A` benutzt. Ein Haken ist die einzige Stelle, an der sich das ABFANGEN lässt, ohne dass jemand daran denken muss. `core.hooksPath` statt `.git/hooks`: Letzteres wird nicht versioniert und wäre nach jedem frischen Klon weg — also genau dann nicht da, wenn es gebraucht wird. Der Ordner liegt deshalb im Baum, und `einrichten` zeigt git darauf. --------------------------------------------------------------------- */ function einrichten() { let jetzt = ""; try { jetzt = execFileSync("git", ["config", "--get", "core.hooksPath"], { cwd: WURZEL, encoding: "utf8" }).trim(); } catch { jetzt = ""; } if (jetzt === HAKEN_ORDNER) { console.log(`Der git-Haken ist eingerichtet (core.hooksPath = ${HAKEN_ORDNER}).`); return 0; } if (jetzt) { console.error(rot(`core.hooksPath zeigt auf "${jetzt}", nicht auf "${HAKEN_ORDNER}".`)); console.error(" Das überschreibe ich nicht von mir aus — dort hängt vielleicht"); console.error(" etwas anderes dran. Von Hand:"); console.error(` git config core.hooksPath ${HAKEN_ORDNER}`); return 1; } execFileSync("git", ["config", "core.hooksPath", HAKEN_ORDNER], { cwd: WURZEL }); console.log(`Eingerichtet: core.hooksPath = ${HAKEN_ORDNER}`); return 0; } /* --------------------------------------------------------------------- Für andere Werkzeuge: selbst nehmen, selbst freigeben --------------------------------------------------------------------- */ /** Nimmt das Schloss für die Dauer von `tun()` und gibt es danach frei. * * GIBT AUCH BEI EINEM FEHLER FREI. Ein Schloss, das nach einem * Absturz liegen bleibt, ist der Anfang vom Umgehen — es verfällt * zwar nach zwei Stunden, aber zwei Stunden sind lang. * * UND ES GIBT NICHT FREI, WAS IHM NICHT GEHÖRT: Hielt ich das * Schloss schon vorher (weil jemand es von Hand genommen hat), * bleibt es danach stehen. Sonst nähme ein Stempellauf dem * Menschen sein Schloss weg. */ export async function mitSchloss(was, tun) { const ich = werIch(); const vorher = lesen(); const schonMeins = vorher && vorher.wer === ich.name && !verfallen(vorher); const lage = fremdLage(ich); if (lage.fremd) { console.error(rot("\nABBRUCH: In diesem Verzeichnis arbeitet gerade jemand anders.")); console.error(` Wer: ${lage.wer}`); console.error(` Seit: vor ${lage.alterMin} Minuten`); if (lage.was) console.error(` Was: ${lage.was}`); console.error(""); console.error(` „${was}" schreibt Dateien um, die dort offen sein könnten.`); console.error(" Genau daraus wurde am 01.10.2026 zweimal fast ein Schaden."); console.error(" Erst abstimmen. Wenn das Schloss liegen geblieben ist:"); console.error(" node tools/arbeitsschloss.mjs ansehen"); process.exit(9); } if (!schonMeins) schreiben(ich.name, was); try { return await tun(); } finally { if (!schonMeins) { try { unlinkSync(DATEI); } catch { /* war schon weg -- in Ordnung */ } } } } /** Dasselbe wie `mitSchloss`, aber in EINER Zeile am Kopf eines * Werkzeugs — ohne dass es seinen Inhalt in eine Funktion packen * muss. * * WARUM ES DAS ZUSAETZLICH GIBT: Die Stempelwerkzeuge sind von oben * nach unten geschrieben. Sie für `mitSchloss` umzubauen wäre mehr * Risiko als Nutzen — und ein Schloss, dessen Einbau riskant ist, * wird nicht eingebaut. * * Gibt beim Programmende frei, auch nach einem Absturz: Ein Schloss, * das liegen bleibt, ist der Anfang vom Umgehen. */ export function schlossKurz(was) { const ich = werIch(); const lage = fremdLage(ich); if (lage.fremd) { console.error(rot("\nABBRUCH: In diesem Verzeichnis arbeitet gerade jemand anders.")); console.error(` Wer: ${lage.wer}`); console.error(` Seit: vor ${lage.alterMin} Minuten`); if (lage.was) console.error(` Was: ${lage.was}`); console.error(""); console.error(` „${was}" schreibt viele Dateien um — auch solche, die dort`); console.error(" gerade offen sein könnten. Am 01.10.2026 ist genau das"); console.error(" zweimal nur gutgegangen."); console.error(""); console.error(" Erst abstimmen. Nachsehen: node tools/arbeitsschloss.mjs ansehen"); process.exit(9); } if (lage.eigenes) return; /* nicht wegnehmen, was schon meins ist */ schreiben(ich.name, was); let weg = false; const loesen = () => { if (weg) return; weg = true; try { unlinkSync(DATEI); } catch { /* war schon weg */ } }; process.on("exit", loesen); /* Auch bei Strg+C -- sonst bleibt es nach jedem Abbruch liegen. */ for (const art of ["SIGINT", "SIGTERM"]) { process.on(art, () => { loesen(); process.exit(130); }); } } /** Hält das Schloss JEMAND ANDERS? Siehe Punkt 2 im Kopf. */ export function fremdLage(ich = werIch()) { const s = lesen(); if (s === undefined) return { unklar: true, fremd: false }; if (s === null) return { frei: true, fremd: false }; if (verfallen(s)) return { verfallen: true, fremd: false, ...s }; if (s.wer === ich.name) return { eigenes: true, fremd: false, ...s }; return { fremd: true, ...s }; } /* --------------------------------------------------------------------- Aufruf von der Befehlszeile --------------------------------------------------------------------- */ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { const befehl = process.argv[2] || "ansehen"; const ich = werIch(); if (ich.schwach && befehl !== "ansehen") { console.error(gelb( `[Schloss] Kennung ist nur der Benutzername ("${ich.name}").`)); console.error(gelb( " Zwei Sitzungen auf demselben Benutzer sehen damit gleich aus — genau")); console.error(gelb( " dafür hat RunOne am 04.09.2026 bezahlt. Besser: ARBEITER= setzen.")); } if (befehl === "nehmen") { const grund = process.argv[3] || ""; const s = lesen(); if (s === undefined) { console.error(gelb("[Schloss] Nicht lesbar — ich nehme es trotzdem und sage es.")); schreiben(ich.name, grund); process.exit(0); } if (s && s.wer === ich.name && !verfallen(s)) { schreiben(ich.name, grund); console.log(`Schloss war schon deins ("${ich.name}"), Zeit erneuert.`); process.exit(0); } if (s && verfallen(s)) { console.log(gelb(`Achtung: "${s.wer}" hatte das Schloss seit ${s.alterMin} Minuten`)); console.log(gelb("und hat es nicht freigegeben. Es gilt als verfallen.")); if (s.was) console.log(` Was dort lief: ${s.was}`); schreiben(ich.name, grund); process.exit(0); } if (s) { console.error(rot("HIER ARBEITET GERADE JEMAND ANDERS.")); console.error(` Wer: ${s.wer}`); console.error(` Seit: ${s.seit} (vor ${s.alterMin} Minuten)`); if (s.was) console.error(` Was: ${s.was}`); if (s.rechner) console.error(` Rechner: ${s.rechner}`); console.error(""); console.error(" Nicht einfach weitermachen. Erst abstimmen — bei zwei"); console.error(" Claude-Sitzungen geht das mit SendMessage."); process.exit(1); } schreiben(ich.name, grund); console.log(`Schloss genommen von "${ich.name}" (${ich.woher})` + `${grund ? ` — ${grund}` : ""}.`); process.exit(0); } if (befehl === "freigeben") { const s = lesen(); if (!s) { console.log("Es war gar nicht abgeschlossen."); process.exit(0); } if (s.wer !== ich.name && process.env.SCHLOSS_ZWANG !== "ja") { console.error(rot(`Das Schloss gehört "${s.wer}", nicht "${ich.name}".`)); console.error(" Wenn du sicher bist: SCHLOSS_ZWANG=ja node tools/arbeitsschloss.mjs freigeben"); process.exit(1); } unlinkSync(DATEI); console.log("Schloss freigegeben."); process.exit(0); } if (befehl === "ansehen") { const s = lesen(); if (s === undefined) { console.log("konnte nicht nachsehen"); process.exit(0); } if (s === null) { console.log("frei"); process.exit(0); } if (verfallen(s)) { console.log(`verfallen: "${s.wer}" seit ${s.alterMin} Minuten` + `${s.was ? ` (${s.was})` : ""}`); process.exit(0); } console.log(`belegt: "${s.wer}" seit ${s.alterMin} Minuten${s.was ? ` (${s.was})` : ""}`); process.exit(1); } if (befehl === "fremd") { const l = fremdLage(ich); if (l.unklar) { console.log("konnte nicht nachsehen"); process.exit(0); } if (l.frei) { console.log("frei"); process.exit(0); } if (l.verfallen) { console.log(`verfallen: "${l.wer}" seit ${l.alterMin} Minuten`); process.exit(0); } if (l.eigenes) { console.log(`dein eigenes Schloss (seit ${l.alterMin} Minuten)`); process.exit(0); } console.log(`belegt von "${l.wer}" seit ${l.alterMin} Minuten${l.was ? ` (${l.was})` : ""}`); process.exit(1); } if (befehl === "einrichten") process.exit(einrichten()); console.error("Aufruf: node tools/arbeitsschloss.mjs" + " {nehmen|freigeben|ansehen|fremd|einrichten} [\"was ich tue\"]"); process.exit(2); }