-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlib.sh
More file actions
executable file
·2388 lines (2266 loc) · 120 KB
/
Copy pathlib.sh
File metadata and controls
executable file
·2388 lines (2266 loc) · 120 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# Bahn: bash | Gegenstueck: lib.psm1
# team/lib.sh — gemeinsame Bibliothek der T.E.A.M.-Rollen (siehe CLAUDE.md, Anhang A).
# Wird per `source` eingebunden, nicht direkt ausgeführt.
# --- Projekt-Konfiguration (T.E.A.M.-Starterkit) ------------------------------
# Alle projektspezifischen Werte stehen in team.config.sh. Sie wird ZUERST
# gesourct, damit die ${VAR:-default}-Zuweisungen unten sie stehen lassen.
# Fehlt die Datei, laufen die Rollen mit den Defaults dieser Bibliothek weiter —
# so bricht ein Lauf nie an einer fehlenden Konfigdatei ab (gleiche Logik wie
# der Pflicht-Fallback in team_briefing).
# lib.sh liegt in team/, team.config.sh eine Ebene darüber in der Repo-Wurzel.
if [ -f "$(dirname "${BASH_SOURCE[0]}")/../team.config.sh" ]; then
# shellcheck source=../team.config.sh disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/../team.config.sh"
else
echo "[team-lib] WARNUNG: team.config.sh fehlt — Bibliotheks-Defaults aktiv." >&2
fi
# --- Ein Platzhalter ist kein Befehl (BL-149) ---------------------------------
# Jede Weiche unterhalb unterscheidet "konfiguriert" von "nicht konfiguriert"
# ueber leer/nicht-leer. Das ist richtig — nur ist "noch nicht ausgefuellt" im
# Kit jahrelang als TODO-SATZ geschrieben worden, und der ist nicht leer.
#
# Im Feld traf das jede erste Kaskade: Der Satz "TODO: noch keiner — Stufe 1
# der ersten Kaskade" stand als Vorbelegung in team.config.sh, galt damit als
# Befehl, landete im Prompt jeder bauenden Rolle ("Smoke-Test ausfuehren:
# TODO: …"), in der Werkzeug-Allowlist des Red Teams (Bash(TODO …)) und wurde
# von team_quittung_selbstpruefung WOERTLICH ausgefuehrt — Exit 127, Meldung
# "ist ROT". Der vierte Ausgang aus BL-41 konnte in Stufe 1 damit nie
# automatisch quittieren, obwohl genau diese Stufe die Aufgabe hat, den
# Smoke-Test ueberhaupt erst zu bauen.
#
# Die Vorbelegung ist weg (der Installer fuellt team.config.* jetzt LEER). Diese
# Weiche steht trotzdem hier, und zwar aus zwei Gruenden: Ein Mensch traegt in
# eine leere Zeile gern selbst ein "TODO" ein, und ein Platzhalter dieser Sorte
# wird im Kit erfahrungsgemaess an anderer Stelle wieder eingefuehrt. Sie
# normalisiert EINMAL, oben, statt an drei Verbrauchsstellen einzeln.
#
# Bewusst nur der Praefix und bewusst gross geschrieben: "TODO" am Anfang ist
# im ganzen Kit die Marke fuer "noch nicht ausgefuellt". Ein echter Pruefbefehl
# faengt nicht so an; ein Skript namens ./todo.sh bleibt unangetastet.
case "${TEAM_SMOKE_TEST:-}" in
TODO*) TEAM_SMOKE_TEST="" ;;
esac
# --- Wie lange der Verifikationsbefehl im Vordergrund laufen darf (BL-207) ----
# Die Auflage "im Vordergrund, nie als Hintergrund-Task" (unten) ist richtig,
# war aber NICHT ERFUELLBAR, sobald die Suite laenger lief als die
# Vordergrundgrenze des Agenten-Werkzeugs (120 s Default). Feld B, 2026-08-28:
# Suite 149-220 s, in EINEM Lauf dreimal in den vierten Ausgang gelaufen
# (Ralph einmal, Frank zweimal), zusammen 4,9480 USD = 32 % der Rollenkosten.
# Die Rolle stand vor der Wahl zwischen Regelverletzung und Werkzeug-Timeout
# und waehlte in drei von drei Faellen dieselbe Verletzung.
#
# BL-201 hatte dieselbe Bauform zweimal mit einer SCHAERFEREN Auflage
# beantwortet. Das hier ist der Beleg, dass Schaerfe nicht hilft: Es ist kein
# Disziplinproblem. Eine Auflage, die die Rolle nicht einhalten KANN, erzeugt
# genau das Verhalten, das sie verbieten soll. Deshalb bekommt die Auflage eine
# Zahl, die die Rolle im Werkzeug einstellen kann — und die Zahl steht im
# Prompt, nicht nur in der Konfiguration.
#
# Default 600 s: grosszuegig gegenueber der 120-s-Wand des Werkzeugs und immer
# noch eine Grenze. Wer laenger braucht, traegt es in team.config.sh ein.
TEAM_SMOKE_TEST_TIMEOUT="${TEAM_SMOKE_TEST_TIMEOUT:-600}"
# --- Der Suitenstand ueberlebt die Rolle, die ihn gemessen hat (BL-256) -------
# WARUM ES DIESE DATEI GIBT. Im Feld aktivierte ein KORREKTER Frank-Fix einen
# latenten Defekt in einem aelteren Waechter. Frank trug den Beifang als
# eigenen Fund ein und belegte regelkonform, dass SEIN Fix keinen NEUEN
# Fehlschlag erzeugt (BL-205). Der Fund bekam Status `offen`; die Fixphase
# fragt nach `an Frank uebergeben` und meldete folgerichtig *nichts zu tun*.
# Der Abschlussbericht meldete den Lauf als fertig, waehrend der Baum seit
# einer Stunde rot war.
#
# JEDE ROLLE HAT SICH REGELKONFORM VERHALTEN. Strukturell fehlte ein ORT: Jede
# Rolle misst den Suitenstand einzeln und keine gibt ihn weiter - und was
# nirgends steht, kann keine Zusammenfassung lesen. Die Regel *ein Fix
# scheitert nicht an fremdem Flackern* bleibt unangetastet; was hier entsteht,
# ist ihre GEGENRICHTUNG: Der Lauf darf weiterlaufen, aber er darf sich nicht
# als fertig melden.
#
# WARUM DIE FUND-EBENE NICHT REICHT: Ein rotes Gate kann ohne Fundeintrag
# entstehen, und ein offener Fund heisst umgekehrt nicht, dass die Suite rot
# ist. Der Suitenstand gehoert an den Suitenstand gebunden.
#
# STEHT HIER und nicht bei den Funktionen weiter unten: Der Prompt-Baustein
# gleich darunter NENNT die Datei. Auf der bash-Bahn faellt eine spaetere
# Definition unter `set -u` sofort auf; auf der pwsh-Bahn waere sie still zu
# einem Leerstring geworden - eine Auflage ohne Ziel.
TEAM_GATE_DATEI="${TEAM_GATE_DATEI:-.team-gate-rot}"
# --- Abgeleitete Prompt-Bausteine (Starterkit) --------------------------------
# Smoke-Test-Zeile für die bauenden Rollen. Ist kein Befehl konfiguriert, wird
# der Schritt AUSDRÜCKLICH als offener Punkt benannt, statt still zu
# verschwinden — sonst merkt niemand, dass das Sicherheitsnetz fehlt.
if [ -n "${TEAM_SMOKE_TEST:-}" ]; then
# Der Nachsatz ist eine Notbremse gegen einen teuren Fehlermodus, nicht
# Ausschmückung (BL-41, Feld A K27/K28): Eine bauende Rolle
# startete den Smoke-Test als HINTERGRUND-Task und wartete danach auf eine
# Benachrichtigung, die in einer headless-Sitzung nie eintrifft. Der Lauf
# endet mit subtype=success und is_error=false — er SIEHT AUS WIE EIN
# ERFOLG —, gibt aber kein Promise und committet nicht. Dreimal passiert,
# zusammen 13,25 USD, jedes Mal für Arbeit, die bereits fertig und grün
# war. Der Satz steht hier statt in den Rollen-Briefings, weil er hier
# JEDE bauende Rolle trifft statt nur eine. Die Begründung gehört in den
# Prompt, damit die Regel nicht als willkürlich gelesen wird.
SMOKE_ZEILE="Smoke-Test ausführen: ${TEAM_SMOKE_TEST} — muss grün sein.
Führe ihn im VORDERGRUND aus und warte auf seine Ausgabe. Starte ihn
NIEMALS als Hintergrund-Task und plane keinen Wakeup darauf: Diese Sitzung
ist headless, es kommt keine Benachrichtigung, und du wartest bis zum
Zeitlimit auf ein Ereignis, das nicht eintreten kann.
Er darf dafür bis zu ${TEAM_SMOKE_TEST_TIMEOUT} Sekunden brauchen: Erhöhe das
Zeitlimit deines Werkzeugs entsprechend, statt in den Hintergrund
auszuweichen — viele Werkzeuge erwarten MILLISEKUNDEN, das wären
${TEAM_SMOKE_TEST_TIMEOUT}000 (BL-258). Läuft er länger, ist das ein Befund
für den Menschen — melde ihn, weiche nicht aus.
War der Baum schon VOR deiner Arbeit rot, hänge eine Zeile
'<ISO-Zeit> | <deine Rolle> | <Namen der roten Tests>' an
${TEAM_GATE_DATEI} an (BL-256). Dein Auftrag scheitert daran NICHT — aber
ohne diese Zeile meldet sich der Lauf am Ende als fertig, während das Gate
aus ist. Ist der Baum am Ende grün, lösche die Datei wieder."
# BL-207: Frank bekommt NUR diesen Nachsatz, nicht SMOKE_ZEILE — und er
# faehrt den Smoke-Test oefter als Ralph. Im Feld endeten 10 von 28
# Frank-Laeufen ohne Promise, bei 9 davon stand das Warten auf einen
# Hintergrundlauf woertlich im Log-Feld `result` (10,7249 USD an EINEM
# Tag). Bei ihm kostet der vierte Ausgang zusaetzlich einen
# Fehlversuch (.frank-attempts) und eskaliert ab dem dritten an Axel —
# das teure Modell wird also fuer einen Formfehler gerufen. Deshalb
# steht die Auflage hier ausgeschrieben statt nur bei Ralph.
SMOKE_SUFFIX=" Smoke-Test grün: ${TEAM_SMOKE_TEST}. Führe ihn im VORDERGRUND aus und warte auf seine Ausgabe — er darf bis zu ${TEAM_SMOKE_TEST_TIMEOUT} Sekunden brauchen, erhöhe das Zeitlimit deines Werkzeugs entsprechend (viele Werkzeuge erwarten MILLISEKUNDEN — das wären ${TEAM_SMOKE_TEST_TIMEOUT}000, BL-258). NIEMALS als Hintergrund-Task und kein Wakeup darauf: Diese Sitzung ist headless, es kommt keine Benachrichtigung, und der Lauf endet als Erfolg ohne Quittung (BL-41). War die Suite schon VOR deinem Fix rot, brich nicht ab: Miss beide Staende und belege, dass durch DEINEN Fix kein NEUER Fehlschlag entsteht (BL-205) — und haenge die Zeile '<ISO-Zeit> | frank | <Namen der roten Tests>' an ${TEAM_GATE_DATEI} an, sonst meldet sich der Lauf am Ende als fertig, waehrend das Gate aus ist (BL-256). Ist der Baum am Ende gruen, loesche die Datei wieder."
else
SMOKE_ZEILE="(Kein Smoke-Test konfiguriert — Schritt entfällt. Das Team arbeitet ohne Sicherheitsnetz; TEAM_SMOKE_TEST in team.config.sh nachtragen.)"
SMOKE_SUFFIX=""
fi
# --- Bedienung: Hilfe und Zurueckweisung (BL-223) -----------------------------
# Warum das in der Bibliothek steht und nicht siebenmal daneben: Eine
# Konvention, die nur dasteht, wird an der Haelfte der Stellen nicht befolgt —
# und genau so ist BL-223 entstanden. Von vierzehn Einstiegspunkten hatten
# DREI eine Hilfe, und die Rollen-Skripte lasen ihre Argumente ueberhaupt
# nicht: `./ralph.sh --hilfe` zeigte keine Hilfe, wies nichts zurueck und
# startete einen bezahlten Rollenlauf. Das ist BL-222 eine Stufe teurer — dort
# war die Folge eine falsche Statusausgabe, hier ein Modellaufruf, den niemand
# bestellt hat.
# team_hilfe_kopf [datei]: der DATEIKOPF ist der Hilfetext, keine zweite
# Fassung daneben (dieselbe Bauart wie `install.sh --hilfe`, BL-154/BL-156) —
# eine Abschrift laeuft irgendwann auseinander, und dann sagt --hilfe etwas
# anderes als die Datei. Gelesen wird ab Zeile 3 (1 = Shebang, 2 =
# Bahn-Kopfzeile, beides Maschinensache) bis zur ersten Zeile, die kein
# Kommentar mehr ist. Waechst der Kopf, waechst die Hilfe mit.
#
# Default ist der BASENAME: Jeder Entrypoint wechselt vorher in sein eigenes
# Verzeichnis, ein relativer Pfad zeigt danach ins Leere.
team_hilfe_kopf() {
local datei="${1:-$0}"
[ -f "$datei" ] || datei="$(basename "$datei")"
sed -n '3,${
/^#/!q
s/^# \{0,1\}//
p
}' "$datei"
}
# team_argumente_pruefen "$@": fuer Skripte, die KEINE Argumente kennen.
# --hilfe/--help/-h druckt den Kopf und endet mit 0; jedes andere Argument
# endet mit 2 auf stderr. Der argumentlose Aufruf laeuft unveraendert durch.
#
# Der EXIT-CODE ist die Zusicherung, nicht der Text: Er ist das, was ein
# aufrufendes Skript auswerten kann (dieselbe Trennung wie bei den
# Buchungsverben aus BL-222).
team_argumente_pruefen() {
case "${1:-}" in
"") return 0 ;;
--hilfe|--help|-h) team_hilfe_kopf; exit 0 ;;
*) echo "$(basename "$0"): unbekanntes Argument '$1' — dieses Skript nimmt keine Argumente. '--hilfe' zeigt den Dateikopf." >&2
exit 2 ;;
esac
}
# team_hilfe_wenn_gefragt "$@": fuer Skripte, die ihre uebrigen Argumente
# WEITERREICHEN (team-test.sh und kit-test.sh reichen an pytest durch). Hier
# darf nichts zurueckgewiesen werden — sonst faengt der Riegel genau die
# Argumente, wegen derer es die Durchreiche gibt. Abgefangen wird deshalb nur
# `--hilfe`: die deutsche Schreibweise, die pytest nicht kennt. `--help`/`-h`
# gehen bewusst weiter an pytest, das dafuer eine eigene, bessere Hilfe hat.
team_hilfe_wenn_gefragt() {
[ "${1:-}" = "--hilfe" ] || return 0
team_hilfe_kopf
exit 0
}
# team_allowed_tools <redteam|axel>: Werkzeug-Allowlist für Guard-Linie 2.
# Axel bekommt NUR den Plan-Ordner, das Red Team zusätzlich den Test-Ordner.
team_allowed_tools() {
local basis="Read Grep Glob Bash(${TEAM_BEUTEBUCH_TOOL}:*) Bash(git log:*) Bash(git diff:*) Bash(git show:*)"
[ -n "${TEAM_SMOKE_TEST:-}" ] && basis="$basis Bash(${TEAM_SMOKE_TEST})"
local plan="Edit(${TEAM_PLAN_ORDNER%/}/**) Write(${TEAM_PLAN_ORDNER%/}/**)"
case "$1" in
axel) echo "$basis $plan" ;;
*) echo "$basis $plan Edit(${TEAM_TEST_ORDNER%/}/**) Write(${TEAM_TEST_ORDNER%/}/**)" ;;
esac
}
# --- Modelle -----------------------------------------------------------------
# Loop-Rollen (Ralph, Harry, Marv, Frank): günstiges Modell.
# Axel & Der Architekt: starkes Modell, immer API.
TEAM_MODEL_LOOP="${TEAM_MODEL_LOOP:-sonnet}"
TEAM_MODEL_STRONG="${TEAM_MODEL_STRONG:-opus}"
# --- Budget pro Rolle (Zwei-Schwellen-Modell) --------------------------------
# Stakeholder-Entscheid 2026-07-12 (realer Auslöser HM-32): Ein zu tiefes
# Pro-Rolle-Budget ist ökonomisch absurd — der alte 1-USD-Frank-Cap griff ERST
# NACH dem (bereits bezahlten) Claude-Aufruf und warf über den Rollback die
# schon bezahlte Arbeit weg, der nächste Versuch kostete erneut: der Cap
# "sparte" nichts, er vervielfachte die Kosten und blockierte obendrein den Fund.
#
# Neues Modell — EINE zentrale Basiszahl statt drei divergierender Defaults:
# TEAM_ROLE_BUDGET_USD (Default 5) — Soft-Cap, gilt für ALLE Rollen.
# TEAM_ROLE_HARDCAP_USD (Default 10) — Hard-Cap für die iterierenden
# "Sorgenkinder" Frank & Axel (2× Soft).
#
# Zwei Schwellen (siehe team_budget_check):
# - Frank & Axel: SOFT-Cap = nur deutlicher Hinweis, KEIN Rollback, der Fix
# bleibt gültig und wird normal geprüft; erst der HARD-Cap (Ausreißer:
# Endlosschleife/kaputter Prompt) bricht mit Rollback+Cleanup ab.
# - Ralph/Harry/Marv: sofortiger HART-Cap beim Soft-Wert (kein Soft-Fenster) —
# Ralph hat nach dem Commit ohnehin Feierabend (kein Rollback, Mensch
# schaltet weiter), Harry/Marv sind read-only (nichts Bezahltes geht verloren).
# BL-131: Wie der Python-Interpreter heisst, ist eine Eigenschaft der
# MASCHINE, nicht der Bahn. Bis hierher stand `python3` dreizehnmal fest im
# Text — mit der Begruendung, diese Bahn laufe unter Linux. Unter Git-Bash auf
# Windows laeuft sie das nicht, und dort ist `python3` der App-Execution-Alias
# aus dem Microsoft Store: Er startet den Store, schreibt "Python was not
# found" und endet mit 49. Jeder dieser dreizehn Aufrufe war damit tot.
# Die pwsh-Bahn hatte das Problem nie — sie hat dieselben dreizehn Bloecke
# durch native Ausdruecke ersetzt. Der Default hier bleibt `python3`, weil er
# unter POSIX richtig ist; der Installer traegt in team.config.sh ein, was er
# auf der Maschine wirklich gefunden hat.
TEAM_PYTHON="${TEAM_PYTHON:-python3}"
TEAM_ROLE_BUDGET_USD="${TEAM_ROLE_BUDGET_USD:-5}"
TEAM_ROLE_HARDCAP_USD="${TEAM_ROLE_HARDCAP_USD:-10}"
# --- Auth-Modus --------------------------------------------------------------
# team_resolve_auth_mode [rollen-default]
#
# Auflösung (Prio absteigend):
# 1. Env AUTH_MODE — explizite Übersteuerung pro Aufruf
# 2. ~/.config/claude-team/auth-mode — maschinenlokaler Default
# 3. Rollen-Default ($1) — Loop-Rollen übergeben "abo",
# starke Rollen (Axel/Architekt) nichts → "api"
#
# "abo" = Claude-Abo via `claude login`. Der API-Key wird aus der Umgebung
# entfernt, damit die CLI sicher über das Abo abrechnet.
# "api" = Pay-per-Use. Key-Quelle: Env ANTHROPIC_API_KEY, sonst
# ~/.config/claude-team/api-key (eine Zeile, chmod 600).
# Der Key gehört bewusst NICHT in .bashrc — sonst greifen ihn auch
# interaktive Abo-Sessions auf.
# team_auth_mode_effektiv [rollen-default] — löst NUR den Modus auf
# (Env AUTH_MODE → ~/.config/claude-team/auth-mode → Rollen-Default) und gibt ihn
# auf stdout aus. KEINE Seiteneffekte (kein unset, kein Key-Laden). Erlaubt
# Orchestratoren (vollautomatik.sh/halbautomatik.sh), den effektiven Modus zu
# prüfen, ohne die Prozess-Umgebung anzufassen.
team_auth_mode_effektiv() {
local rollen_default="${1:-api}"
local cfg="$HOME/.config/claude-team/auth-mode"
if [ -n "${AUTH_MODE:-}" ]; then
printf '%s\n' "$AUTH_MODE"
elif [ -f "$cfg" ]; then
head -n1 "$cfg" | tr -d '[:space:]'
else
printf '%s\n' "$rollen_default"
fi
}
# team_warnung_abo_key — warnt EINMAL pro Prozessbaum, wenn im Abo-Modus ein
# ANTHROPIC_API_KEY in der Umgebung liegt (das Abo-first-Design still aushebelt).
# Idempotent: setzt beim ersten Auslösen TEAM_ABO_KEY_WARNUNG_GEZEIGT=1
# (exportiert), sodass alle NACHKOMMEN-Prozesse schweigen. Damit „einmal pro
# Prozessbaum" auch über die SIBLING-Rollenprozesse von vollautomatik.sh/
# halbautomatik.sh hält (HM-32), seeden die Orchestratoren diese Funktion selbst
# EINMAL im eigenen Prozess — analog team_lock/TEAM_LOCK_HELD.
team_warnung_abo_key() {
if [ -n "${ANTHROPIC_API_KEY:-}" ] \
&& [ "${TEAM_ABO_KEY_WARNUNG:-1}" != "0" ] \
&& [ "${TEAM_ABO_KEY_WARNUNG_GEZEIGT:-0}" != "1" ] \
&& [ "${TEAM_KEY_AUS_FALLBACK:-0}" = "1" ]; then
# BL-48 (Feld K29/136): Der Key stammt NICHT aus der Umgebung des
# Menschen, sondern aus einem API-Fallback derselben Prozesskette —
# team_resolve_auth_mode hat ihn selbst exportiert und dort bleibt er
# für die Folgestufe stehen. Die .bashrc-Empfehlung unten zeigt dann
# auf eine Datei, in der nichts steht, und beschreibt eine Ursache, die
# nicht vorlag; im Feld wurde real per /proc/<pid>/environ nachgesehen.
# Deshalb: eigener Satz, der sagt was wirklich passiert ist — und
# KEIN TEAM_ABO_KEY_WARNUNG_GEZEIGT. Das eine Warnfenster pro
# Prozessbaum gehört dem echten Fall (Key aus .bashrc, ~13,8 USD
# Leerlauf über API), nicht diesem Fehlalarm.
echo "Hinweis: ANTHROPIC_API_KEY liegt in der Prozess-Umgebung, gesetzt vom API-Fallback" >&2
echo " eines vorigen Aufrufs (nicht aus .bashrc). Er wird für diesen Abo-Aufruf" >&2
echo " unmittelbar entfernt — kein Handlungsbedarf (BL-48)." >&2
return 0
fi
if [ -n "${ANTHROPIC_API_KEY:-}" ] \
&& [ "${TEAM_ABO_KEY_WARNUNG:-1}" != "0" ] \
&& [ "${TEAM_ABO_KEY_WARNUNG_GEZEIGT:-0}" != "1" ]; then
echo "WARNUNG: AUTH_MODE=abo, aber ANTHROPIC_API_KEY liegt in der Prozess-Umgebung —" >&2
echo " die Claude-CLI kann dann den (teuren) API-Weg dem Abo vorziehen." >&2
echo " Empfohlen: den Key aus .bashrc/der Shell-Env nehmen und stattdessen in" >&2
echo " ~/.config/claude-team/api-key (chmod 600) ablegen — siehe CLAUDE.md 'Auth-Modi'." >&2
echo " Selbstprüfen: echo \"\${ANTHROPIC_API_KEY:+gesetzt}\" (leer = ok)." >&2
export TEAM_ABO_KEY_WARNUNG_GEZEIGT=1
fi
}
# team_api_weg_vorhanden: Gibt es ueberhaupt einen API-Schluessel? (0 = ja)
#
# BL-174: Reine ABFRAGE, ohne etwas zu setzen und ohne zu meckern. Sie
# beantwortet die Frage, die team_claude vor dem Fallback stellen muss —
# `team_resolve_auth_mode` kann das nicht, weil sie im Fehlerfall MELDET und
# der Aufrufer ihren Rueckgabewert bisher als Abbruch gelesen hat.
#
# In einer reinen Abo-Installation ist ein fehlender Schluessel kein Fehler,
# sondern der ERWARTETE Zustand — seit dem Entscheid "keine Rolle ist fest
# api" ist das der empfohlene Normalfall.
# BL-173: WIE die Agenten-CLI heisst, entscheidet die Maschine — nicht diese
# Datei. Dieselbe Lehre wie TEAM_PYTHON (BL-131), und sie wiegt hier schwerer:
# Claude Code wird legitim IDE-GEBUENDELT ausgeliefert (VS Code / VSCodium,
# Binary unter resources/native-binary/claude). Eine Maschine kann eine
# vollstaendig eingerichtete, angemeldete Installation haben, ohne dass
# `claude` in irgendeinem PATH aufloesbar ist — genau die Lage lag im Feld vor:
# ~/.claude/.credentials.json vorhanden, Abo aktiv, Erweiterung lief, und
# `command -v claude` leer.
TEAM_CLAUDE_BIN="${TEAM_CLAUDE_BIN:-claude}"
# team_cli_vorhanden: Ist die Agenten-CLI ueberhaupt aufloesbar? (0 = ja)
#
# BL-173, die WICHTIGERE Haelfte. Ohne diese Pruefung war der Ablauf:
# `claude: command not found` (eine Zeile, scrollt vorbei) -> ein 0-Byte-Log
# -> team_bewerte_ergebnis schreibt einen ERSATZZETTEL fuer einen Aufruf, der
# nie stattgefunden hat -> der Abo-Fehler loest planmaessig den API-Fallback
# aus -> und der bricht mit der Meldung ab, die stehen bleibt und die der
# Mensch liest: "FEHLER: AUTH_MODE=api, aber weder ANTHROPIC_API_KEY gesetzt
# noch …/api-key lesbar."
#
# Diagnostiziert wird ein Auth-Problem; vorliegt ein PATH-Problem. Wer dieser
# Meldung folgt, besorgt einen API-Schluessel — und scheitert ein zweites Mal
# an derselben Stelle, weil auch der API-Weg dasselbe `claude` aufruft.
#
# Eine fehlende Programmdatei ist KEINE Fehlerklasse, die ein Auth-Fallback
# heilen kann. Dieselbe Erwaegung wie bei Exit 42/43: eine eigene Klasse, die
# man BENENNT, statt sie in eine bestehende zu pressen.
team_cli_vorhanden() {
case "$TEAM_CLAUDE_BIN" in
*/*) [ -x "$TEAM_CLAUDE_BIN" ] && return 0 ;;
*) command -v "$TEAM_CLAUDE_BIN" >/dev/null 2>&1 && return 0 ;;
esac
return 1
}
team_cli_fehlt_melden() {
local rolle="${1:-team}"
echo "[$rolle] FEHLER: Die Agenten-CLI ist nicht auffindbar (gesucht: '$TEAM_CLAUDE_BIN')." >&2
echo " Das ist KEIN Auth-Problem — ein API-Schluessel hilft hier nicht." >&2
echo " Ist Claude Code als IDE-Erweiterung installiert, liegt die Binaerdatei" >&2
echo " meist unter <erweiterung>/resources/native-binary/claude und nicht im PATH." >&2
echo " Trag den vollen Pfad als TEAM_CLAUDE_BIN in team.config.sh ein." >&2
}
team_api_weg_vorhanden() {
[ -n "${ANTHROPIC_API_KEY:-}" ] && return 0
[ -r "$HOME/.config/claude-team/api-key" ] && return 0
return 1
}
team_resolve_auth_mode() {
local rollen_default="${1:-api}"
local keyfile="$HOME/.config/claude-team/api-key"
AUTH_MODE="$(team_auth_mode_effektiv "$rollen_default")"
case "$AUTH_MODE" in
api)
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
if [ -r "$keyfile" ]; then
ANTHROPIC_API_KEY="$(head -n1 "$keyfile" | tr -d '[:space:]')"
export ANTHROPIC_API_KEY
# BL-48: Merken, dass DIESE Kette den Key gesetzt hat. Ein
# späterer Abo-Aufruf im selben Prozessbaum erbt ihn und
# bekäme sonst die .bashrc-Warnung für eine Ursache, die
# nie vorlag (siehe team_warnung_abo_key).
export TEAM_KEY_AUS_FALLBACK=1
else
echo "FEHLER: AUTH_MODE=api, aber weder ANTHROPIC_API_KEY gesetzt noch $keyfile lesbar." >&2
return 1
fi
fi
;;
abo)
team_warnung_abo_key
# Abo-Abrechnung erzwingen: API-Key aus der Umgebung nehmen.
unset ANTHROPIC_API_KEY
;;
*)
echo "FEHLER: Unbekannter AUTH_MODE '$AUTH_MODE' (erlaubt: api|abo)." >&2
return 1
;;
esac
echo "Auth-Modus: $AUTH_MODE"
}
# --- Zentraler Claude-Aufruf ---------------------------------------------------
# team_claude <rolle> <modell> <outfile> <prompt> [weitere claude-Flags …]
#
# Abo-first mit automatischem API-Fallback (Stakeholder-Entscheid 2026-07-10:
# gilt für ALLE Rollen, auch Axel): Auth wird pro Aufruf frisch aufgelöst;
# scheitert der Abo-Aufruf (Exit ≠ 0 oder is_error), folgt genau EIN API-Retry.
# Nach dem Aufruf stehen TEAM_LAST_COST (USD) und TEAM_LAST_OUT (Log-Datei).
# TEAM_LAST_COST ist die Summe ALLER Versuche dieses Aufrufs (gescheiterter
# Abo-Versuch + API-Fallback + 429-Retries), nicht nur des letzten — sonst waere
# der Pro-Stufe-Budget-Cap durch einen teuren Fehlversuch umgehbar (BL-55).
# TEAM_LAST_OUT bleibt das FINALE Log (Promise-Pruefung, HM-20).
#
# Session-Limit (429, Kaskade 9 / Stufe 30, Strategie A+B; API-Fallback-Reihenfolge
# per Stakeholder-Entscheid 2026-07-11 umgestellt, Commit f787936): Bei JEDEM
# Abo-Fehler — Timeout, normaler Fehler ODER 429/Session-Limit — versucht
# team_claude SOFORT den einmaligen API-Fallback (eigenes, separates Kontingent;
# hilft auch bei einem Abo-429). Erst wenn AUCH das finale (ggf. API-)Ergebnis
# noch ein 429 ist (team_result_is_429), greift die Warte-und-Wiederhol-Logik
# (A). Ist der Reset unbekannt, zu weit in der Zukunft, oder sind die Retries
# erschöpft, gibt team_claude ein eindeutiges Pausen-Signal zurück (B):
# Exit-Code 42, TEAM_LAST_PAUSE=1 und TEAM_LAST_RESET="HH:MM". Ein normaler
# (nicht-429) Fehler, der auch über API scheitert, endet mit Exit 1.
#
# TEAM_429_MAX_RETRIES Anzahl Warte-und-Wiederhol-Zyklen (Default 2).
# TEAM_429_MAX_WARTEN Harte Obergrenze der Wartezeit in Sekunden
# (Default 1800 = 30 Min); 0 schaltet A komplett ab
# (nur B, reiner Pausen-Exit).
# TEAM_429_PUFFER Sicherheitspuffer nach dem Reset-Zeitpunkt in
# Sekunden (Default 30).
#
# TEAM_DRY_RUN=1: kein echter Aufruf — schreibt ein Stub-JSON mit dem Inhalt
# von TEAM_DRY_RESULT (Default leer) für Mechanik-Tests der Pipeline.
TEAM_AUTH_USER="${AUTH_MODE:-}"
TEAM_429_MAX_RETRIES="${TEAM_429_MAX_RETRIES:-2}"
TEAM_429_MAX_WARTEN="${TEAM_429_MAX_WARTEN:-1800}"
TEAM_429_PUFFER="${TEAM_429_PUFFER:-30}"
# team_429_env_int <variablenname> <default>
# Erzwingt, dass die benannte TEAM_429_*-Variable eine reine, nicht-negative
# Ganzzahl ist, BEVOR sie in $(( … ))/[ … ] verwendet wird. Ein aus der
# Umgebung übernommener Wert wie "$(beliebiger Befehl)" würde in einer
# Bash-Arithmetik-Expansion sonst ausgeführt (Command-Injection, HM-17); ein
# nicht-numerischer Wert wie "abc" ließe `test` stillschweigend scheitern und
# den TEAM_429_MAX_WARTEN-Deckel fail-open statt fail-safe wirkungslos werden
# (ebenfalls HM-17). Bei ungültigem Wert: Fehlermeldung + Fallback auf Default.
team_429_env_int() {
local name="$1" default="$2" wert
eval "wert=\"\${$name}\""
case "$wert" in
''|*[!0-9]*)
echo "[team_claude] Ungültiger Wert für ${name}=\"${wert}\" (muss eine nicht-negative Ganzzahl sein) — falle auf Default ${default} zurück." >&2
eval "$name=\"$default\""
;;
esac
}
team_429_env_int TEAM_429_MAX_RETRIES 2
team_429_env_int TEAM_429_MAX_WARTEN 1800
team_429_env_int TEAM_429_PUFFER 30
# team_429_sleep <sekunden>
# Kapselt `sleep` fürs Warten auf einen Session-Limit-Reset. TEAM_DRY_RUN=1
# oder TEAM_429_SKIP_SLEEP=1 überspringen das echte Warten — so darf ein Test
# niemals real bis zu 30 Minuten blockieren (Verifikationspflicht Stufe 30).
team_429_sleep() {
local sekunden="$1"
if [ "${TEAM_DRY_RUN:-0}" = "1" ] || [ "${TEAM_429_SKIP_SLEEP:-0}" = "1" ]; then
echo "[429] (Test) sleep ${sekunden}s übersprungen." >&2
return 0
fi
sleep "$sekunden"
}
# team_versuch_sichern <rolle> <json-datei> <dauer-sekunden>
# BL-46 (Feld K29/135, 2026-08-10): Ein gescheiterter Abo-Anlauf hinterließ ein
# Log von 0 Byte — nach 47 Minuten Laufzeit. Eine Quittung über null ist von
# „hat nichts gekostet" nicht zu unterscheiden: `kosten.py summe` addierte
# stillschweigend 0.0000, der Pro-Stufe-Deckel bekam auf diese Hälfte keinen
# Griff, und die Stufe erschien in der Kostentabelle als die BILLIGSTE der
# Kaskade, obwohl sie als teuerste angesetzt war. Wer diese Tabelle später als
# Vergleichsband liest, schreibt eine Zahl fort, die eine halbe Stufe beschreibt.
#
# Deshalb: Ist das Versuchslog nicht als JSON lesbar, tritt an seine Stelle ein
# ERSATZZETTEL mit dem, was belegbar ist — Dauer und die ausdrückliche Aussage
# „Kosten unbekannt". NICHT geschätzt: Der Zettel behauptet keine Zahl, er macht
# die Lücke sichtbar. `is_error` bleibt gesetzt, damit der bestehende
# Fehlerpfad (API-Fallback) unverändert greift — vorher war die unlesbare Datei
# über team_result_is_error genau das, und das soll sie bleiben.
# Rückgabe 0, wenn ein Zettel geschrieben wurde (Aufrufer meldet es).
team_versuch_sichern() {
local rolle="$1" out="$2" dauer="$3"
"$TEAM_PYTHON" - "$out" "$dauer" <<'PY'
import json, sys
pfad, dauer = sys.argv[1], sys.argv[2]
try:
with open(pfad) as fh:
json.load(fh)
sys.exit(1) # brauchbares Log — nichts zu tun
except Exception:
pass
try:
sekunden = int(float(dauer))
except (TypeError, ValueError):
sekunden = None
json.dump({"is_error": True, "result": "", "total_cost_usd": None,
"team_versuch": "verworfen", "team_dauer_s": sekunden},
open(pfad, "w"))
sys.exit(0)
PY
}
# team_versuch_melden <rolle> <json-datei> <start-epoch>
# Bequemer Aufrufer von team_versuch_sichern: rechnet die Dauer selbst aus und
# sagt laut, dass hier ein bezahlter Anlauf ohne Quittung liegt. Die Meldung ist
# der eigentliche Zweck — im Feld fiel die 0-Byte-Datei nur auf, weil ein Mensch
# den Ordner ansah (BL-46).
team_versuch_melden() {
local rolle="$1" out="$2" t0="$3" dauer
dauer=$(( $(date +%s) - t0 ))
if team_versuch_sichern "$rolle" "$out" "$dauer"; then
echo "[$rolle] VERWORFENER VERSUCH: '$out' war nach ${dauer}s nicht als JSON lesbar (0 Byte/abgeschnitten)." >&2
echo " Ersatzzettel geschrieben — die Dauer ist belegt, die Kosten sind UNBEKANNT und werden" >&2
echo " in keiner Summe geschätzt. Der Aufruf gilt weiter als Fehler (API-Fallback greift)." >&2
fi
}
team_claude() {
local rolle="$1" modell="$2" out="$3" prompt="$4"
shift 4
TEAM_LAST_PAUSE=0
TEAM_LAST_RESET=""
TEAM_LAST_KEIN_ZUG=0
if [ "${TEAM_DRY_RUN:-0}" = "1" ]; then
"$TEAM_PYTHON" - "$out" "${TEAM_DRY_RESULT:-}" <<'PY'
import json, sys
json.dump({"result": sys.argv[2], "total_cost_usd": 0.01, "is_error": False},
open(sys.argv[1], "w"))
PY
TEAM_LAST_COST="0.01"; TEAM_LAST_OUT="$out"
echo "[$rolle] DRY-RUN — kein Claude-Aufruf."
return 0
fi
# BL-173: VOR dem ersten Aufruf pruefen, ob es die CLI ueberhaupt gibt.
# Kein API-Fallback, kein Ersatzzettel, kein Fehlversuchs-Zaehler —
# nichts davon passt auf "das Programm gibt es nicht".
if ! team_cli_vorhanden; then
team_cli_fehlt_melden "$rolle"
return 1
fi
AUTH_MODE="$TEAM_AUTH_USER"
team_resolve_auth_mode abo || return 1
local fehler=0 cli_exit=0 t0=0
# Alle Logs DIESES Aufrufs — auch die gescheiterten Vorversuche. TEAM_LAST_COST
# ist die Summe darueber, nicht nur der letzte Versuch (BL-55).
local -a versuch_logs=()
t0="$(date +%s)"
"$TEAM_CLAUDE_BIN" -p "$prompt" --model "$modell" --output-format json "$@" > "$out" || cli_exit=1
team_versuch_melden "$rolle" "$out" "$t0"
versuch_logs+=("$out")
if team_bewerte_ergebnis "$rolle" "$out" "$cli_exit"; then fehler=0; else fehler=1; fi
# Stakeholder-Entscheid (2026-07-11): Bei JEDEM Abo-Fehler — egal ob
# Timeout, normaler Fehler ODER 429/Session-Limit — SOFORT den API-Fallback
# versuchen. Der API-Key hat ein eigenes, separates Kontingent (bewiesen
# 2026-07-11: bei erschöpftem Abo-Kontingent liefert derselbe Prompt mit dem
# Key erfolgreich `result:ok`), daher hilft der Fallback auch bei einem
# Abo-429. Erst wenn AUCH der API-Weg scheitert, greift die 429-Warte-/
# Retry-/Pausen-Logik weiter unten.
#
# WICHTIG (Bugfix 2026-07-11): Der Key wird dem `claude`-Aufruf EXPLIZIT als
# Aufruf-lokales Environment vorangestellt (ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
# claude …). Ein bloßes `export` über team_resolve_auth_mode reicht NICHT:
# die CLI bevorzugte im selben Prozess weiterhin die (limitierte) Abo-Session
# und ignorierte den frisch exportierten Key — der Fallback lief real erneut
# ins Abo-429. Mit dem vorangestellten Key greift der API-Weg zuverlässig.
# BL-174: ZUERST fragen, ob es ueberhaupt einen API-Weg gibt.
#
# Vorher stand hier `team_resolve_auth_mode || return 1`. In einer reinen
# Abo-Installation gibt die Funktion fuer AUTH_MODE=api eine 1 zurueck —
# das `|| return 1` verliess team_claude also SOFORT, und die gesamte
# 429-Sonderbehandlung darunter wurde nie erreicht. Ein Session-Limit,
# also genau die Klasse, fuer die BL-20/BL-25 den Exit 42 eingefuehrt
# haben ("weder ein sauberer Erfolg noch ein echter Fehler, sondern eine
# eigene, klar benannte Klasse"), kam als Exit 1 heraus: "ECHTER Fehler,
# Mensch gefragt". Kein Warten bis zum Reset, kein Pausen-Signal, keine
# der drei dokumentierten Zusicherungen.
#
# Der Airbag war damit ausgerechnet im empfohlenen Normalfall ausgebaut,
# und zwar unsichtbar: Der Fehler zeigt sich erst, wenn das Kontingent
# voll ist — nach Stunden Laufzeit, an der teuersten Stelle.
#
# Ein fehlender Schluessel ist hier kein Fehler, sondern der erwartete
# Zustand. Er darf den Ablauf nicht abschneiden.
if [ "$fehler" -eq 1 ] && [ "$AUTH_MODE" = "abo" ] && ! team_api_weg_vorhanden; then
echo "[$rolle] Abo-Aufruf fehlgeschlagen — kein API-Schluessel hinterlegt, also kein Fallback. Weiter mit der regulaeren Limit-Behandlung." >&2
elif [ "$fehler" -eq 1 ] && [ "$AUTH_MODE" = "abo" ]; then
echo "[$rolle] Abo-Aufruf fehlgeschlagen (Timeout/Limit/429?) — einmaliger API-Fallback. Log: $out"
AUTH_MODE=api
team_resolve_auth_mode || return 1
out="${out%.json}-api-fallback.json"
cli_exit=0
t0="$(date +%s)"
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" "$TEAM_CLAUDE_BIN" -p "$prompt" --model "$modell" --output-format json "$@" > "$out" || cli_exit=1
team_versuch_melden "$rolle" "$out" "$t0"
versuch_logs+=("$out")
if team_bewerte_ergebnis "$rolle" "$out" "$cli_exit"; then fehler=0; else fehler=1; fi
fi
# 429-Sonderbehandlung jetzt auf dem FINALEN (ggf. API-)Ergebnis: nur wenn
# auch der API-Weg noch einen 429 liefert, wird gewartet/gepausiert.
if [ "$fehler" -eq 1 ] && team_result_is_429 "$out"; then
local versuch=1 reset_epoch jetzt warten reset_hhmm pausieren=1
while [ "$versuch" -le "$TEAM_429_MAX_RETRIES" ]; do
reset_epoch="$(team_429_reset_epoch "$out")" || reset_epoch=""
if [ -z "$reset_epoch" ]; then
echo "[$rolle] 429 erkannt, Reset-Zeit unbekannt — kein Warten, Pausen-Signal." >&2
break
fi
jetzt="$(date +%s)"
warten=$(( reset_epoch - jetzt + TEAM_429_PUFFER ))
reset_hhmm="$(date -d "@$reset_epoch" +%H:%M 2>/dev/null || date -r "$reset_epoch" +%H:%M)"
if [ "$TEAM_429_MAX_WARTEN" -le 0 ] || [ "$warten" -gt "$TEAM_429_MAX_WARTEN" ]; then
echo "[$rolle] 429 erkannt, Reset erst in ${warten}s (> TEAM_429_MAX_WARTEN=${TEAM_429_MAX_WARTEN}s) — kein Warten, Pausen-Signal." >&2
break
fi
echo "[$rolle] 429/Session-Limit erkannt (Versuch $versuch/$TEAM_429_MAX_RETRIES) — warte ${warten}s bis Reset (${reset_hhmm}) + Puffer." >&2
team_429_sleep "$warten"
out="${out%.json}-429-retry${versuch}.json"
cli_exit=0
t0="$(date +%s)"
# Wie beim API-Fallback oben: Key explizit voranstellen, falls der
# aktuelle AUTH_MODE api ist (sonst greift die CLI ggf. die Abo-Session).
if [ "$AUTH_MODE" = "api" ]; then
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" "$TEAM_CLAUDE_BIN" -p "$prompt" --model "$modell" --output-format json "$@" > "$out" || cli_exit=1
else
"$TEAM_CLAUDE_BIN" -p "$prompt" --model "$modell" --output-format json "$@" > "$out" || cli_exit=1
fi
team_versuch_melden "$rolle" "$out" "$t0"
versuch_logs+=("$out")
if team_bewerte_ergebnis "$rolle" "$out" "$cli_exit"; then fehler=0; else fehler=1; fi
if [ "$fehler" -eq 0 ]; then
echo "[$rolle] Retry nach 429 erfolgreich. Log: $out" >&2
pausieren=0
break
fi
if ! team_result_is_429 "$out"; then
echo "[$rolle] Retry lieferte einen normalen (nicht-429) Fehler — weiter im normalen Fehlerpfad." >&2
pausieren=0
break
fi
versuch=$((versuch + 1))
done
if [ "$fehler" -eq 1 ] && [ "$pausieren" -eq 1 ]; then
TEAM_LAST_PAUSE=1
TEAM_LAST_RESET="${reset_hhmm:-unbekannt}"
TEAM_LAST_COST="$(team_summe_cost_usd "${versuch_logs[@]}")"
TEAM_LAST_OUT="$out"
echo "[$rolle] 429/Session-Limit — Retries erschöpft oder Reset zu weit entfernt. Pausen-Signal (Reset: $TEAM_LAST_RESET)." >&2
return 42
fi
fi
TEAM_LAST_COST="$(team_summe_cost_usd "${versuch_logs[@]}")"
TEAM_LAST_OUT="$out"
if [ "$fehler" -eq 1 ]; then
# BL-228: Kam überhaupt ein Modell zum Zug? Der Rückgabewert bleibt 1 —
# ein Lauf ohne Ergebnis ist ein Lauf ohne Ergebnis, und die
# Stagnations-Bremse der Vollautomatik muss ihn weiter sehen. Was sich
# ändert, ist die ZURECHNUNG: Der Aufrufer erfährt über
# TEAM_LAST_KEIN_ZUG, dass die Rolle nichts dafür kann.
if team_kein_zug "${versuch_logs[@]}"; then
TEAM_LAST_KEIN_ZUG=1
echo "[$rolle] Kein Modell kam zum Zug (0 Turns, 0.0000 USD) — Netz/Proxy vor dem ersten Token. Log: $out" >&2
else
echo "[$rolle] Claude-Aufruf endgültig fehlgeschlagen, Log: $out" >&2
fi
return 1
fi
return 0
}
# team_kein_zug <json-datei>...
# War in diesem Aufruf überhaupt ein Modell am Zug? (BL-228)
#
# Ein Netzfehler VOR dem ersten Token — Proxy nicht gesetzt, VPN-Aussetzer,
# kurzer Abriss — endet mit `num_turns: 0` und `total_cost_usd: 0.0000`: Kein
# Modell hat den Auftrag je gesehen. Für die Rolle ist das derselbe Fall wie
# das Session-Limit (Exit 42, HM-24) — kein INHALTLICHER Fehlversuch, sondern
# gar keiner. Im Feld standen nach zwei solchen Aussetzern zwei Fehlversuche
# gegen einen Fund, den nie ein Modell gelesen hatte; nach dreien hätte der
# Fund auf „an Axel übergeben" gestanden, also auf der teuersten Rolle.
#
# WARUM AN DEN ZAHLEN UND NICHT AM FEHLERTEXT: Der Text der CLI ist ihre Sache
# und ändert sich ("Connection refused", "a firewall or proxy may be blocking
# it"); die beiden Zahlen sind der Vertrag. Es ist zudem die dritte Variante
# desselben Musters (Session-Limit, unquittierte Sitzung, Netzfehler) — ein
# dritter Textzweig wäre genau die Bauform, die BL-214 beim Abtragen eng
# ziehen musste. „0 Turns und 0.0000 USD" trifft alle drei und ist maschinell
# prüfbar.
#
# BEWEISLAST STATT VERMUTUNG: Verlangt werden BEIDE Zahlen, in JEDEM Log dieses
# Aufrufs (Abo-Versuch, API-Fallback, 429-Retries). Fehlt `num_turns` — etwa im
# Ersatzzettel nach einem abgeschnittenen Log (BL-46) — oder fehlt die Datei,
# sind die Kosten UNBEKANNT, und dann bleibt es ein gewöhnlicher Fehlversuch.
# Die Fehlerrichtung ist gewollt: lieber einen Fehlversuch zu viel zählen als
# einen echten verschlucken (BL-160).
team_kein_zug() {
[ "$#" -gt 0 ] || return 1
"$TEAM_PYTHON" - "$@" <<'PY'
import json, sys
for pfad in sys.argv[1:]:
try:
daten = json.load(open(pfad))
except Exception:
sys.exit(1)
turns, kosten = daten.get("num_turns"), daten.get("total_cost_usd")
if turns is None or kosten is None:
sys.exit(1)
try:
if float(turns) != 0 or float(kosten) != 0:
sys.exit(1)
except (TypeError, ValueError):
sys.exit(1)
sys.exit(0)
PY
}
# team_promise_in <json-datei> <promise-text>
# Rückgabe 0, wenn das Ergebnis <promise>TEXT</promise> enthält.
team_promise_in() {
"$TEAM_PYTHON" -c '
import json, sys
try:
data = json.load(open(sys.argv[1]))
except Exception:
sys.exit(1)
result = data.get("result", "") or ""
sys.exit(0 if f"<promise>{sys.argv[2]}</promise>" in result else 1)
' "$1" "$2"
}
# --- Planmaessig uebersprungene Stufe (BL-255) ---------------------------------
# WARUM ES DIE ZWEITE QUITTUNGSFORM GIBT. Eine Stufe mit im Plan
# ausgeschriebener ABBRUCHBEDINGUNG trat im Feld ein: Die Rolle hat gemessen,
# die Konfiguration unangetastet gelassen, den Befund in den [Unreleased]-Block
# eingetragen, committet — und REGELKONFORM kein Promise gegeben, weil die
# Stufe nicht abgeschlossen, sondern abgebrochen wurde.
#
# Fuer diese Lage gab es keine Vokabel. Es gab Promise oder kein Promise, und
# *kein Promise* ist mit dem teuersten Bericht des Werkzeugs belegt (BL-41,
# Exit 43). Die Selbstpruefung machte es nicht besser, sondern zweideutig:
# Pruefung 1 (*hat die Sitzung Arbeit hinterlassen*) trifft zu, Pruefung 2
# (*gibt es eine beruehrte Testdatei*) in der Regel nicht — also faellt sie
# durch, und der Lauf landet im Exit 43 mit der Begruendung *Produktivcode ohne
# Zusicherung*, die ebenfalls nicht zutrifft. Der Mensch bekommt in BEIDEN
# Zweigen eine falsche Diagnose; eine abgebrochene Stufe, die zufaellig doch
# eine Testdatei angefasst hat, wuerde sogar still als abgeschlossen
# durchgewunken.
#
# WARUM DAS MEHR IST ALS KOSMETIK: Der vierte Ausgang ist die teuerste Meldung
# des Werkzeugs. Wird sie bei einem GEORDNETEN Abschluss gedruckt, stumpft sie
# ab und wird beim naechsten echten Fall weggeklickt.
#
# team_plan_erlaubt_uebersprung <stufe> [plan-datei]
# Rueckgabe 0, wenn der PLAN die zweite Quittungsform fuer genau diese Stufe
# ausschreibt. Das ist der Riegel (a) gegen ein Schlupfloch: Die Vokabel gilt
# nur dort, wo der Architekt die Abbruchbedingung vorher hingeschrieben hat.
# Geprueft wird die Zeichenkette selbst — sie traegt die Stufennummer, kann
# also nicht aus dem Block einer anderen Stufe stammen.
team_plan_erlaubt_uebersprung() {
local stufe="$1" plan="${2:-$(team_plan_datei)}"
[ -n "$plan" ] && [ -f "$plan" ] || return 1
grep -q "STUFE_${stufe}_UEBERSPRUNGEN" "$plan"
}
# team_gate_rot_seit: erste Zeile der Gate-Datei, oder leer. Der Aufrufer
# entscheidet, was er damit tut — gelesen wird an genau einer Stelle.
team_gate_rot_seit() {
[ -s "$TEAM_GATE_DATEI" ] || return 1
head -n1 "$TEAM_GATE_DATEI"
}
# --- Vierte Fehlerklasse: Sitzung beendet, Auftrag unquittiert (BL-41) --------
# Neben Erfolg, echtem Fehler und Session-Limit gibt es einen vierten Ausgang:
# Die Rolle beendet ihre Antwort freiwillig, ohne den Auftrag zu quittieren —
# typischerweise, weil sie einen Hintergrund-Task/Monitor/Wakeup gestartet hat
# und auf eine Benachrichtigung wartet, die in einer headless-Sitzung nie
# eintrifft. Das Ergebnis-JSON trägt dann `subtype: "success"`, `is_error:
# false`, `stop_reason: end_turn` — für team_result_is_error ein sauberer
# Erfolg. Nur das fehlende Promise verrät den Fall, und die bisherige Meldung
# ("KEIN Promise — Log prüfen") schickte den Menschen ins Log, das Erfolg meldet,
# und von dort in den PLAN statt in den Fehlermodus.
#
# Vier Vorfälle im Feld (K27/125, K28/131, K28/133, K33/154), zusammen
# 19,47 USD — jedes Mal für Arbeit, die fertig und grün war. Der Neustart wirft
# sie weg; von Hand quittieren rettet sie (im Feld belegt, Commit 458f660).
#
# WARUM NICHT AUF VOKABELN GEPRÜFT WIRD: Die drei Vorfälle formulierten es
# dreimal anders ("background pytest run and monitor", "fallback check /
# wakeup", "set up a monitor to catch its completion") — die vierte Variante
# schreibt jemand morgen. Geprüft wird deshalb die STRUKTUR: kein Promise, aber
# das Log erklärt sich selbst für erfolgreich.
#
# Abgrenzung zur Prävention (SMOKE_ZEILE, oben): Die Auflage steht am
# Prompt-Anfang, der Vorfall passiert nach 65 Turns — Prävention per Prompt
# skaliert gegenläufig zur Stufenlänge. Sie senkt die Wahrscheinlichkeit; DIESE
# Erkennung fängt den Rest.
# team_result_meldet_erfolg <json-datei>
# Rückgabe 0, wenn das Log sich selbst für erfolgreich erklärt (lesbar,
# is_error falsch und subtype "success" — beides muss zutreffen, sonst wäre
# jedes lesbare Log ohne is_error-Feld schon ein "Erfolg").
team_result_meldet_erfolg() {
"$TEAM_PYTHON" -c '
import json, sys
try:
data = json.load(open(sys.argv[1]))
except Exception:
sys.exit(1)
if data.get("is_error"):
sys.exit(1)
sys.exit(0 if data.get("subtype") == "success" else 1)
' "$1"
}
# team_quittung_fehlt_melden <rolle> <json-datei> <was-fehlt> <weiterweg…>
# Druckt die BENANNTE Meldung für den Fall oben. Der Aufrufer ruft sie NUR,
# wenn die Quittung fehlt; ob der Fall vorliegt, entscheidet diese Funktion.
# Rückgabe 0 = benannter Fall erkannt und gemeldet, 1 = gewöhnlicher Fehlschlag
# (der Aufrufer meldet dann wie bisher).
team_quittung_fehlt_melden() {
local rolle="$1" out="$2" was="$3"; shift 3
team_result_meldet_erfolg "$out" || return 1
echo "[$rolle] STUFE FERTIG, QUITTUNG FEHLT (BL-41) — $was" >&2
echo " Das Log meldet sich selbst als Erfolg (subtype=success, is_error=false), gibt aber" >&2
echo " keine Quittung. Das ist der benannte vierte Ausgang: Die Sitzung hat sich beendet," >&2
echo " meist im Warten auf einen Hintergrund-Task/Monitor/Wakeup, den es headless nicht gibt." >&2
echo " Die Arbeit ist in diesem Fall meist FERTIG — viermal im Feld, 19,47 USD. Prüfe in" >&2
echo " dieser Reihenfolge, BEVOR du neu startest (ein Neulauf wirft die bezahlte Arbeit weg):" >&2
local schritt
for schritt in "$@"; do
echo " - $schritt" >&2
done
# BL-201: Der kuerzeste Weg zur richtigen Diagnose, und er kostet eine
# Zeile. Im Feld loeste den Fall ALLEIN das Feld `result` im Lauf-Log —
# dort stand die Ursache woertlich ("waiting for the background test
# run"). Die Anleitung erwaehnte es an keiner Stelle; ohne den Hinweis
# sucht der Mensch in der Pruefreihenfolge weiter, deren beide Zweige
# einen roten Baum voraussetzen, und wirft im zweiten eine fertige,
# bezahlte Stufe weg.
echo " Log: $out" >&2
echo " Lies dort ZUERST das Feld \`result\` — die Rolle sagt darin meist selbst," >&2
echo " worauf sie gewartet hat. Neunmal im Feld stand dort das Warten auf einen" >&2
echo " Hintergrundlauf, und kein einziger erfolgreicher Lauf zeigte dieses Muster." >&2
return 0
}
# --- Laeuft der Verifikationsbefehl schon? (BL-207) ---------------------------
# team_smoke_parallel_lauf
# 0 = ein ZWEITER Lauf des Verifikationsbefehls ist nachweisbar
# 1 = keiner nachweisbar (das schliesst "auf diesem Wirt nicht feststellbar"
# ein — im Zweifel bleibt es beim bisherigen Verhalten)
#
# WARUM ES SIE GIBT (Feld B, 2026-08-28): Die Selbstpruefung startete den
# Verifikationsbefehl bedingungslos ein zweites Mal. Im Feld lief der
# Hintergrund-pytest der Rolle noch, als die Selbstpruefung IHREN eigenen
# startete. Zwei gleichzeitige Testlaeufe kollidieren (Datenbankdateien, Ports,
# App-Nutzerverzeichnisse); die Selbstpruefung meldete daraufhin ROT fuer einen
# Baum, der allein gefahren gruen war (199 passed, im Closeout nachgemessen) —
# und schickte den Menschen per BL-61-Text ausdruecklich auf die falsche
# Faehrte "Testaufbau". Wer ihr geglaubt und die Stufe neu gebaut haette,
# haette 2,36 USD bezahlte, fertige Arbeit weggeworfen.
#
# Eine Selbstpruefung, die im Zweifel "rot" BEHAUPTET, ist schlimmer als eine,
# die schweigt. Deshalb: erkannter Parallellauf ⇒ UNBEKANNT, nicht rot.
#
# Die gefundene Zeile wird mitgegeben (TEAM_SMOKE_PARALLEL_ZEILE), damit ein
# Fehlalarm — ein Dauerlaeufer wie pytest-watch, der den Befehl in seiner
# Kommandozeile traegt — in einem Blick als solcher erkennbar ist statt als
# Raetsel.
TEAM_SMOKE_PARALLEL_ZEILE=""
team_smoke_parallel_lauf() {
local muster="${TEAM_SMOKE_TEST:-}" tabelle treffer
TEAM_SMOKE_PARALLEL_ZEILE=""
[ -z "$muster" ] && return 1
tabelle="$(ps -eo args= 2>/dev/null || ps ax 2>/dev/null || true)"
[ -z "$tabelle" ] && return 1
treffer="$(echo "$tabelle" | grep -F -- "$muster" | head -1 || true)"
[ -z "$treffer" ] && return 1
TEAM_SMOKE_PARALLEL_ZEILE="$treffer"
return 0
}
# --- Der vierte Ausgang, selbst geprüft (BL-41 automatisiert) -----------------
# team_quittung_selbstpruefung <rolle> <stufe>
#
# WARUM ES SIE GIBT: Die Erkennung oben ist richtig, aber sie hält den Lauf an
# und legt einem Menschen eine Prüfliste vor, deren drei Schritte IMMER
# dieselben sind. Im Feld (Feld A) ist der Fall in neun Kaskaden
# aufgetreten — K27, K28, K29, K33, K34, K35 (dort dreimal), K36, K37 — und
# JEDES Mal lautete das Ergebnis "Arbeit fertig, nur die Quittung fehlt". Eine
# Prüfliste, die neunmal dasselbe ergibt, ist eine Funktion, die noch niemand
# geschrieben hat. Der Stopp kostet dabei mehr als nur Wartezeit: Er
# unterbricht die Vollautomatik mitten in der Kaskade, und der Mensch muss den
# Lauf von Hand fortsetzen.
#
# WAS SIE NICHT TUT: Sie quittiert NICHT auf Verdacht. Sie prüft genau das,
# was der Mensch geprüft hätte — plus den Punkt, an dem die Prüfliste selbst
# blind war (BL-135: Commit und grüner Baum unterscheiden eine HALB gebaute
# Stufe nicht von einer fertigen; sie fragen nicht nach der Existenz der
# Zusicherungen). Fällt auch nur eine Prüfung durch, gibt sie 1 zurück und der
# Aufrufer meldet unverändert an den Menschen. Der teure Fehler wäre, eine
# unfertige Stufe durchzuwinken — deshalb ist jede Prüfung ein UND, keine
# Mehrheit, und im Zweifel gilt "nicht bestanden".
#
# Rückgabe 0 = alle Prüfungen bestanden, der Aufrufer darf selbst quittieren.
# 1 = mindestens eine Prüfung offen (Grund steht auf stderr).
#
# Abschaltbar über TEAM_QUITTUNG_AUTO=0 (Default an). Aus bleibt die alte
# Prüflisten-Meldung, unverändert.
team_quittung_selbstpruefung() {
local rolle="$1" stufe="$2"
local test_ordner="${TEAM_TEST_ORDNER:-tests/}"
local smoke="${TEAM_SMOKE_TEST:-}"
if [ "${TEAM_QUITTUNG_AUTO:-1}" != "1" ]; then
return 1
fi
echo "[$rolle] Selbstprüfung des vierten Ausgangs (BL-41) für Stufe $stufe:" >&2
# (1) Hat die Sitzung überhaupt etwas hinterlassen? Ohne Arbeit gibt es
# nichts zu quittieren — dann ist es kein "fertig ohne Quittung",
# sondern eine Stufe, die nie angefangen hat.
local uncommittet betreff hat_arbeit=0
uncommittet="$(git status --porcelain 2>/dev/null)"
betreff="$(git log -1 --pretty=%s 2>/dev/null)"
[ -n "$uncommittet" ] && hat_arbeit=1
case "$betreff" in *"stufe$stufe"*|*"Stufe $stufe"*) hat_arbeit=1 ;; esac
if [ "$hat_arbeit" -eq 0 ]; then
echo " ✗ Kein Commit für Stufe $stufe und keine uncommitteten Änderungen." >&2
echo " Die Sitzung hat nichts hinterlassen — das ist NICHT der vierte Ausgang." >&2
return 1
fi
echo " ✓ Arbeit vorhanden (uncommittet und/oder Commit der Stufe)." >&2
# (2) BL-135: Gibt es eine ZUSICHERUNG? Genau der Punkt, an dem die
# Prüfliste für den Menschen blind war. Eine Stufe, die Produktivcode
# baut und keine einzige Testdatei berührt, ist nach den
# Verifikationsregeln dieses Projekts nicht fertig — egal wie grün der
# Baum ist, denn der bestehende Bestand deckt das Neue nicht ab.
local dateien
if [ -n "$uncommittet" ]; then
dateien="$(printf '%s\n' "$uncommittet" | sed 's/^...//')"
else
dateien="$(git show --name-only --pretty=format: HEAD 2>/dev/null)"
fi
if ! printf '%s\n' "$dateien" | grep -q "^\"\?${test_ordner}"; then
echo " ✗ Keine Datei unter ${test_ordner} berührt (BL-135)." >&2
echo " Die Stufe hat keine nachweisbare Zusicherung — grüner Baum beweist hier" >&2
echo " nichts, weil der Bestand das Neue nicht prüft. Das gehört an den Menschen." >&2
return 1
fi
echo " ✓ Zusicherung vorhanden — mindestens eine Datei unter ${test_ordner} berührt (BL-135)." >&2
# (3) Ist der Baum grün? Der teuerste, aber unverzichtbare Schritt — er
# ist derselbe Befehl, den die Prüfliste dem Menschen nennt.
if [ -z "$smoke" ]; then
echo " ✗ Kein TEAM_SMOKE_TEST konfiguriert — ohne Verifikationsbefehl wird nicht" >&2
echo " automatisch quittiert." >&2
return 1
fi
# BL-207: Kein zweiter Lauf neben einen laufenden stellen. Zwei
# gleichzeitige Testlaeufe kollidieren, und das Ergebnis waere eine
# Eigenschaft der MASCHINE statt eine des Codes.
if team_smoke_parallel_lauf; then
echo " ? Es läuft bereits ein Verifikationslauf — Ergebnis UNBEKANNT, nicht rot (BL-207)." >&2
echo " Gefunden: $TEAM_SMOKE_PARALLEL_ZEILE" >&2
echo " Ein zweiter Lauf daneben kollidiert (Datenbankdateien, Ports," >&2
echo " Nutzerverzeichnisse) und meldete ROT für einen Baum, der allein gefahren" >&2
echo " grün ist. Es wird deshalb NICHT automatisch quittiert und NICHTS behauptet." >&2