Skip to content

feat(bin): add weekly resource-governance gate for the work and home PC - #3570

Closed
Valentino-Sole wants to merge 9 commits into
kunchenguid:mainfrom
Valentino-Sole:fm/fm-ressourcen-steuerung
Closed

feat(bin): add weekly resource-governance gate for the work and home PC#3570
Valentino-Sole wants to merge 9 commits into
kunchenguid:mainfrom
Valentino-Sole:fm/fm-ressourcen-steuerung

Conversation

@Valentino-Sole

Copy link
Copy Markdown

Intent

Build fleet resource governance for firstmate's own dispatch: weekly clock-window and percentage caps per host (work PC / Arbeits-PC and home PC / Heim-PC), plus a GPU-exclusivity check between Qwen and the JARVIS voice worker on the home PC. Fresh scope, not a continuation of the parked, unmerged branch fm/fm-ressourcen-parallelitaet (PR #3208) - that branch's policy (Hetzner-3 cap, presence-based work-PC gating, Friday-evening reachability-gated home-PC routing) is superseded and materially different from the policy implemented here. Only the parked branch's bounded SSH-timeout helper pattern, POSIX-probe-then-Windows-probe dispatch shape (aimed at hosts Valentino/Valentino-Arbeit), and fail-closed-on-unreadable-measurement discipline were reused as reference; its slot-count formula, Hetzner-3/presence routing, and config/compute-hosts.json model were not carried over.

