Files
DogFatherGitandClaude Opus 5 603319a145 Arbeitsschloss: zwei Sitzungen sehen sich jetzt
HEUTE ZWEIMAL NUR GUTGEGANGEN. Zwei Claude-Sitzungen arbeiteten
gleichzeitig in diesem Verzeichnis, ohne voneinander zu wissen.
Keine hat etwas falsch gemacht — sie konnten es nicht wissen.

  * Beide haben `git add -A` benutzt. Haette die eine
    unfestgeschriebene Arbeit der anderen im Baum gehabt, waere sie
    mitcommittet worden — unter fremdem Namen, in einer fremden
    Begruendung, und niemandem waere es aufgefallen. Nachgesehen:
    diesmal war nichts dabei.
  * Beide haben den Stempellauf gestartet. Der schreibt 45 Dateien
    um. Wer dort eine offen hatte, bekam sie unter den Haenden weg
    geaendert.

Dasselbe hat in RunOne am 03.09.2026 sieben Minuten Ausfall
gekostet. Dort gibt es seitdem `arbeitsschloss.sh`; diese Fassung
uebernimmt seine Lehren.

WAS ES IST UND WAS NICHT. Es ist kein Riegel — wer wirklich muss,
kommt vorbei. Es beantwortet die eine Frage, die heute niemand
beantworten konnte: „arbeitet hier gerade sonst jemand?"

Die teuren Fehler liegen bei einem Schloss alle in derselben
Richtung: Es blockiert zu viel und wird deshalb abgeschafft. Also:

  * Es blockiert NICHT, wenn das Schloss DEINES ist. RunOnes erste
    Fassung fragte „ist abgeschlossen" statt „haelt es jemand
    anders" — damit haette, wer ordentlich abschliesst, nie mehr
    ausliefern koennen. Dafuer gibt es `fremd`.
  * Es VERFAELLT nach zwei Stunden, und dass da jemand war, steht
    beim Uebernehmen dabei.
  * Es blockiert NICHT, wenn es selbst unlesbar ist — dritter
    Ausgang, kein Stillstand.
  * Notausgang: SCHLOSS_ZWANG=ja git commit …

DAS PROBLEM, AN DEM RUNONE HAENGT, IST HIER GELOEST. Dort faellt die
Kennung im Zweifel auf den Systembenutzer zurueck, und zwei
Claudian-Sitzungen laufen BEIDE als `claudian` — die Sicherung griff
ausgerechnet zwischen den zwei Faellen nicht, fuer die sie gebaut
wurde. Deshalb ist dort `export ARBEITER=…` Pflicht, und Pflicht
heisst: man vergisst es.

Hier steht `CLAUDE_CODE_SESSION_ID` 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. Reihenfolge: ARBEITER, dann
Sitzungskennung, dann Benutzername MIT Warnung.

NIEMAND MUSS DARAN DENKEN:
  * die beiden Stempelwerkzeuge nehmen es selbst und geben es selbst
    frei — auch nach einem Absturz und bei Strg+C (wie `bauen.sh` im
    Shop; eines, an das man denken muss, wird vergessen und ab da
    umgangen)
  * `tools/git-haken/pre-commit` bricht jeden Commit ab, solange
    jemand ANDERS das Schloss haelt. Das ist die Stelle, die heute
    gefehlt hat: `git add -A` ist der Griff, den man ohne Nachdenken
    macht, und gegen einen Reflex hilft keine Regel auf Papier.
  * `core.hooksPath` statt `.git/hooks` — letzteres ist nicht
    versioniert und waere nach einem Klon genau dann leer, wenn es
    gebraucht wird.

GEPRUEFT, server/pruef-arbeitsschloss.mjs: 33 Pruefungen, 0 Fehler —
in einem WEGWERF-Verzeichnis mit eigenem git, damit kein echtes
Schloss angefasst wird. Darunter am echten git:

    ohne Schloss committen        -> geht      (0)
    mit dem EIGENEN Schloss       -> geht      (0)
    mit einem FREMDEN Schloss     -> bricht ab (1), nennt wer und warum
    und es stehen genau ZWEI Commits da, nicht drei
    SCHLOSS_ZWANG=ja              -> kommt vorbei
    ohne das Schloss-Werkzeug     -> laesst durch

Dazu: verfallenes Schloss laesst durch, mit laengerer Frist blockt
dasselbe Schloss wieder (Gegenprobe), unlesbarer Inhalt und
unlesbarer Zeitstempel blockieren nicht.

Portnummern nach der neuen Pruefdatei nachgemessen: Pruefbereich bis
5415, 462 Nummern, 0 Kollisionen. pruef-struktur 75/0,
pruef-fingermass 5/0, pruef-ports 10/0.

In DEPLOY.md steht es jetzt an erster Stelle — eine Sicherung, von
der nur der weiss, der sie gebaut hat, ist die erste, die umgangen
wird.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 20:04:28 +02:00

372 lines
16 KiB
JavaScript

/* =====================================================================
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=<name> 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);
}