Policy implemented (captain's exact words): Arbeits-PC frei Mo-Fr 19:30-10:00 und am gesamten Wochenende (Fr 19:30 bis Mo 10:00 durchgehend), Mo-Fr 10:00-19:30 hoechstens 50 Prozent. Heim-PC frei Mo-Fr 04:00-19:00, sonst (Mo-Fr 19:00-04:00 und komplettes Wochenende) hoechstens 50 Prozent. Wenn Qwen auf dem Heim-PC laeuft ist die RTX 4080 fuer Qwen reserviert, keine weiteren GPU-Jobs dort; umgekehrt gilt dasselbe fuer die JARVIS-Stimme - Qwen und Stimme duerfen nie gleichzeitig auf der Karte laufen.

Binding clarifications from the captain, applied as accepted requirements:

  • The schedule is evaluated once against the authoritative Hetzner host's own clock forced into Europe/Berlin, never a remote (possibly wrong/drifted) host's own clock; only the resulting capped/uncapped state and percentage travel to whichever host is being gated.
  • The free/weekend span on the work PC runs continuously from Friday 19:30 through Monday 10:00 with no cap at all during that whole span.
  • Any unreadable measurement (load, GPU, port, remote reachability, clock) fails closed to 0 capacity / must-not-proceed, never guessed or defaulted permissive.
  • JARVIS-voice detection uses its gateway port (currently 7414, confirmed still current in data/learnings.md), never a process name - process-name detection has broken this fleet's integration three times before. Qwen detection avoids a bare fragile process-name match where a more stable signal is practical.
  • A durable manual override marker (analogous to state/.afk) lets the captain force the 50% cap immediately outside the scheduled window. Firstmate arms it the moment the captain says 'Kappung' in chat (case-insensitive; 'Kappung ' arms that host only, bare 'Kappung' arms both) and clears it on 'Kappung auf'. This is documented precisely enough for firstmate to set/clear the marker file directly; it is a captain-facing manual control, not new tool/skill plumbing.

Build order followed (each stage independently reviewable): (1) weekly clock-window gate per host producing per-host uncapped/capped state, standalone; (2) percentage-of-capacity cap (50%) applied when a host is in its capped window, as a reusable apply-percentage primitive rather than a new remote slot-measurement system (no existing dispatch mechanism to the work/home PCs exists on main to wire into); (3) GPU named-process-and-port exclusivity between Qwen and JARVIS voice on the home PC, built last, reusing the parked branch's proven bounded-SSH-timeout and dual-probe-dispatch shape as reference only; (4) the manual override marker wired into the same gate read so an armed override forces capped state immediately regardless of the clock-window computation.

Implementation note accepted during the build: live-testing the GPU probe against the real home host showed nvidia-smi --query-compute-apps lists every ordinary desktop GPU context on Windows/WDDM (not just genuine compute workloads) with no per-process memory field to filter the noise by, so per the captain's own guidance to avoid a fragile signal where a more stable one is practical, Qwen detection uses a named-process check (currently 'ollama', the live-confirmed identity) corroborated by aggregate GPU memory clearing a threshold, not per-process compute-app attribution.

Incidental fix carried in the same branch: adding the new tests/fm-resgate.test.sh file exposed a pre-existing, unrelated latent bug in bin/fm-test-run.sh's coverage guard - it sorts candidate lists with LC_ALL=C but then invoked bare comm (ambient locale) to compare them, which can disagree with the C-locale sort under a locale like en_US.UTF-8 and produce wrong results silently. Live-verified this was already latent independent of any specific test file (an existing filename pair was already out of ambient-locale order). Fixed by running every comm invocation in that function, and in its test's equivalent checks, under LC_ALL=C to match the sort that produced their inputs. This is the smallest downstream change needed to keep the already-accepted coverage-guard behavior correct given the new test file's addition.

What Changed

  • New bin/fm-resgate-lib.sh plus the bin/fm-resgate.sh CLI (schedule, cap, override set|clear|status, gpu status|allow) gate fleet use of the work PC (Valentino-Arbeit) and home PC (Valentino): the weekly clock windows are evaluated once against this host's own clock forced into Europe/Berlin, capped roles get a 50% capacity cap via a reusable apply-percentage primitive, and every unreadable measurement — clock, wrong-zone date fallback, SSH probe, port check, GPU query, or an invalid FM_RESGATE_VOICE_PORT / GPU_BUSY_MB / SSH_TIMEOUT — fails closed to 0% blocked or GPU unknown.
  • Home-PC GPU exclusivity between Qwen and the JARVIS voice worker resolves ownership from one bounded SSH round trip, deciding on the voice gateway port (7414) first and authoritatively so the voice worker can never block itself, and detecting Qwen by named process (ollama) corroborated by aggregate GPU memory rather than per-process compute-app attribution. A durable state/.resgate-cap-<role> marker, written atomically like state/.afk, forces the cap immediately but can only tighten the gate, never loosen it; docs/configuration.md, docs/scripts.md, and AGENTS.md document the surface and the case-insensitive "Kappung" / "Kappung auf" trigger with its release-form-first and longest-hostname-first matching.
  • tests/fm-resgate.test.sh covers the schedule boundaries, cap arithmetic, override behavior, timezone validation, and GPU precedence; adding it surfaced a latent locale bug in bin/fm-test-run.sh's coverage guard, where comm ran under the ambient locale against LC_ALL=C-sorted inputs — every such comm invocation there and in tests/fm-test-run.test.sh now runs under LC_ALL=C. .gitignore also drops .squish/.

Risk Assessment

✅ Low: Die Aenderung ist additiv und gut abgegrenzt (zwei neue Skripte plus Tests; die einzige Aenderung an vorhandenem Code ist der Locale-Fix an comm in fm-test-run.sh), jede neue Entscheidung faellt im Zweifel restriktiv aus, und die Zeitfenster, die Zonenpruefung, die Override-Praezedenz und die GPU-Eigentuemerlogik habe ich Fall fuer Fall gegen die woertliche Policy nachgerechnet, ohne einen falschen Wert oder ein falsches Label zu finden.

Testing

Ich habe die beiden vom Änderungssatz betroffenen Test-Dateien gezielt laufen lassen (tests/fm-resgate.test.sh 41/41 grün, tests/fm-documentation-audiences.test.sh grün, tests/fm-test-run.test.sh grün bis auf einen umgebungsbedingten Ruby-Fehlschlag), den Locale-Fix als Vorher/Nachher-Regression des Coverage-Guards reproduziert (vorher "comm: file 2 is not in sorted order", exit 1; nachher FM_TEST_COVERAGE ok, exit 0) und die eigentliche Funktion als Endnutzer über die CLI belegt: Wochenplan-Transkript für beide Rechner samt Fenstergrenzen, Kappung-Marker armen/freigeben mit Vorrang der Fail-closed-Regel, die Prozent-Primitive auf einen Roh-Slotbestand angewandt, ein Live-Lauf der GPU-Sonde gegen den echten Heim-PC über ssh (owner=qwen, "gpu allow voice" verweigert) sowie die Gegenrichtung und alle Ausfallfälle über einen ssh-Stellvertreter mit echter CRLF-Ausgabe. Bis auf den Ruby-Punkt ist alles grün und das Arbeitsverzeichnis ist sauber.

Evidence: Live-Beleg: GPU-Exklusivität gegen den echten Heim-PC (ssh Valentino, RTX 4080 Super)

Source: Live-Beleg: GPU-Exklusivität gegen den echten Heim-PC (ssh Valentino, RTX 4080 Super)

$ bin/fm-resgate.sh gpu status owner=qwen voice_port=no process(ollama)=yes gpu_used_mb=14896 gpu_busy_threshold_mb=4096 $ bin/fm-resgate.sh gpu allow qwen allow=yes reason=Qwen already holds the GPU exit=0 $ bin/fm-resgate.sh gpu allow voice allow=no reason=GPU is reserved for Qwen; JARVIS voice must not start exit=1

==============================================================================
 5) GPU-Exklusivitaet auf dem ECHTEN Heim-PC (ssh Valentino, RTX 4080 Super)
    Nur lesende Sonden: Get-NetTCPConnection (Port 7414), Get-Process ollama,
    nvidia-smi --query-gpu=memory.used - alles in EINEM begrenzten SSH-Roundtrip.
==============================================================================

$ bin/fm-resgate.sh gpu status
  owner=qwen
  voice_port=no
  process(ollama)=yes
  gpu_used_mb=14896
  gpu_busy_threshold_mb=4096

$ bin/fm-resgate.sh gpu allow qwen; echo "exit=$?"
  allow=yes
  reason=Qwen already holds the GPU
  exit=0

$ bin/fm-resgate.sh gpu allow voice; echo "exit=$?"
  allow=no
  reason=GPU is reserved for Qwen; JARVIS voice must not start
  exit=1
Evidence: Wochenplan beider Rechner plus die Prozent-Primitive auf Roh-Slots

Source: Wochenplan beider Rechner plus die Prozent-Primitive auf Roh-Slots

Arbeits-PC (work) Mo 09:59 100% outside Mo-Fr 10:00-19:30 on the work PC Mo 10:00 50% Mo-Fr 10:00-19:30 on the work PC (captain working hours) Mo 19:29 50% Mo-Fr 10:00-19:30 on the work PC (captain working hours) Mo 19:30 100% outside Mo-Fr 10:00-19:30 on the work PC Fr 19:29 50% Mo-Fr 10:00-19:30 on the work PC (captain working hours) Fr 19:30 100% outside ... Sa 12:00 100% So 12:00 100% Mo 09:59 100% Heim-PC (home) Mo 03:59 50% outside Mo-Fr 04:00-19:00 on the home PC Mo 04:00 100% Mo-Fr 04:00-19:00 on the home PC Di 18:59 100% Mo-Fr 04:00-19:00 on the home PC Di 19:00 50% outside Mo-Fr 04:00-19:00 on the home PC Sa 10:00 50% So 15:00 50% Prozent-Primitive, 7 Roh-Slots: work Di 14:00 -> 50% -> 3 | work Di 21:00 -> 100% -> 7 | home So 14:00 -> 50% -> 3

==============================================================================
 firstmate fleet resource governance - captain-facing CLI transcript
 repo: firstmate   branch: fm/fm-ressourcen-steuerung
 Every reading below comes from bin/fm-resgate.sh; the clock is pinned with
 FM_RESGATE_NOW_OVERRIDE="<dow 1=Mo..7=So> HH MM" so the whole week is shown.
==============================================================================

--- 1) Arbeits-PC (work): frei Mo-Fr 19:30-10:00 und das ganze Wochenende,
---    Mo-Fr 10:00-19:30 hoechstens 50 Prozent

Zeitpunkt              Kapazit. Grund
---------------------- -------- ------------------------------------------
Mo 09:59               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Mo 10:00               50%      Mo-Fr 10:00-19:30 on the work PC (captain working hours)
Mo 14:00               50%      Mo-Fr 10:00-19:30 on the work PC (captain working hours)
Mo 19:29               50%      Mo-Fr 10:00-19:30 on the work PC (captain working hours)
Mo 19:30               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Mo 23:00               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Mi 03:00               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Fr 19:29               50%      Mo-Fr 10:00-19:30 on the work PC (captain working hours)
Fr 19:30               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Fr 23:59               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Sa 12:00               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
So 12:00               100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
Mo 09:59 (Wochenende endet) 100%     outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)

--- 2) Heim-PC (home): frei Mo-Fr 04:00-19:00, sonst hoechstens 50 Prozent

Zeitpunkt              Kapazit. Grund
---------------------- -------- ------------------------------------------
Mo 03:59               50%      outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)
Mo 04:00               100%     Mo-Fr 04:00-19:00 on the home PC
Di 12:00               100%     Mo-Fr 04:00-19:00 on the home PC
Di 18:59               100%     Mo-Fr 04:00-19:00 on the home PC
Di 19:00               50%      outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)
Mi 01:00               50%      outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)
Fr 20:00               50%      outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)
Sa 10:00               50%      outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)
So 15:00               50%      outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)

==============================================================================
 2b) Die wiederverwendbare Prozent-Primitive auf einen Rohbestand angewandt
     fm_resgate_capacity_pct + fm_resgate_apply_pct (bin/fm-resgate-lib.sh)
==============================================================================

Rolle        Zeitpunkt              Proz.  von 7 Roh-Slots erlaubt
-----------  ---------------------- -----  -----------------------
work         Di 14:00               50%    3
work         Di 21:00               100%   7
work         Sa 14:00               100%   7
home         Di 14:00               100%   7
home         Di 21:00               50%    3
home         So 14:00               50%    3

  (Ganzzahlige Abrundung: 50 Prozent von 7 sind 3, nie 3,5 oder 4.)
Evidence: Kappung-Marker armen/freigeben und Fail-closed bei unlesbarer Uhr

Source: Kappung-Marker armen/freigeben und Fail-closed bei unlesbarer Uhr

Di 08:00 ohne Marker: cap work -> pct=100 uncapped | cap home -> pct=100 uncapped $ bin/fm-resgate.sh override set both (exit 0) $ ls -a state/ .resgate-cap-home .resgate-cap-work $ cat state/.resgate-cap-work armed_at=2026-09-03T00:56:54+0200 / note=Kappung Di 08:00 mit Marker: cap work -> pct=50 capped (manual override armed) | cap home -> pct=50 capped "Kappung auf Valentino-Arbeit": work=clear/home=armed -> cap work 100%, cap home 50% "Kappung auf": work=clear home=clear TZDIR=/nonexistent (Europe/Berlin nicht aufloesbar): pct=0 / state=blocked / reason=authoritative clock unreadable; refusing to guess the schedule window ... und mit armiertem Marker weiterhin pct=0, nie 50 (der Override kann nur verschaerfen)

==============================================================================
 3) Manueller Override - Captain sagt "Kappung" / "Kappung auf" im Chat
    Marker: state/.resgate-cap-work / state/.resgate-cap-home (wie state/.afk)
==============================================================================

Ausgangslage: Dienstag 08:00 - beide Rechner ausserhalb ihrer Kappungsfenster.
  cap work -> pct=100 state=uncapped reason=outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend) 
  cap home -> pct=100 state=uncapped reason=Mo-Fr 04:00-19:00 on the home PC 

$ # Captain: "Kappung"  -> firstmate armt beide Marker
$ bin/fm-resgate.sh override set both
  (exit 0)
$ bin/fm-resgate.sh override status both
  work=armed
  home=armed
$ ls -a state/
  .resgate-cap-home
  .resgate-cap-work
$ cat state/.resgate-cap-work
  armed_at=2026-09-03T00:56:54+0200
  note=Kappung

Gleicher Zeitpunkt, Dienstag 08:00 - sofort gekappt, unabhaengig vom Fenster:
  cap work -> pct=50 state=capped reason=manual override armed (Kappung); forced capped regardless of the clock window 
  cap home -> pct=50 state=capped reason=manual override armed (Kappung); forced capped regardless of the clock window 

$ # Captain: "Kappung auf Valentino-Arbeit" -> nur der Arbeits-PC wird freigegeben
$ bin/fm-resgate.sh override clear work
  (exit 0)
  work=clear
  home=armed
  cap work -> pct=100 state=uncapped reason=outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend) 
  cap home -> pct=50 state=capped reason=manual override armed (Kappung); forced capped regardless of the clock window 

$ # Captain: "Kappung auf" -> beide frei
  (exit 0)
  work=clear
  home=clear

==============================================================================
 4) Fail-closed: unlesbare Uhr / nicht aufloesbare Zeitzone -> 0 Prozent
==============================================================================

$ TZDIR=/nonexistent bin/fm-resgate.sh cap work    # Europe/Berlin nicht aufloesbar
  pct=0
  state=blocked
  reason=authoritative clock unreadable; refusing to guess the schedule window

$ # Selbst mit armiertem "Kappung" bleibt eine unlesbare Uhr bei 0%, nie bei 50%
  pct=0
  state=blocked
  reason=authoritative clock unreadable; refusing to guess the schedule window

$ # Echte Uhr, echte Zone (dieser Hetzner-Host, nach Europe/Berlin gezwungen):
  jetzt in Berlin: Thu 2026-09-03 00:56 CEST +0200
  pct=100
  state=uncapped
  reason=outside Mo-Fr 10:00-19:30 on the work PC (evening, night, or weekend)
  pct=50
  state=capped
  reason=outside Mo-Fr 04:00-19:00 on the home PC (evening, night, or weekend)
Evidence: GPU-Gegenrichtung und Ausfallszenarien (ssh-Stellvertreter mit echter CRLF-Ausgabe)

Source: GPU-Gegenrichtung und Ausfallszenarien (ssh-Stellvertreter mit echter CRLF-Ausgabe)

Stimme haelt die Karte (Port 7414 lauscht): owner=voice gpu allow qwen -> exit=1 GPU is reserved for JARVIS voice; Qwen must not start gpu allow voice -> exit=0 JARVIS voice already holds the GPU Karte frei (kein ollama, 812 MiB): owner=none -> beide exit=0 "GPU is free" nvidia-smi-Messung fehlgeschlagen: owner=unknown -> beide exit=1 "could not be measured; refusing rather than guessing" Heim-PC nicht erreichbar (ssh exit 255): owner=unknown -> beide exit=1

==============================================================================
 6) Die Gegenrichtung und der Ausfall - Heim-PC durch einen Stellvertreter
    ersetzt (PATH-Shim fuer ssh), der exakt die PowerShell-Ausgabe des echten
    Rechners liefert, inklusive CRLF. Die JARVIS-Stimme laesst sich auf dem
    echten Rechner nicht gefahrlos starten, nur um sie zu messen.
==============================================================================

--- Szenario: Stimme haelt die Karte (Port 7414 lauscht)
  owner=voice
  voice_port=yes
  process(ollama)=unknown
  gpu_used_mb=11000
  gpu_busy_threshold_mb=4096
  gpu allow qwen  -> exit=1  GPU is reserved for JARVIS voice; Qwen must not start
  gpu allow voice -> exit=0  JARVIS voice already holds the GPU

--- Szenario: Karte frei (kein ollama, 812 MiB)
  owner=none
  voice_port=no
  process(ollama)=no
  gpu_used_mb=812
  gpu_busy_threshold_mb=4096
  gpu allow qwen  -> exit=0  GPU is free
  gpu allow voice -> exit=0  GPU is free

--- Szenario: nvidia-smi-Messung fehlgeschlagen
  owner=unknown
  voice_port=no
  process(ollama)=yes
  gpu_used_mb=unknown
  gpu_busy_threshold_mb=4096
  gpu allow qwen  -> exit=1  GPU ownership could not be measured; refusing rather than guessing
  gpu allow voice -> exit=1  GPU ownership could not be measured; refusing rather than guessing

--- Szenario: Heim-PC nicht erreichbar (ssh exit 255)
  owner=unknown
  voice_port=unknown
  process(ollama)=unknown
  gpu_used_mb=unknown
  gpu_busy_threshold_mb=4096
  gpu allow qwen  -> exit=1  GPU ownership could not be measured; refusing rather than guessing
  gpu allow voice -> exit=1  GPU ownership could not be measured; refusing rather than guessing
Evidence: Locale-Fix als Regression: Coverage-Guard vorher (fehlerhaft) und nachher (sauber)

Source: Locale-Fix als Regression: Coverage-Guard vorher (fehlerhaft) und nachher (sauber)

### VORHER (bin/fm-test-run.sh @ 67ba5f9, LANG=en_US.UTF-8) $ bash bin/fm-test-run.sh --check-coverage comm: file 2 is not in sorted order comm: input is not in sorted order exit=1 ### NACHHER (bin/fm-test-run.sh @ HEAD, identische Umgebung) $ bash bin/fm-test-run.sh --check-coverage FM_TEST_COVERAGE ok total=177 parallel=24 serial=141 serial_shards=5 serial_unhinted=1 herdr=12 exit=0

### BEFORE the locale fix (bin/fm-test-run.sh @ 67ba5f9, ambient LANG=en_US.UTF-8)
$ bash bin/fm-test-run.sh --check-coverage
comm: file 2 is not in sorted order
comm: input is not in sorted order
exit=1
- Outcome: ⚠️ 1 warning across 1 run (8m34s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-resgate-lib.sh:474 - Der aggregierte VRAM-Wert wird ausschliesslich Qwen zugerechnet, obwohl er nicht sagen kann, wer die Karte belegt. Konkret reproduziert: voice_port=listening (Stimme haelt die Karte), gpu_process=running (der ollama-Dienst laeuft gestartet-aber-leerlaufend - genau der Fall, den der Datei-Header selbst als Grund fuer die Korroboration nennt) und gpu_used_mb=6100 (der VRAM der Stimme). qwen_active wird dadurch auf yes gesetzt, FM_RESGATE_GPU_OWNER meldet 'conflict', und 'fm-resgate.sh gpu allow voice' verweigert mit rc=1 genau der Arbeitslast das Weiterlaufen, die die Karte rechtmaessig haelt - mit der falschen Begruendung, beide seien gleichzeitig aktiv. Da die Stimme mit geladenem Modell praktisch immer ueber der 4096-MiB-Schwelle liegt und der ollama-Dienst auf Windows dauerhaft laeuft, ist das der wahrscheinliche Normalzustand, nicht ein Randfall. Das Ergebnis ist restriktiv, also nicht unsicher, aber Label und Entscheidung sind falsch. Vorschlag zur Klaerung mit dem Captain: bei listening-Port den Aggregatspeicher nicht mehr als Qwen-Korroboration werten, oder eine Qwen-eigene Speicherzuordnung beschaffen - beides aendert die im Intent akzeptierte Erkennungsstrategie und ist daher eine Entscheidung des Captains.
  • ℹ️ bin/fm-resgate-lib.sh:328 - FM_RESGATE_SSH_TIMEOUT ist der einzige Stellwert, der eine Messung erreicht, ohne validiert zu werden, obwohl der Datei-Header (Zeile 120-123) genau diese Invariante fuer sich beansprucht: 'Every tunable that reaches a measurement is validated before it is used'. bound=$((${FM_RESGATE_SSH_TIMEOUT:-5} + 5)) ergibt bei einem negativen Pin (z. B. -5) den Wert 0, und bin/fm-timeout-lib.sh dokumentiert im Kopf ausdruecklich, dass 0 kein Bound ist ('timeout 0 ... disable the deadline, so callers must reject 0 before calling'). Praktisch faellt der Probe hier trotzdem geschlossen aus, weil ssh das ungueltige ConnectTimeout selbst ablehnt; die Luecke ist also die verletzte Zusage, nicht ein erreichbar permissives Ergebnis. Behebung analog zu Port und Schwellwert: fm_resgate_is_uint auf FM_RESGATE_SSH_TIMEOUT anwenden und bei ungueltigem Wert auf den Default 5 zurueckfallen.

🔧 Fix: decide GPU owner by voice port first, validate ssh timeout
1 info still open:

  • ℹ️ AGENTS.md:134 - Der Ausloeser "Kappung" steht in AGENTS.md genau einmal - als Zeile in der state/-Dateiliste in Abschnitt 2 "Layout and state". Der Intent verlangt aber ein Verhalten: "Firstmate arms it the moment the captain says 'Kappung' in chat (case-insensitive; 'Kappung <hostname>' arms that host only, bare 'Kappung' arms both) and clears it on 'Kappung auf'." Der einzige vergleichbare Captain-Wort-Ausloeser dieser Flotte steht dagegen in einem Verhaltensabschnitt: AGENTS.md:436, "Invoke the /afk skill when the captain says /afk, says they are going afk, state/.afk exists, ..." unter 8. Supervision protocol. docs/configuration.md beschreibt die Wortabgleich-Regeln vollstaendig und korrekt (Release-Form zuerst, Hostnamen laengste zuerst), wird aber nicht bei jeder Sitzung geladen - AGENTS.md schon. Konkrete Folge, wenn der Ausloeser nicht zuendet: der Captain sagt am Mittwoch 09:50 "Kappung Valentino-Arbeit", niemand legt state/.resgate-cap-work an, und der Arbeits-PC laeuft bis 10:00 sowieso und danach nur nach Uhrzeit - die sofortige Kappung ausserhalb des Fensters, also genau der Zweck des Markers, bleibt aus, ohne dass irgendetwas fehlschlaegt. Der Code selbst ist einwandfrei; es geht nur um die Platzierung. Vorschlag zur Entscheidung des Captains: die zwei Saetze zusaetzlich in einen Verhaltensabschnitt aufnehmen (9. Escalation and captain etiquette oder neben den /afk-Ausloeser in Abschnitt 8), die Inventarzeile als Dateibeschreibung belassen. Da der Intent ausdruecklich "documented precisely enough ... not new tool/skill plumbing" festlegt, ist die Platzierung eine bewusste Entscheidung des Autors und keine, die ich hier selbst umstellen sollte.

🔧 Fix: document the Kappung trigger as a behavioral instruction
1 info still open:

  • ℹ️ bin/fm-resgate-lib.sh:500 - Der Transport-Fehler der GPU-Sonde wird vollstaendig verschluckt: out=$(fm_resgate_ssh_raw ... 2&gt;/dev/null) || out= wirft die ssh-Diagnose weg, und bin/fm-resgate.sh:102 faengt den Rueckgabewert mit || true ab. Konkreter Ablauf: der Hostkey des Heim-PCs aendert sich, ssh schreibt 'Host key verification failed' nach stderr und beendet mit 255. Der Captain sieht von bin/fm-resgate.sh gpu status genau owner=unknown, voice_port=unknown, process(ollama)=unknown, gpu_used_mb=unknown - dieselbe Ausgabe wie bei einem ausgeschalteten Rechner, einem fehlenden nvidia-smi oder einem vertippten FM_RESGATE_VOICE_PORT. Der Zustand ist korrekt (fail-closed, beide Arbeitslasten werden verweigert), aber es gibt nirgends einen Hinweis auf die Ursache, waehrend ab da jeder gpu allow fehlschlaegt. Das steht im Gegensatz zur ausdruecklichen Sorgfalt derselben Datei an anderer Stelle: der Kopf von fm_resgate_home_gpu_probe_cmd begruendet ausfuehrlich, warum jede Einzelmessung probe-failed statt eines stillen Negativs meldet, damit 'diese Messung schlug fehl' von 'keine Ausgabe' unterscheidbar bleibt - genau diese Unterscheidung fehlt auf der Transportebene. Kleinster Fix: in cmd_gpu status die stderr der Sonde durchreichen oder eine zusaetzliche Zeile (etwa probe=ssh-failed rc=&lt;n&gt;) ausgeben, statt sie zu verwerfen; die Entscheidungslogik bleibt unveraendert.
⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-test-run.test.sh:1126 - tests/fm-test-run.test.sh scheitert lokal an test_herdr_ci_family_run_has_a_step_timeout mit "ruby is required to parse .github/workflows/ci.yml as YAML", weil in dieser Sandbox kein Ruby installiert ist. Der Test ist von dieser Änderung unberührt und scheitert identisch auf dem Basis-Commit d22318e; ein Systempaket nachzuinstallieren liegt ausserhalb der erlaubten Arbeitsgrenze. Die beiden vom Locale-Fix tatsächlich geänderten Prüfungen derselben Datei laufen grün. Die Abdeckung dieses Punktes gehört damit der CI.
  • bash bin/fm-test-run.sh tests/fm-resgate.test.sh — 41/41 ok, exit=0
  • bash bin/fm-test-run.sh tests/fm-test-run.test.sh — die beiden vom Locale-Fix berührten Prüfungen (test_list_all_exact_suite_coverage, test_portable_shard_union_and_coverage_guard) grün; ein umgebungsbedingter Fehlschlag (ruby fehlt)
  • bash bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh — grün, deckt die neuen Einträge in docs/scripts.md und docs/configuration.md ab
  • Regression Vorher/Nachher: git show 67ba5f9:bin/fm-test-run.sh &gt; bin/.prefix-fm-test-run.sh &amp;&amp; bash bin/.prefix-fm-test-run.sh --check-coverage (exit 1, "comm: file 2 is not in sorted order") gegen bash bin/fm-test-run.sh --check-coverage (exit 0, FM_TEST_COVERAGE ok total=177), beide unter LANG=en_US.UTF-8
  • sort -c auf der LC_ALL=C-sortierten Testdatei-Liste: bestätigt die Locale-Kollisionsabweichung (disorder bei tests/fm-backend-herdr.test.sh)
  • Wochenplan-Transkript: FM_RESGATE_NOW_OVERRIDE=&#34;&lt;dow&gt; HH MM&#34; bin/fm-resgate.sh cap work|home an 22 Zeitpunkten inklusive der Grenzen 10:00/19:30 und 04:00/19:00 sowie der Spanne Fr 19:30 bis Mo 10:00
  • Kappung-Ablauf: bin/fm-resgate.sh override set|status|clear work|home|both mit anschliessendem cap, plus cat state/.resgate-cap-work
  • Fail-closed-Uhr: env TZDIR=/nonexistent bin/fm-resgate.sh cap work → pct=0/blocked, auch bei armiertem Override
  • Prozent-Primitive: fm_resgate_capacity_pct + fm_resgate_apply_pct 7 &lt;pct&gt; über bin/fm-resgate-lib.sh an sechs Zeitpunkten (50 % von 7 = 3, abgerundet)
  • Live gegen den echten Heim-PC: bin/fm-resgate.sh gpu status, gpu allow qwen (exit 0), gpu allow voice (exit 1) über ssh Valentino, nur lesende Sonden
  • GPU-Szenarien über PATH-Shim mit echter CRLF-Ausgabe: Stimme lauscht auf 7414 (Qwen abgewiesen), Karte frei, nvidia-smi-Fehler, ssh exit 255 — beide letzteren verweigern beide Seiten
⚠️ **Document** - 1 info
  • ℹ️ docs/fm-test-portable-shards.md:83 - Adding tests/fm-resgate.test.sh grew the derived portable-serial lane, so this page's shard-balance record is further out of date: it states the lane's 139 scripts with per-shard counts 27/27/28/28/29 and ~761980 ms weights, while the lane now holds 141 scripts partitioned 27/29/30/26/29 at ~770410-770419 ms (bin/fm-test-run.sh --check-coverage reports serial=141, serial_unhinted=1). One of the two extra scripts predates this branch. I did not hand-edit the table: it is dated maintainer-verification evidence whose own refresh procedure requires fm-test-timing-portable-serial-* artifacts from several green CI runs that include the new test, which do not exist yet, and rewriting the counts alone would leave the following replay sentence ("worst shard at 12.54 min") describing a partition nobody replayed. Follow-up: after this branch is green on CI, refresh the hints with the documented gh run download procedure and update the table.
🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: no lint changes needed; lint passes with actionlint installed
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

Valentino-Sole and others added 8 commits September 2, 2026 22:37
Implements the captain's clock-window + percentage-cap policy for the
Arbeits-PC and Heim-PC, a durable "Kappung"/"Kappung auf" manual override
marker, and GPU exclusivity between Qwen and JARVIS voice on the home PC.
Fresh scope per the fm-ressourcen-scout report: the parked, unmerged
fm/fm-ressourcen-parallelitaet branch (PR kunchenguid#3208) carries a materially
different, superseded policy and is not touched or built upon; only its
proven SSH-timeout and POSIX/Windows probe-dispatch shape is reused as
reference.

The schedule is evaluated once against this host's own clock forced into
Europe/Berlin, never a remote host's possibly-wrong clock. Every
unreadable measurement (clock, SSH probe, port, GPU reading) fails closed
to the most restrictive answer.

Qwen detection was redesigned during implementation after a live probe
against the real home host showed nvidia-smi --query-compute-apps lists
every ordinary desktop GPU context on Windows/WDDM, not just genuine
compute workloads, with no per-process memory field to filter it by; a
named-process check corroborated by aggregate GPU memory is used instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zxi25rQx7a53vRK6u3b7D
… marker

Adds a "Fleet resource governance" section to docs/configuration.md
covering bin/fm-resgate.sh usage, the state/.resgate-cap-<role> manual
override marker and its "Kappung"/"Kappung auf" trigger words, the
fail-closed rules, and the GPU-exclusivity detection signals. Registers
both new bin/ scripts in docs/scripts.md and adds the one-line state-file
pointer to AGENTS.md's layout list, matching the existing state/.afk entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zxi25rQx7a53vRK6u3b7D
bin/fm-test-run.sh's coverage guard (and its own test's equivalent
checks) sort candidate lists with LC_ALL=C before comparing them with
comm, but invoked comm itself under the process's ambient locale. On a
host whose locale collates filenames differently from the C locale (this
sandbox is en_US.UTF-8), comm's own "is this sorted" check can disagree
with the sort that produced the file, printing "not in sorted order" and
silently returning wrong set-difference/intersection results.

Live-verified: `sort -c` under this environment's ambient locale already
finds tests/fm-backend.test.sh out of C-locale order among unrelated,
pre-existing filenames, so this was a latent bug independent of any
specific test file - adding tests/fm-resgate.test.sh in the prior commit
only changed which comm comparison happened to be evaluated and expose
it. Every comm invocation in bin/fm-test-run.sh's coverage guard and in
tests/fm-test-run.test.sh's equivalent checks is now run under LC_ALL=C
to match the sort that produced its inputs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zxi25rQx7a53vRK6u3b7D
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge because the previously reported partial-override ambiguity has been resolved and no blocking failure remains.

No blocking failure remains.

Reviews (2): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile

Comment thread bin/fm-resgate.sh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant