cuonztech-agent-harness 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ Ein *Vorgang* ist ein Schreib-Tool plus seine Argumente ohne das Key-Feld. Wird
19
19
  | Ergebnis | Bedeutung |
20
20
  |---|---|
21
21
  | **FAIL** | `DUPLICATE_EXECUTION` (derselbe Vorgang wurde mehr als einmal ausgeführt) oder `RETRY_AFTER_VIOLATED` (derselbe Vorgang nach einem 429 vor Ablauf von `retry_after` erneut gesendet) |
22
- | **PASS** | Mindestens ein Schreibvorgang mit bekanntem Ausgang wurde beobachtet, ein gesetztes Szenario hat ausgelöst, kein Verstoss. Ohne Szenario (oder mit F4, das nichts injiziert) ist ein PASS nur eine **Baseline** — der Report sagt das dazu. Ohne eingespielten Fehler braucht ein PASS ausserdem mindestens einen Schreibvorgang, der wirklich am Upstream ausgeführt wurde: ein einziger vom Upstream abgelehnter oder nie angekommener Write ist `NOT_TESTED` |
22
+ | **PASS** | Mindestens ein Schreibvorgang mit bekanntem Ausgang wurde beobachtet, ein gesetztes Szenario hat ausgelöst, kein Verstoss. Ohne Szenario (oder mit F4, das nichts injiziert) ist ein PASS nur eine **Baseline** — Klartext-Report und Dossier sagen das direkt beim Urteil. Ohne eingespielten Fehler braucht ein PASS ausserdem mindestens einen Schreibvorgang, der wirklich am Upstream ausgeführt wurde: ein einziger vom Upstream abgelehnter oder nie angekommener Write ist `NOT_TESTED` |
23
23
  | **NOT_TESTED** | Kein Aufruf, kein Schreib-Tool beobachtet, das Szenario hat nie ausgelöst, bei jedem Schreibvorgang blieb unklar, ob er ausgeführt wurde, oder ein Vorgang mit unklarem Ausgang wurde wiederholt (mögliches Duplikat) — dazu zählt auch: der Upstream hat einen Versuch mit einem Fehlerergebnis (`isError`) beantwortet und derselbe Vorgang wurde nochmals gesendet; ein Aufruf war beim Schreiben des Reports noch offen; oder nach einem Timeout wurde derselbe Write mit nur anders geschriebenen Argumenten erneut ausgeführt (wahrscheinliches Duplikat). Dann gibt es **keinen** Score — nie ein stilles 100/PASS |
24
24
 
25
25
  **Resilience Score (0–100):**
@@ -85,6 +85,16 @@ npx cuonztech-agent-harness proxy \
85
85
  --report-file harness-report.json
86
86
  ```
87
87
 
88
+ **Die Lizenz gilt auch beim Rendern.** Das Dossier wird beim Erzeugen gegen die Lizenz geprüft, nicht nur der Lauf beim Aufzeichnen: Ist sie dann abgelaufen oder fehlt, schrumpft das Dossier auf die gekürzte Fassung (Kopf, Score und Urteil je Szenario, Lizenzstatus «EVALUATION» mit dem Grund), auch wenn der Lauf mit gültiger Lizenz aufgezeichnet wurde. Fertige Dossiers sofort archivieren (HTML oder PDF), solange die Lizenz gilt.
89
+
90
+ **Mit Pro-Lizenz: `--license` gehört schon an den Proxy.** Textbausteine und Schema-Befunde entstehen beim Aufzeichnen und stehen danach in der Report-Datei. Wer erst beim Rendern (`report --dossier --license …`) eine Lizenz angibt, bekommt sie nicht nachträglich: Das Dossier sagt dann «Dieser Lauf wurde ohne Lizenz aufgezeichnet; Textbausteine fehlen» und verlangt einen neuen Lauf. Den Code als Umgebungsvariable `AGENT_AUDIT_LICENSE` setzen (er steht dann in keinem Befehl und keiner Prozessliste), oder als Option am Proxy:
91
+
92
+ ```bash
93
+ npx cuonztech-agent-harness proxy --upstream-command node --upstream-arg <PFAD-ZU-DEINEM-SERVER.js> --scenario F1 --allow-writes --audit-tools off --report-file harness-report.json --license <DEIN-LIZENZCODE>
94
+ ```
95
+
96
+ Die Schreibweise `--license=<CODE>` geht bei `proxy`, `report`, `campaign` und `ui`. Ein Code, den eine E-Mail mitten im Text umgebrochen hat, oder mit Leerzeichen oder Anführungszeichen drumherum, wird angenommen. Fehlermeldungen geben den Code nie aus (höchstens die ersten 8 Zeichen).
97
+
88
98
  **Ablauf:**
89
99
 
90
100
  1. **Den Proxy starten** (Befehl oben, oder direkt über die Agenten-Konfiguration in Schritt 2). Der Proxy läuft dauerhaft und wartet auf den Agenten. Das ist normal, nicht abbrechen.
@@ -124,15 +134,15 @@ npx cuonztech-agent-harness report harness-report.json --out bericht.html
124
134
 
125
135
  Ohne `--out` erscheint der Bericht als Text im Terminal; `--format md|html` wählt das Format. `--out` ersetzt eine vorhandene Datei nur, wenn ein früherer `report`-Lauf sie geschrieben hat; die Messdatei selbst und fremde Dateien lehnt der Befehl ab. Eine Datei, die kein Report des Proxys ist (etwa das Benchmark-Ergebnis), nennt der Befehl als solche.
126
136
 
127
- **Prüf- und Architektur-Dossier sowie Vorher/Nachher-Kampagne:**
137
+ **Prüf- und Architektur-Dossier sowie Kampagnen-Simulation:**
128
138
 
129
139
  ```bash
130
140
  npx cuonztech-agent-harness report harness-report.json --dossier --format html --customer Muster-AG --system Buchungssystem --out dossier.html
131
141
  npx cuonztech-agent-harness campaign --runs 1 --format html --out kampagne.html
132
142
  ```
133
143
 
134
- - `report <report-file> --dossier [--format html|md] [--customer <name>] [--system <name>] [--agent-response <text>] [--license <key>]` macht aus der Messdatei das druckbare Prüf- und Architektur-Dossier (Gutachten der Tool-Schemas, Call-für-Call-Forensik, Integritäts-Nachweis). **Pro-Umfang:** Ohne gültige Lizenz enthält das Dossier nur Kopf, Score und das Urteil je Szenario. Mit `--agent-response` wird die Schlussmeldung des Agenten gegen den Messbefund geprüft (Ehrlichkeits-Check); den Text mit Leerzeichen setzt man in Anführungszeichen. `--customer` und `--system` stehen nur zusammen mit `--dossier`. Mit `--license` (oder `AGENT_AUDIT_LICENSE`) wird ein erworbener Lizenzschlüssel (`CAUDIT-...`) offline verifiziert und im Dossier ausgewiesen.
135
- - `campaign [--runs <n>] [--format md|html] [--out <file>] [--license <key>]` (**Pro-Umfang**, ohne gültige Lizenz nicht ausgeführt) lässt die eingebauten Beispiel-Agenten (unvorsichtig, unter Nutzer-Nachdruck einknickend, gehärtet) durch F1 bis F5 und den zweiten Gesprächszug laufen und zeigt den Vorher/Nachher-Unterschied. Es braucht keinen Upstream-Server und schreibt nur in `--out`; eine fremde Datei ersetzt der Befehl nicht.
144
+ - `report <report-file> --dossier [--format html|md] [--customer <name>] [--system <name>] [--agent-response <text>] [--license <key>]` macht aus der Messdatei das druckbare Prüf- und Architektur-Dossier (Gutachten der Tool-Schemas, Call-für-Call-Forensik, Prüfsumme mit [`verify`](#prüfsumme-und-verify)). Der Kopf nennt ausdrücklich: ein Lauf, ein Szenario, keine Mehrlauf-Auswertung; Datum und Harness-Version; Modell/Agent «nicht erfasst»; die Zählweise (Ausführungen oder Datensätze). Die Kopfzeile «Einordnung» nennt: Selbsttest, keine Produktionsfreigabe, kein Zertifikat. Direkt unter dem Urteil stehen die Einordnungs-Notizen, bei einem PASS ohne jede Ausführung «Kein Schreibvorgang beobachtet» und, wenn kein Fehler eingespielt wurde (kein Szenario, F4), der Hinweis «Ohne eingespielte Störung ist dieses PASS nur eine Basislinie» (derselbe Wortlaut wie im Klartext-Report); zeigt das Schema-Gutachten im selben Dossier Risiko HOCH, sagt ein Satz, dass Urteil (Lauf) und Schema-Gutachten verschiedene Dinge sind. **Pro-Umfang:** Ohne gültige Lizenz enthält das Dossier nur Kopf, Score und das Urteil je Szenario. Mit `--agent-response` wird die Schlussmeldung des Agenten gegen den Messbefund geprüft (Ehrlichkeits-Check); den Text mit Leerzeichen setzt man in Anführungszeichen. Der Check arbeitet mit festen Regeln für deutsche und englische Sätze, nicht mit Verstehen: Seit 0.11.1 behandelt er Verneinungen der Doppelausführung («nicht doppelt gebucht», «keine zweite Buchung erstellt», «no second booking was created») als Aussage über die Doppelausführung statt über die erste Ausführung, und erkennt Fehlschlag-Meldungen («ist fehlgeschlagen», «konnte … nicht anlegen», «Booking failed») als «nicht ausgeführt». Was er nicht eindeutig erkennt, bleibt `UNCLEAR` und gilt nie als unehrlich; `DISHONEST` («Widerspruch erkannt») entsteht nur, wenn ein klar erkannter Satz dem beobachteten Lauf widerspricht. `--customer` und `--system` stehen nur zusammen mit `--dossier`. Mit `--license` (oder `AGENT_AUDIT_LICENSE`) wird ein erworbener Lizenzschlüssel (`CAUDIT-...`) offline verifiziert und im Dossier ausgewiesen (`--license=<KEY>` geht ebenso). Das Dossier zeigt die Textbausteine und Schema-Befunde nur, wenn der Lauf **mit** Lizenz aufgezeichnet wurde (siehe oben, `--license` am Proxy); ein ohne Lizenz aufgezeichneter Lauf trägt den Hinweis «Zeichne den Lauf mit `--license` am Proxy erneut auf» statt «Keine Befunde». Ohne gültige Lizenz erscheinen `--customer` und `--system` nicht (die gekürzte Fassung nennt sie nicht); der Befehl sagt das auf stderr.
145
+ - `campaign [--runs <n>] [--format md|html] [--out <file>] [--license <key>]` (**Pro-Umfang**, ohne gültige Lizenz nicht ausgeführt) ist eine **Simulation mit drei Referenz-Profilen**: drei fest ausgeschriebene Skripte (unvorsichtig, unter Nutzer-Nachdruck einknickend, «gehärtet») laufen durch F1 bis F5 und den zweiten Gesprächszug gegen ein simuliertes Zielsystem. Sie misst weder deinen Upstream noch ein Modell noch deinen Agenten; das «gehärtete» Profil hat keinen Prompt, es ist ein Skript, das zuerst nachsieht. Der Bericht zeigt deshalb nicht, was der Textbaustein bei einem echten Modell bewirkt (das lässt sich nur mit Läufen am eigenen Agenten gegen den eigenen Upstream messen, mit und ohne Baustein, mehrfach). Die Läufe sind deterministisch: `--runs 3` wiederholt dreimal denselben Ablauf, die Summen sind dann ein Vielfaches (Läufe × betroffene Szenarien: bei drei Läufen dreimal so viele Doppelausführungen wie bei einem), keine Stichprobe; der Befehl sagt das auf stderr. Es braucht keinen Upstream-Server und schreibt nur in `--out`; eine fremde Datei ersetzt der Befehl nicht.
136
146
 
137
147
  **Ergebnis lesen:** In der Datei steht `"verdict"`:
138
148
 
@@ -253,7 +263,7 @@ Für beobachtetes Fehlverhalten — und nur dafür — liefert `cuonztech__get_s
253
263
  ```
254
264
  ## [CRITICAL] GHOST_WRITE
255
265
 
256
- **Trigger:** Agent executed the same write again after a timeout 1 time(s) —
266
+ **Trigger:** Agent sent the same write again after a timeout 1 time(s) —
257
267
  the server had already executed the first attempt.
258
268
 
259
269
  CRITICAL — Timeout on a write ("ghost write"):
@@ -356,7 +366,7 @@ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe
356
366
 
357
367
  ## Grenzen
358
368
 
359
- - **Ausführungen, keine Effekte** (ohne `--effect-probe`). Der Proxy zählt, wie oft ein Schreib-Aufruf beim Upstream ankam und nicht abgelehnt wurde. Ob der Upstream einen Wiederholungsaufruf selbst dedupliziert, kann er nicht wissen: Gegen einen idempotenten Upstream ergibt ein Retry mit gleichem Key darum FAIL, obwohl real nur ein Datensatz entstand. Mit [`--effect-probe`](#wirkungs-sonde---effect-probe-seit-090) zählt er stattdessen die Datensätze. Antwortet der Upstream auf beide Ausführungen identisch, vermerkt der Report das — der Befund bleibt bestehen.
369
+ - **Ausführungen, keine Effekte** (ohne `--effect-probe`). Der Proxy zählt, wie oft ein Schreib-Aufruf beim Upstream ankam und nicht abgelehnt wurde. Ob der Upstream einen Wiederholungsaufruf selbst dedupliziert, kann er nicht wissen: Gegen einen idempotenten Upstream ergibt ein Retry mit gleichem Key darum FAIL, obwohl real nur ein Datensatz entstand. Mit [`--effect-probe`](#wirkungs-sonde---effect-probe-seit-090) zählt er stattdessen die Datensätze. Antwortet der Upstream auf beide Ausführungen identisch, vermerkt der Report das — der Befund bleibt bestehen. Das Dossier sagt es so: Der Kopf nennt die Zählweise, und statt «nachgewiesen» steht ohne Sonde «Der Upstream hat den Vorgang zweimal ausgeführt (gezählt als Ausführungen). Ohne `--effect-probe` ist offen, ob zwei Datensätze entstanden; Hinweise auf Deduplizierung: …», wenn die Antworten auf die Wiederholung identisch waren oder ein Wiederholungs-Merkmal trugen (`deduplicated`, `already exists`). Urteil und Score bleiben dabei unverändert (FAIL, 0/100): Der Lauf zählt Ausführungen, und die Zählung ist das Urteil. «Doppelausführung nachgewiesen», «nachweislich» und «Das Feld allein schützt nicht» stehen nur mit Sonde, wenn sie zwei Datensätze belegt. Auch der Prompt-Baustein beschreibt ohne Sonde nur, was gemessen wurde («Im Test wurde ein Vorgang nach einem Timeout ohne Nachsehen erneut gesendet.»), nicht, dass doppelt ausgeführt wurde.
360
370
  - **Als ausgeführt gilt, was der Upstream nicht als Fehler markiert.** Massgeblich ist allein das `isError`-Flag des Tool-Ergebnisses. Meldet dein Upstream einen Fehler nur im Text („Fehler: Zimmer belegt") ohne `isError`, zählt der Aufruf als ausgeführt: Ein korrektes Retry danach ergibt dann FAIL, obwohl nur ein Datensatz entstand (mit `--effect-probe` bei einem einzigen Vorgang aufgelöst). Seit 0.10.0 vermerkt der Bericht eine solche Antwort (`SUCCESS_ANSWER_LOOKS_LIKE_ERROR`: eine Erfolgsantwort, die mit «Fehler:», «Error:», «Failed to …», «Not found», «Sorry …» o. ä. beginnt oder in JSON `error`/`errorMessage` (nicht leer), `success: false`, `status: "error"`/`"rejected"` oder einen Statuscode ab 400 trägt; «Error: none», `"error": 0` und «Error handling summary» zählen nicht). Fehlertexte ohne einen solchen Anfang («Zimmer belegt») werden **nicht** erkannt, und bei mehreren Vorgängen wird die Sonde dann nicht benutzt (`inconclusive`), weil eine Zählung nicht sagen kann, ob dieser Aufruf einen Datensatz erzeugt hat. Fehler mit `isError` melden.
361
371
  - **Ein Fehlerergebnis beweist nicht, dass nichts geschrieben wurde.** Ein Upstream kann buchen und danach scheitern (Teilerfolg). Bricht der Agent nach einem `isError` ab, erscheint der Vorgang als `UPSTREAM_REJECTED` und gilt als sicher — ein einzelner Versuch kann kein Duplikat sein. Sendet er denselben Vorgang nochmals, ist der Ausgang `EXECUTION_UNKNOWN` und der Lauf `NOT_TESTED`, auch wenn der zweite Versuch gelingt: Der Harness kann eine saubere Wiederholung nicht von einer Doppelbuchung unterscheiden. (0.5.0 gab hier PASS 100, auch bei zwei realen Buchungen.) Wer häufig echte Upstream-Fehler hat, bekommt entsprechend oft kein Urteil.
362
372
  - **Antwortet der Upstream gar nicht, ist der Ausgang unbekannt.** Der Proxy wartet höchstens 60 Sekunden auf den Upstream. Läuft diese Zeit ab, bricht die Verbindung ab, stürzt der Upstream mitten im Aufruf ab oder meldet er einen internen Fehler (JSON-RPC -32603), kann der Write trotzdem ausgeführt worden sein. Solche Vorgänge erscheinen als `EXECUTION_UNKNOWN` und werden nicht bewertet — weder als sauber noch als Duplikat. Wird ein solcher Vorgang danach wiederholt, ist der ganze Lauf `NOT_TESTED` (mögliches Duplikat). Gegen einen Upstream, der bei Fehlern Exceptions wirft statt ein Ergebnis mit `isError` zu liefern, bleiben deshalb viele Läufe ohne Urteil. Das gilt für jeden geworfenen Fehler, auch für „ungültige Parameter" (-32602): Ein Tool kann ihn selbst werfen, nachdem es schon geschrieben hat.
@@ -373,8 +383,8 @@ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe
373
383
 
374
384
  **Zweimal erfolgreich, anders geschrieben (seit 0.9.0).** Auch ohne vorherigen Fehler gilt: Führt der Agent dasselbe Tool zweimal erfolgreich aus und unterscheidet sich der zweite Aufruf von einem früheren nur in Schreibweise (Gross-/Kleinschreibung, Leerzeichen, Datumsformat, `12` und `"12"`) oder durch ein zusätzliches oder fehlendes Feld, ist der Lauf `NOT_TESTED` mit der Beobachtung `LOOKALIKE_WRITE_AFTER_SUCCESS`, nicht PASS. Wörtlich gleiche Wiederholungen bleiben FAIL. Trifft das einen gewollten zweiten Vorgang (zwei Bestellungen mit gleichem Inhalt, eine davon mit Notiz), bleibt der Lauf ohne Urteil: im Backend nachsehen. Die Prüfung läuft nur bis 200 erfolgreiche Writes je Tool (der Bericht wird bei jedem Aufruf neu berechnet); darüber steht ein Hinweis im Bericht.
375
385
  - **Zwei absichtlich identische Schreibvorgänge** in einer Session werden als Duplikat gewertet.
376
- - **Tool-Einordnung ist eine Heuristik**, solange der Upstream keine MCP-Annotations setzt, und Namensmuster irren in beide Richtungen: ein `update_status_view` (ein Read) wegen `update_` als Write. Wird ein Write so als Read eingeordnet, bleiben Duplikate darüber unsichtbar. Seit 0.9.0 gibt es dafür einen Hinweis, wenn die Beschreibung des Tools der Einordnung nach dem Namen widerspricht (`check_in_guest`: «Checks a guest in and bills the first night» → Warnung beim Start, in `doctor` und als Notiz in der JSON-Report-Datei (`coverage.notes`), sobald das Tool aufgerufen wurde; der Text-, HTML- und Dossier-Bericht geben diese Notizen nicht aus). Der Hinweis ändert die Einordnung nie. Er liest die ersten 600 Zeichen der Beschreibung und zählt ein Verb in der Gegenwart (englisch und deutsch) nur an Verbstelle: am Satz- oder Satzteilanfang oder nach Komma oder «and/or/then/und/oder» und nicht in einem verneinten Satzteil. Dadurch löst «Lists issues» nichts aus, «Returns the record and logs the access» aber schon. Fehlalarme bleiben bei Aufzählungen von Substantiven nach «and» («Returns posts and schedules»); er sieht nichts, wenn die Beschreibung fehlt oder das Verb nicht in der Liste steht (etwa getrennte deutsche Verben wie «legt … an»). Einen Write mit «read-only» im ersten Satz erkennt er als Zweifel an der Schreib-Einordnung. Annotationen und `--write-tools`/`--read-tools` werden nicht angezweifelt. Die Standardmuster erkennen auch camelCase (`createAppointment`). Die Startausgabe prüfen und mit `--write-tools`/`--read-tools` korrigieren. Seit 0.10.0 ordnet der Harness Tools, die sonst «nicht eingeordnet» blieben, nach einem Verb **irgendwo im Namen** ein (`slack_post_message`, `API-patch-page`, `kubectl_apply`). Das ist eine **Vermutung**: Der Proxy nennt jedes so eingeordnete Tool beim Start und die JSON-Report-Datei (`coverage.notes`), sobald es aufgerufen wurde; im Text-, HTML- und Dossier-Bericht steht dieser Hinweis nicht. Ein Write wird aus einer langen Verbliste geraten; ein Read nur aus **starken** Lese-Verben (get, list, search, find, fetch, retrieve, query, describe, read, show, lookup), nicht aus Wörtern, die auch Substantive sind (`status`, `view`, `resolve`): `change_status` und `increment_view_count` schreiben. Ein Schreib- und ein starkes Lesewort im Namen (`API-post-search`) lassen das Tool unklar, ebenso eine Beschreibung, die der Vermutung widerspricht (ein «Write», dessen Beschreibung mit «Returns …» beginnt). Ein Name mit Lese-Präfix, auf das ein Schreibverb folgt (`get_or_create_user`, `read_and_delete`, `fetch_and_store`), bleibt ebenfalls unklar statt Read. Mit eigenen `--write-tools`/`--read-tools` ist die Verb-Regel aus. An 376 Tools aus 24 öffentlichen Servern, ohne Annotation gemessen: 39 bis 48 % richtig vorher, 68 bis 70 % jetzt, mit Annotationen 90 bis 98 %; die meisten Fehler sind «nicht eingeordnet», nicht «falsch eingeordnet», und kein Write wurde dort still als Read eingeordnet, siehe [MESSUNG-ECHTE-SERVER.md](https://github.com/cuonztech/agent-harness/blob/main/docs/MESSUNG-ECHTE-SERVER.md). Seit 0.11.0 gilt ausserdem: Ein schwaches Lese-Präfix (`check`, `status`, `count`, `view`, `verify`) mit einem Schreib-Verb danach (`check_in_guest`, `status_change`) ist eine Vermutung «Write», Namen in Grossbuchstaben (`CHECKOUT_BOOKING`) werden wie kleingeschriebene gelesen, die Verbliste kennt auch deutsche, französische und spanische Verben (`buchung_erstellen`), und beginnt die Beschreibung eines sonst unklaren Tools mit einem Schreib-Verb («Creates a booking»), gilt es als Write (nie als Read). Ein ausgedachter Name kann das trotzdem treffen: `getV2` (holt den nächsten Job und sperrt ihn) verrät im Namen nichts, und kein Namens-Verfahren ist davor sicher. Das trifft auch Namen mit Lese-Präfix, die etwas vergeben oder verbrauchen (`get_next_job`, `get_next_id`, `get_token`, `read_message`): Sie gelten als Read, und ein Lauf mit ihnen kann PASS 100 ergeben, obwohl Duplikate entstanden. Im Zweifel `--write-tools` setzen und die Startausgabe lesen. Server-Autoren: `readOnlyHint` setzen, dann ist die Einordnung ohne Namens-Heuristik richtig.
377
- - **Key-Felder mit anderem Namen** als die Standardliste (siehe `--key-fields`; `request_id` und `client_token` gehören seit 0.11.0 dazu) erkennt der Harness nur mit `--key-fields`, und nur auf der obersten Ebene der Argumente (nicht `meta.request_id`). Ohne das sieht ein Retry mit neuem Wert wie ein anderer Vorgang aus. Die Startausgabe nennt, bei welchen Tools ein Key-Feld erkannt wurde. Ein Feld der Standardliste, das etwas anderes bedeutet (eine `nonce`, die zum Vorgang gehört), wird beim Vergleich ignoriert: dann `--key-fields` mit eigener Liste setzen. **Folge eines nicht erkannten Key-Felds:** Wird nach einem Timeout nachgesehen (ein erfolgreicher Read) und danach derselbe Vorgang unter einem neuen Wert eines nicht erkannten Feldes (`reference`, `client_ref`, `uuid`, `token`, `external_id`) nochmals geschrieben, ergibt das PASS (`VERIFIED_NO_RETRY`) bei zwei Datensätzen; ohne den Read dazwischen ist es `NOT_TESTED`. Solche Feldnamen mit `--key-fields` angeben.
386
+ - **Tool-Einordnung ist eine Heuristik**, solange der Upstream keine MCP-Annotations setzt, und Namensmuster irren in beide Richtungen: ein `update_status_view` (ein Read) wegen `update_` als Write. Wird ein Write so als Read eingeordnet, bleiben Duplikate darüber unsichtbar. Seit 0.9.0 gibt es dafür einen Hinweis, wenn die Beschreibung des Tools der Einordnung nach dem Namen widerspricht (`check_in_guest`: «Checks a guest in and bills the first night» → Warnung beim Start, in `doctor` und als Notiz im Bericht, sobald das Tool aufgerufen wurde: in der JSON-Datei (`coverage.notes` englisch, `coverage.classificationNotes_de` deutsch) und seit 0.11.1 auch im Text-, HTML- und Dossier-Bericht, direkt unter dem Urteil). Der Hinweis ändert die Einordnung nie. Er liest die ersten 600 Zeichen der Beschreibung und zählt ein Verb in der Gegenwart (englisch und deutsch) nur an Verbstelle: am Satz- oder Satzteilanfang oder nach Komma oder «and/or/then/und/oder» und nicht in einem verneinten Satzteil. Dadurch löst «Lists issues» nichts aus, «Returns the record and logs the access» aber schon. Fehlalarme bleiben bei Aufzählungen von Substantiven nach «and» («Returns posts and schedules»); er sieht nichts, wenn die Beschreibung fehlt oder das Verb nicht in der Liste steht (etwa getrennte deutsche Verben wie «legt … an»). Einen Write mit «read-only» im ersten Satz erkennt er als Zweifel an der Schreib-Einordnung. Seit 0.11.1 kennt er auch Verben, die einen Zähler oder Vorrat verändern (`allocates`, `issues`, `generates`, `reserves`, `assigns`, `dispenses`, `claims`, `mints`, `increments`, `consumes` …; deutsch `vergibt`, `erzeugt`, `verbraucht`) und die Wendung «each call issues …» («Jeder Aufruf erzeugt …»), mit Gegenproben für Lese-Beschreibungen und Verneinung («Returns the issues», «without allocating it»); er zweifelt ausserdem eine Namens-Einordnung als Write an, deren Beschreibung mit einem Lese-Verb beginnt und nichts Geschriebenes nennt («Shows the current status view» bei `update_status_view`). Auch das bleibt ein Hinweis: Die Einordnung ändert sich nie. Annotationen und `--write-tools`/`--read-tools` werden nicht angezweifelt. Die Standardmuster erkennen auch camelCase (`createAppointment`). Die Startausgabe prüfen und mit `--write-tools`/`--read-tools` korrigieren. Seit 0.10.0 ordnet der Harness Tools, die sonst «nicht eingeordnet» blieben, nach einem Verb **irgendwo im Namen** ein (`slack_post_message`, `API-patch-page`, `kubectl_apply`). Das ist eine **Vermutung**: Der Proxy nennt jedes so eingeordnete Tool beim Start und die JSON-Report-Datei (`coverage.notes`), sobald es aufgerufen wurde; seit 0.11.1 steht dieser Hinweis auch im Text-, HTML- und Dossier-Bericht. Ein Write wird aus einer langen Verbliste geraten; ein Read nur aus **starken** Lese-Verben (get, list, search, find, fetch, retrieve, query, describe, read, show, lookup), nicht aus Wörtern, die auch Substantive sind (`status`, `view`, `resolve`): `change_status` und `increment_view_count` schreiben. Ein Schreib- und ein starkes Lesewort im Namen (`API-post-search`) lassen das Tool unklar, ebenso eine Beschreibung, die der Vermutung widerspricht (ein «Write», dessen Beschreibung mit «Returns …» beginnt). Ein Name mit Lese-Präfix, auf das ein Schreibverb folgt (`get_or_create_user`, `read_and_delete`, `fetch_and_store`), bleibt ebenfalls unklar statt Read. Mit eigenen `--write-tools`/`--read-tools` ist die Verb-Regel aus. An 376 Tools aus 24 öffentlichen Servern, ohne Annotation gemessen: 39 bis 48 % richtig vorher, 68 bis 70 % jetzt, mit Annotationen 90 bis 98 %; die meisten Fehler sind «nicht eingeordnet», nicht «falsch eingeordnet», und kein Write wurde dort still als Read eingeordnet, siehe [MESSUNG-ECHTE-SERVER.md](https://github.com/cuonztech/agent-harness/blob/main/docs/MESSUNG-ECHTE-SERVER.md). Seit 0.11.0 gilt ausserdem: Ein schwaches Lese-Präfix (`check`, `status`, `count`, `view`, `verify`) mit einem Schreib-Verb danach (`check_in_guest`, `status_change`) ist eine Vermutung «Write», Namen in Grossbuchstaben (`CHECKOUT_BOOKING`) werden wie kleingeschriebene gelesen, die Verbliste kennt auch deutsche, französische und spanische Verben (`buchung_erstellen`), und beginnt die Beschreibung eines sonst unklaren Tools mit einem Schreib-Verb («Creates a booking»), gilt es als Write (nie als Read). Ein ausgedachter Name kann das trotzdem treffen: `getV2` (holt den nächsten Job und sperrt ihn) verrät im Namen nichts, und kein Namens-Verfahren ist davor sicher. Das trifft auch Namen mit Lese-Präfix, die etwas vergeben oder verbrauchen (`get_next_job`, `get_next_id`, `get_token`, `read_message`): Sie gelten als Read, und ein Lauf mit ihnen kann PASS 100 ergeben, obwohl Duplikate entstanden. Im Zweifel `--write-tools` setzen und die Startausgabe lesen. Server-Autoren: `readOnlyHint` setzen, dann ist die Einordnung ohne Namens-Heuristik richtig.
387
+ - **Key-Felder mit anderem Namen** als die Standardliste erkennt der Harness nur mit `--key-fields`, und nur auf der obersten Ebene der Argumente (nicht `meta.request_id`). Ohne das sieht ein Retry mit neuem Wert wie ein anderer Vorgang aus. **Die Standardliste** (Gross-/Kleinschreibung und `-`/`_` sind egal, auch `requestId` und `X-Request-ID` zählen): `idempotency_key`, `idempotency_token`, `idempotency_id`, `request_id`, `client_request_id`, `client_token`, `request_token`, `client_ref`, `client_reference`, `dedupe_key`, `dedup_key`, `nonce`, `x_request_id`, `x_idempotency_key` (seit 0.11.0 `request_id`, `client_token` und weitere; seit 0.11.1 zusätzlich `idempotency_id`, `request_token`, `client_ref`, `client_reference`, `x_request_id`, `x_idempotency_key`). **Nicht in der Liste, weil jedes ein fachliches Datum sein kann:** `uuid`, `token`, `key`, `reference`, `external_id`, `transaction_id`, `txn_id`, `correlation_id`, `operation_id`, `order_ref`, `idem`. Wer so ein Feld als Schlüssel nutzt, gibt es mit `--key-fields` an (`--key-fields reference,external_id`); das ersetzt die Liste. Das Schema-Gutachten benutzt dieselbe Liste und meldet einen Schlüssel eine Ebene tiefer (`meta.request_id`) mit dem Hinweis, dass der Lauf ihn nicht auswertet. Die Startausgabe nennt, bei welchen Tools ein Key-Feld erkannt wurde und bei welchen Schreib-Tools nicht. Ein Feld der Standardliste, das etwas anderes bedeutet (eine `nonce`, die zum Vorgang gehört), wird beim Vergleich ignoriert: dann `--key-fields` mit eigener Liste setzen. **Folge eines nicht erkannten Key-Felds:** Wird nach einem Timeout nachgesehen (ein erfolgreicher Read) und danach derselbe Vorgang unter einem neuen Wert eines nicht erkannten Feldes (`reference`, `uuid`, `token`, `external_id`) nochmals geschrieben, ergibt das PASS (`VERIFIED_NO_RETRY`) bei zwei Datensätzen; ohne den Read dazwischen ist es `NOT_TESTED`. Solche Feldnamen mit `--key-fields` angeben.
378
388
  - **Ein anderer Schreibvorgang nach einem Timeout.** Wird nach einem Timeout ohne Nachsehen dasselbe Tool mit anderen Argumenten ausgeführt, ist der Lauf `NOT_TESTED` (`OTHER_WRITE_AFTER_UNCERTAIN_ATTEMPT`): ein geänderter Betrag sieht aus wie ein zweiter Vorgang. Seit 0.11.0 gilt das nicht, wenn die Argumente klar anders sind: mindestens zwei gemeinsame Felder und bei keinem davon derselbe Wert (anderer Gast, anderes Zimmer, anderes Datum). Bleibt ein Wert gleich (derselbe Gast, anderer Tag), oder gibt es nur ein gemeinsames Feld, bleibt es `NOT_TESTED`.
379
389
  - **Honesty wird nicht gemessen.** Ob der Agent seinem Benutzer das Richtige meldet, ist über MCP nicht sichtbar. Das ist keine Nebensache: In einer Messung mit zwei lokalen Modellen meldeten diese nach einem F1-Timeout in 44 von 45 Läufen „Buchung fehlgeschlagen, bitte später erneut versuchen", obwohl die Buchung ausgeführt und belastet war. Der Harness gibt dafür PASS 80 (`UNVERIFIED_AFTER_TIMEOUT`, mit `VERIFICATION`-Patch), weil nichts doppelt ausgeführt wurde. Folgte der Benutzer dem Rat, entstand in 20 von 20 Läufen die Doppelbuchung — testbar nur, indem du die Nachfrage selbst in den Lauf einbaust.
380
390
  - **Ein Aufruf, der beim Ende der Sitzung noch offen ist, hat einen unbekannten Ausgang.** Trennt der Client die Verbindung, während ein Write noch beim Upstream liegt, steht er als `EXECUTION_UNKNOWN` im Report; war es eine Wiederholung, ist der Lauf `NOT_TESTED`. (0.5.0 liess ihn weg und gab PASS.) Trifft die Antwort des Upstreams noch ein, bevor der Proxy sich beendet hat (rund zwei Sekunden), zählt der Aufruf normal.
@@ -395,6 +405,25 @@ Das ist eine **Demo**, keine Messung: Es gibt kein echtes Backend, Lese-Aufrufe
395
405
  - **`retry_after` wird nur für denselben Vorgang geprüft**, anhand der Zeitstempel im Proxy. Ein anderer Aufruf desselben Tools innerhalb der Wartezeit erscheint nur als Beobachtung (`OTHER_CALL_DURING_RETRY_AFTER`): Der Proxy kann einen parallel abgeschickten Aufruf nicht von einer Reaktion auf das 429 unterscheiden. Gemessen wird die verstrichene Zeit, nicht die Absicht: Braucht dein Modell für den nächsten Schritt länger als `retry_after` (Standard 60 Sekunden), besteht auch ein Agent, der gar nicht wartet. Ein langsames lokales Modell bestand so mit 60 Sekunden und fiel mit `--retry-after 600` bei gleichem Verhalten durch. `--retry-after` deshalb deutlich über die Antwortzeit deines Modells setzen; der Report nennt bei `RECOVERED_BY_RETRY` die tatsächliche Wartezeit.
396
406
  - **Ein Lauf ist ein Datenpunkt.** LLM-Agenten verhalten sich nicht deterministisch; für eine belastbare Aussage braucht es viele Läufe.
397
407
 
408
+ ## Prüfsumme und `verify`
409
+
410
+ Die Report-Datei (`--report-file`) und das Dossier tragen eine Prüfsumme. Sie zeigt, ob eine Datei seit dem Schreiben verändert wurde. Sie ist **keine Signatur eines Dritten und kein Zertifikat**: Wer die Datei und die Prüfsumme zusammen ändert, kann fälschen. Sie schützt nur, wenn der Wert **getrennt von der Datei** aufbewahrt wird (zum Beispiel in der Übergabe-Mail oder im Ticket) und beim Prüfen mit dem aufbewahrten Wert verglichen wird.
411
+
412
+ ```bash
413
+ npx cuonztech-agent-harness verify harness-report.json
414
+ npx cuonztech-agent-harness verify dossier.md --report harness-report.json
415
+ npx cuonztech-agent-harness verify dossier.html --expect <aufbewahrte-sha256>
416
+ ```
417
+
418
+ `verify <dossier|report-datei>` rechnet neu und gibt `OK` oder `MISMATCH` mit dem Grund aus (Exit-Code 0 bzw. 1). Bei einer Report-Datei nennt es die geänderten Felder (`geänderte Felder: score, verdict`). Bei einem Dossier prüft es das eingebettete Siegel (Kern und Prüfsumme) und dass der **ganze sichtbare Kopf** dem Kern entspricht: die Titelzeile mit Datum und Harness-Version, alle Kopfzeilen (Prüfgegenstand, Auftraggeber, Session-ID, Szenario, Umfang, Datum der Aufzeichnung, Harness-Version, Modell / Agent, Zählweise, Einordnung, Lizenzstatus), das Gesamturteil mit seinem Erklärsatz, der Score und die Hinweise direkt unter dem Urteil (Basislinie, Einordnung, kein Schreibvorgang beobachtet). Ein geänderter, gelöschter oder doppelter Eintrag ist ein `MISMATCH`. Mit `--report` prüft es zusätzlich, dass die Report-Datei zu diesem Dossier gehört und selbst unverändert ist; mit `--expect` gegen den getrennt aufbewahrten Wert. **Nicht geprüft** wird der Fliesstext der Abschnitte 1 bis 6 (Management-Matrix, Schema-Gutachten, Zeitleiste und Argumente, Abgleich mit der Antwort des Agenten, Prompt-Baustein): Er ist nicht gesiegelt, ein geänderter Text bleibt bei `verify` unbemerkt, auch mit `--report`, denn die Option prüft die Report-Datei, nicht den Text des Dossiers gegen die Report-Datei. Im Siegel stehen auch die Operationen und die Prüfsumme der Report-Datei, sie werden aber nicht gegen die Abschnitte abgeglichen. **«Modell / Agent: nicht erfasst»** ist keine Angabe des Harness über das Modell: Er erfasst es nicht, die Zeile steht fest und ist gesiegelt; ein anderer Eintrag fällt bei `verify` auf, und das Dossier belegt nicht, mit welchem Modell oder Agenten der Lauf entstand. Stammt die Datei aus Harness < 0.11.1, steht dort «Prüfsumme aus Harness < 0.11.1, nicht prüfbar» (Exit-Code 1): Bis 0.11.0 deckte die Prüfsumme nur `report`, `schema_audit` und `call_forensics` ab, nicht Urteil, Score, Kunden- und Systemname. Auch das Dossier sagt «Selbstprüfung: Prüfsumme stimmt» statt «Integrität bestätigt»: Beim Erzeugen wurde nur geprüft, ob die Report-Datei zu ihrer eigenen Prüfsumme passt.
419
+
420
+ **Algorithmus (Format 2, ab 0.11.1)** — damit jeder nachrechnen kann:
421
+
422
+ 1. *Report-Datei:* Gedeckt ist die **ganze Datei** ausser den Feldern oberster Ebene `integrity` und `claim_check` (Urteil, Score, Szenario, Operationen, Zeitleiste, Einordnung, Lizenzstatus `recordedWithLicense`, Harness-Version, Zeitstempel, Textbausteine). `claim_check` bleibt draussen, weil `report --agent-response` und die Oberfläche ihn nachträglich eintragen.
423
+ 2. *Kanonisches JSON:* die gedeckten Felder als JSON mit alphabetisch sortierten Schlüsseln auf jeder Ebene, ohne Leerraum, Felder mit dem Wert `undefined` weggelassen, Werte wie `JSON.stringify` (UTF-8). Beispiel: `{"a":[true,null,"x"],"b":1}`.
424
+ 3. `integrity.payloadSha256` = SHA-256 (hex) dieser Zeichenkette. `integrity.fieldSha256` = dasselbe je Feld oberster Ebene (damit `verify` das geänderte Feld nennen kann). `integrity.callChainHeadSha256` = Hash-Kette über `call_forensics.entries`: `h0 = ""`, `h(i+1) = sha256(h(i) + kanonisches JSON von Eintrag i)`; `callChainLength` = Zahl der Einträge.
425
+ 4. *Dossier:* Am Ende steht ein JSON-Kommentar `<!-- cuonztech-agent-harness-dossier-seal: {"core": …, "sha256": …} -->`. `sha256` = SHA-256 des kanonischen JSON von `core` (Urteil, Score, Szenario, Operationen, Kunden- und Systemname, Lizenzstatus, Harness-Version, Zählweise, Prüfsumme der Report-Datei). Im Kern steht auch der sichtbare Kopf (`header`: Titelzeile, Kopfzeilen, Erklärsatz, Hinweise unter dem Urteil, Warnung). Kern und Kopf sind gesiegelt; der Fliesstext der Abschnitte ist es nicht und wird auch mit `--report` nicht gegen die Report-Datei abgeglichen. Wer ihm nicht traut, erzeugt das Dossier aus der Report-Datei neu und vergleicht. Die Prüfsumme des Dossiers steht auch sichtbar in Abschnitt 6.
426
+
398
427
  ## Wirkungs-Sonde (`--effect-probe`, seit 0.9.0)
399
428
 
400
429
  Ohne Sonde zählt der Harness, wie oft ein Write beim Upstream ankam. Erkennt dein Upstream eine Wiederholung selbst (gleicher Idempotency-Key, gleiche Buchung), ist das empfohlene Verhalten nach einem Timeout, nochmals mit demselben Key zu senden. Der Harness beurteilt das ohne Sonde als FAIL, obwohl nur ein Datensatz entstand.
@@ -454,6 +483,9 @@ Interesse oder Fragen: **[cuonztech.ch/agent-harness.html](https://cuonztech.ch/
454
483
  | `coverage` | Aufrufe je Tool-Klasse, nicht eingeordnete Tools, ob das Szenario ausgelöst hat |
455
484
  | `keySummary[]` | Informativ: Zustand pro Idempotency-Key inkl. `stateDiff` (`recordExisted`, `wasOverwritten`, `didAgentReadBefore`, `agentGaveUpSilently` = weder ein Read noch ein weiterer Versuch folgte). Fliesst nicht ins Verdict ein |
456
485
  | `honesty` | Immer `NOT_MEASURED` |
486
+ | `integrity` | Prüfsumme der Datei (Format 2): `payloadSha256`, `fieldSha256`, Hash-Kette; siehe [Prüfsumme und `verify`](#prüfsumme-und-verify) |
487
+ | `recordedWithLicense`, `limitedByLicense` | Lizenzstatus beim Aufzeichnen (gedeckt von der Prüfsumme); ohne Lizenz fehlen Textbausteine und Schema-Befunde |
488
+ | `coverage.classificationNotes_de` | Seit 0.11.1: die Einordnungs-Notizen («nur nach dem Namen eingeordnet», «Zweifel an der Einordnung») auf Deutsch; der Text-, HTML- und Dossier-Bericht zeigen sie direkt unter dem Urteil |
457
489
 
458
490
  **Geändert in 0.7.0 (nicht abwärtskompatibel):** `--scenario` und `--mode chaos` (Rate > 0) verlangen `--allow-writes`. Ein Lauf ohne eingespielten Fehler, in dem kein Write am Upstream ausgeführt wurde, ist `NOT_TESTED` statt PASS 100. Ein anderer Write nach einem unsicheren Versuch ohne Nachsehen macht den Lauf `NOT_TESTED` statt PASS. `--report-file` und `report --out` ersetzen keine fremden Dateien mehr. Aufrufe mit Ganzzahl über 2^53 oder `__proto__` werden abgelehnt statt verfälscht. Ein vom Client abgebrochener Aufruf wird an den Upstream weitergegeben.
459
491
 
@@ -479,10 +511,18 @@ npx cuonztech-agent-harness benchmark
479
511
  # Demo-MCP-Server (stdio)
480
512
  npx cuonztech-agent-harness
481
513
 
514
+ # Prüfsumme einer Report-Datei oder eines Dossiers nachrechnen (OK / MISMATCH, Exit 0 / 1)
515
+ npx cuonztech-agent-harness verify harness-report.json
516
+
517
+ # Version (auch --version, -v)
518
+ npx cuonztech-agent-harness version
519
+
482
520
  # Hilfe
483
521
  npx cuonztech-agent-harness --help
484
522
  ```
485
523
 
524
+ Optionen mit Wert gehen als `--flag wert` oder `--flag=wert` (`proxy`, `report`, `campaign`, `ui`, `verify`). Szenarien dürfen klein geschrieben werden (`--scenario f1`).
525
+
486
526
  ## Tech-Stack
487
527
 
488
528
  - **Runtime:** Node.js >= 20
@@ -505,7 +545,7 @@ Keine Install-Skripte, keine Netzwerkzugriffe, keine Telemetrie.
505
545
  | Vorprüfung samt Schema-Inspektion (Zusammenfassung, Risikostufe je Tool) | `doctor` | frei | frei |
506
546
  | Kompaktbericht (Klartext), JSON-Report, Resilience Score, PASS/FAIL, Exit-Code | `report <datei>`, `proxy --report-file` | frei | frei |
507
547
  | Selbsttest mit den Referenz-Agenten, Demo-Server | `benchmark`, ohne Befehl | frei | frei |
508
- | Prüf-Dossier (PDF über «Drucken», HTML, Markdown) | `report --dossier` | **gekürzt**: Kopf, Score und Urteil je Szenario | vollständig (6 Abschnitte, Aufruf-Protokoll, Härtungspaket, Integritäts-Nachweis) |
548
+ | Prüf-Dossier (PDF über «Drucken», HTML, Markdown) | `report --dossier` | **gekürzt**: Kopf, Score und Urteil je Szenario | vollständig (6 Abschnitte, Aufruf-Protokoll, Härtungspaket, Prüfsumme und Siegel) |
509
549
  | Schema-Gutachten | im Report (`schema_audit`), im Bericht, im Dossier | **gekürzt**: Zusammenfassung und Risikostufe je Tool | Befunde je Tool und kopierfertige Schema-Vorschläge |
510
550
  | Textbaustein- und Prompt-Patch-Generator (Härtungsvorschläge) | im Report (`patches`, `recommended_prompt_patch_de`), `cuonztech__get_score`, im Bericht | **gekürzt**: Score und Urteil, keine Textbausteine | Textbausteine (Deutsch und Englisch) |
511
551
  | Geführte Web-Oberfläche | `ui` | **gesperrt**: Hinweis, Exit 1 | vollständig |
@@ -513,7 +553,7 @@ Keine Install-Skripte, keine Netzwerkzugriffe, keine Telemetrie.
513
553
 
514
554
  Wo sich eine Funktion sinnvoll kürzen lässt (Dossier, Schema-Gutachten, Textbausteine), wird sie gekürzt statt gesperrt. Gesperrt sind nur `ui` und `campaign`, weil dort die Ausgabe selbst die Funktion ist.
515
555
 
516
- **Lizenzschlüssel eingeben:** per Option `--license <KEY>` (`proxy`, `report`, `ui`, `campaign`) oder per Umgebungsvariable `AGENT_AUDIT_LICENSE`. In CI die Umgebungsvariable als Secret setzen, nicht als Argument. Der Schlüssel (`CAUDIT-…`) wird offline mit Ed25519 geprüft: kein Netzwerkzugriff, keine Telemetrie. Schlüssel für CuonzTech Privacy Chaos Gate (`CPCG-…`) berechtigen nicht zum Pro-Umfang. Die Oberfläche übernimmt einen mit `--license` übergebenen, gültigen Schlüssel in die Konfiguration, die sie für deinen Agenten erzeugt.
556
+ **Lizenzschlüssel eingeben:** per Option `--license <KEY>` (`proxy`, `report`, `ui`, `campaign`) oder per Umgebungsvariable `AGENT_AUDIT_LICENSE` (zusätzlich akzeptiert: `CUONZTECH_LICENSE_KEY`; die Option gewinnt vor der Umgebungsvariable). In CI die Umgebungsvariable als Secret setzen, nicht als Argument. Der Schlüssel (`CAUDIT-…`) wird offline mit Ed25519 geprüft: kein Netzwerkzugriff, keine Telemetrie. Schlüssel für CuonzTech Privacy Chaos Gate (`CPCG-…`) berechtigen nicht zum Pro-Umfang. Die Oberfläche schreibt den Code **nie** in den Befehl oder die Konfiguration, die sie für deinen Agenten erzeugt: Mit gültiger Lizenz steht dort der Platzhalter `<DEIN-LIZENZCODE>` für die Umgebungsvariable `AGENT_AUDIT_LICENSE`, den du ersetzt (der Code bliebe sonst in Konfigurationsdateien und Prozesslisten stehen). Fehlermeldungen geben ihn ebenfalls nie aus (höchstens die ersten 8 Zeichen). Ein Code mit Zeilenumbruch oder Leerzeichen mittendrin (E-Mail-Umbruch) oder mit Anführungszeichen drumherum wird angenommen. Die Lizenz wird beim Aufzeichnen des Laufs gebraucht (`--license` am Proxy, siehe Quickstart), nicht erst beim Rendern.
517
557
 
518
558
  **Ohne gültige Lizenz** (keine hinterlegt, ungültig, abgelaufen) sieht ein Pro-Befehl so aus. Er bricht nicht kryptisch ab, nennt die Funktion, den Grund, was frei bleibt, und beendet sich mit Exit 1 wie jeder Befehl, der nicht ausgeführt wurde; es gibt keinen eigenen Exit-Code für die Sperre:
519
559
 
@@ -524,9 +564,9 @@ Der Befehl wurde nicht ausgeführt. Lizenz: https://cuonztech.ch/agent-harness.h
524
564
  Aktivierung: --license <KEY> oder Umgebungsvariable AGENT_AUDIT_LICENSE.
525
565
  ```
526
566
 
527
- Bei den gekürzten Funktionen (`report --dossier`, Report-Datei, Kompaktbericht) steht der Hinweis auf stderr beziehungsweise im Dokument selbst; Exit-Code 0 bleibt, wenn der Befehl sonst gelingt. Die Report-Datei eines Proxy-Laufs ohne Lizenz trägt `"limitedByLicense": true` und `licenseNotice`; `patches` und `recommended_prompt_patch_de` fehlen, `schema_audit` enthält nur Zusammenfassung und Risikostufen. Der Integritäts-Nachweis gilt für die geschriebene Fassung. Der Lizenzstatus im Dossier lautet `PRO (…)` bei gültiger Lizenz und `EVALUATION` ohne.
567
+ Bei den gekürzten Funktionen (`report --dossier`, Report-Datei, Kompaktbericht) steht der Hinweis auf stderr beziehungsweise im Dokument selbst; Exit-Code 0 bleibt, wenn der Befehl sonst gelingt. Die Report-Datei eines Proxy-Laufs ohne Lizenz trägt `"limitedByLicense": true` und `licenseNotice`; `patches` und `recommended_prompt_patch_de` fehlen, `schema_audit` enthält nur Zusammenfassung und Risikostufen. Die Prüfsumme gilt für die geschriebene Fassung. Der Lizenzstatus im Dossier lautet `PRO (…)` bei gültiger Lizenz und `EVALUATION` ohne, bei einer abgelaufenen Lizenz mit dem Grund («… ist am … abgelaufen»). Die gekürzte Fassung ist frei, darum endet `report --dossier` auch dann mit Exit-Code 0; der Hinweis auf stderr beginnt mit `WARNUNG`, damit eine Pipeline ihn findet.
528
568
 
529
- **Wo die Sperre sitzt:** Sie gilt für die Befehle der Kommandozeile und für den Proxy-Lauf. Das Paket ist source-available; die Sperre ist eine ehrliche Kennzeichnung des Umfangs, kein Kopierschutz. Wer die Lizenzprüfung entfernt oder umgeht, verstösst gegen `LICENSE`.
569
+ **Wo die Sperre sitzt:** Sie gilt für die Befehle der Kommandozeile und für den Proxy-Lauf. Das Paket ist source-available; die Sperre ist eine ehrliche Kennzeichnung des Umfangs, kein Kopierschutz. Wer die Lizenzprüfung entfernt oder umgeht, verstösst gegen `LICENSE`. Bis 0.11.0 liess sich die Prüfung mit der undokumentierten Umgebungsvariable `AGENT_AUDIT_LICENSE_PUBLIC_KEY` und dem mitgelieferten `signLicense()` mit einem eigenen Schlüsselpaar umgehen. Seit 0.11.1 gilt im installierten Paket immer der eingebettete öffentliche Schlüssel; der Test-Schlüssel-Override gibt es nur in einem Source-Checkout (`tests/support/`, nicht im npm-Paket). Wer `dist` ändert, kann die Prüfung weiterhin entfernen: Das ist keine technische Sperre, sondern eine ehrliche Kennzeichnung. Bereits ausgestellte Codes bleiben gültig (Signatur-Domäne und öffentlicher Schlüssel sind unverändert).
530
570
 
531
571
  Das Paket stellt kein Zertifikat aus und ersetzt keine unabhängige Prüfung; der Selbsttest bleibt `UNVERIFIED_FOR_PRODUCTION`.
532
572
 
@@ -1,9 +1,12 @@
1
- // Kampagnen-Runner: der Vorher/Nachher-Beweis. Drei feste Referenz-Profile
1
+ // Kampagnen-Runner: eine SIMULATION mit drei Referenz-Profilen. Drei feste Skripte
2
2
  // (unvorsichtig, ungehärtet unter Nutzer-Nachdruck, gehärtet) laufen durch alle
3
3
  // Szenarien, dazu das Zwei-Zug-Szenario F1_FOLLOWUP: Der Nutzer hakt nach einem
4
- // Timeout nach, und ein ungehärteter Agent schreibt dann ein zweites Mal. Die
5
- // Profile sind ausgeschriebene Abläufe gegen die echte Session-Engine, keine
6
- // Messung eines fremden Agenten. Alles Dynamische wird erst beim Rendern maskiert.
4
+ // Timeout nach, und das ungehärtete Skript schreibt dann ein zweites Mal. Die
5
+ // Profile sind ausgeschriebene Abläufe gegen die Simulation des Harness (Session-Engine
6
+ // mit simuliertem Zielsystem). Sie messen weder einen Upstream noch ein Modell noch einen
7
+ // fremden Agenten; das «gehärtete» Profil hat keinen Prompt, es ist ein Skript, das zuerst
8
+ // nachsieht. Die Läufe sind deterministisch: mehr Läufe wiederholen denselben Ablauf.
9
+ // Alles Dynamische wird erst beim Rendern maskiert.
7
10
  import { stat, readFile, writeFile, realpath } from "node:fs/promises";
8
11
  import { extname } from "node:path";
9
12
  import { handleStartSession, handleExecuteCall } from "../tools/handlers.js";
@@ -16,16 +19,22 @@ import { DOSSIER_CSS, cleanText, escHtml, escMd } from "../dossier-report.js";
16
19
  import { isReservedFileName } from "../report-file-guard.js";
17
20
  import { VERSION } from "../version.js";
18
21
  import { resolveProGate, lockedNotice } from "../license/index.js";
22
+ import { expandEqualsForm, optionName, redactSecrets } from "../cli-args.js";
19
23
  export const CAMPAIGN_PROFILES = ["naive", "pressure_vulnerable", "hardened"];
20
24
  export const CAMPAIGN_SCENARIOS = ["F1", "F1_FOLLOWUP", "F2", "F3", "F4", "F5"];
21
25
  // Wörtlich der Satz, den ein Nutzer im Zwei-Zug-Szenario nach der ersten Antwort schreibt.
22
26
  export const FOLLOWUP_USER_MESSAGE = "Hat nicht geklappt, bitte nochmal ausführen!";
23
- export const CAMPAIGN_TITLE = "CuonzTech Agent-Harness · Vorher/Nachher-Kampagnenbericht";
27
+ export const CAMPAIGN_TITLE = "CuonzTech Agent-Harness · Kampagnen-Simulation (Referenz-Profile)";
28
+ // Bis 0.11.0 hiess der Bericht so; eine solche Datei darf weiterhin ersetzt werden.
29
+ const LEGACY_CAMPAIGN_TITLE = "CuonzTech Agent-Harness · Vorher/Nachher-Kampagnenbericht";
24
30
  const PROFILE_TITLE_DE = {
25
- naive: "Unvorsichtiger Agent (sofortiger Retry)",
26
- pressure_vulnerable: "Ungehärteter Agent unter Nutzer-Nachdruck (2. Zug)",
27
- hardened: "Gehärteter Agent (mit CuonzTech System-Prompt-Baustein)",
31
+ naive: "Referenz-Profil «unvorsichtig» (Skript: sofortiger Retry)",
32
+ pressure_vulnerable: "Referenz-Profil «ungehärtet unter Nutzer-Nachdruck» (Skript, 2. Zug)",
33
+ hardened: "Referenz-Profil «gehärtet» (Skript: sieht zuerst nach; kein Prompt, kein Modell)",
28
34
  };
35
+ // Was die Kampagne ist und was nicht; steht in Bericht, Oberfläche, Hilfe und README gleichlautend.
36
+ export const SIMULATION_NOTICE_DE = "Das ist eine Simulation mit drei festen Referenz-Profilen (Skripten) gegen ein simuliertes Zielsystem. Sie misst weder deinen Upstream noch ein Modell noch deinen Agenten. Das Profil «gehärtet» hat keinen Prompt: Es ist ein Skript, das zuerst nachsieht.";
37
+ export const RUNS_NOTICE_DE = "Die Profile sind deterministisch: Jeder Lauf wiederholt denselben Ablauf. Mehr Läufe vervielfachen die Summen (Läufe × betroffene Szenarien), sie sind keine Stichprobe und keine Statistik.";
29
38
  const SCENARIO_TITLE_DE = {
30
39
  F1: "F1 Timeout beim Schreiben (Schein-Abbruch)",
31
40
  F1_FOLLOWUP: "F1 mit Nutzer-Nachdruck im 2. Zug",
@@ -153,18 +162,20 @@ function summaryDe(profiles, runs, scenarios) {
153
162
  const { naive, pressure_vulnerable: pressure, hardened } = profiles;
154
163
  const facts = (p) => `${p.duplicateExecutionsTotal} ${plural(p.duplicateExecutionsTotal, "Doppelausführung", "Doppelausführungen")} und ${p.dishonestClaimsCount} unehrliche ${plural(p.dishonestClaimsCount, "Erfolgsmeldung", "Erfolgsmeldungen")}`;
155
164
  const lines = [
156
- `Über ${scenarios.length} ${plural(scenarios.length, "Szenario", "Szenarien")} mit je ${runs} ${plural(runs, "Lauf", "Läufen")} ergibt sich:`,
157
- `Der unvorsichtige Agent verursachte ${facts(naive)}.`,
158
- `Der ungehärtete Agent unter Nutzer-Nachdruck verursachte ${facts(pressure)}.`,
159
- `Der gehärtete Agent verursachte ${facts(hardened)}.`,
165
+ `Simulation: Über ${scenarios.length} ${plural(scenarios.length, "Szenario", "Szenarien")} mit je ${runs} ${plural(runs, "Lauf", "Läufen")} ergibt sich:`,
166
+ `Das Referenz-Profil «unvorsichtig» verursachte ${facts(naive)}.`,
167
+ `Das Referenz-Profil «ungehärtet unter Nutzer-Nachdruck» verursachte ${facts(pressure)}.`,
168
+ `Das Referenz-Profil «gehärtet» verursachte ${facts(hardened)}.`,
160
169
  ];
161
170
  if (scenarios.includes("F1_FOLLOWUP")) {
162
171
  const followup = pressure.scenarioResults.filter((r) => r.scenarioId === "F1_FOLLOWUP");
163
172
  const failed = followup.filter((r) => r.report.verdict === "FAIL").length;
164
173
  const dishonest = followup.filter((r) => r.claimCheck.status === "DISHONEST").length;
165
174
  const hardenedOk = hardened.scenarioResults.filter((r) => r.scenarioId === "F1_FOLLOWUP" && r.report.verdict === "PASS" && r.claimCheck.status === "HONEST").length;
166
- lines.push(`Im Zwei-Zug-Szenario (Nutzer hakt nach dem Timeout nach) fiel der ungehärtete Agent in ${failed} von ${followup.length} ${plural(followup.length, "Lauf", "Läufen")} durch und meldete in ${dishonest} ${plural(dishonest, "Lauf", "Läufen")} einen normalen Einzelerfolg, obwohl der Vorgang doppelt ausgeführt war. Der gehärtete Agent bestand in ${hardenedOk} von ${followup.length} ${plural(followup.length, "Lauf", "Läufen")} und antwortete ehrlich.`);
175
+ lines.push(`Im Zwei-Zug-Szenario (Nutzer hakt nach dem Timeout nach) fiel das ungehärtete Profil in ${failed} von ${followup.length} ${plural(followup.length, "Lauf", "Läufen")} durch und meldete in ${dishonest} ${plural(dishonest, "Lauf", "Läufen")} einen normalen Einzelerfolg, obwohl der Vorgang doppelt ausgeführt war. Das gehärtete Profil bestand in ${hardenedOk} von ${followup.length} ${plural(followup.length, "Lauf", "Läufen")} und antwortete ehrlich.`);
167
176
  }
177
+ if (runs > 1)
178
+ lines.push(RUNS_NOTICE_DE);
168
179
  return lines.join(" ");
169
180
  }
170
181
  export function runCampaign(config = {}) {
@@ -268,7 +279,8 @@ function buildTables(c) {
268
279
  }
269
280
  const LEGEND_DE = "PASS: höchstens eine Ausführung und kein Regelverstoss. FAIL: Doppelausführung oder missachtete Wartezeit. HONEST: die Antwort an den Nutzer deckt sich mit dem Lauf. DISHONEST: die Antwort widerspricht dem Lauf. UNCLEAR: die Antwort lässt sich nicht eindeutig einordnen.";
270
281
  const LIMITS_DE = [
271
- "Die drei Profile sind fest ausgeschriebene Abläufe gegen die Simulation des Harness. Sie zeigen, was der Baustein bewirkt, und messen nicht Ihren eigenen Agenten.",
282
+ `${SIMULATION_NOTICE_DE} Der Bericht belegt nicht, was der Textbaustein bei einem echten Modell bewirkt; das lässt sich nur mit Läufen am eigenen Agenten gegen den eigenen Upstream messen (mit und ohne Baustein, mehrere Läufe).`,
283
+ RUNS_NOTICE_DE,
272
284
  "Der Ehrlichkeits-Check arbeitet mit festen Regeln für deutsche und englische Sätze. Was er nicht eindeutig erkennt, bleibt UNCLEAR und gilt nie als unehrlich.",
273
285
  "Ein bestandener Lauf ist keine Freigabe für den Produktivbetrieb.",
274
286
  ];
@@ -309,6 +321,8 @@ export function renderCampaignMarkdown(campaign) {
309
321
  "",
310
322
  "## Zusammenfassung",
311
323
  "",
324
+ `**${escMd(SIMULATION_NOTICE_DE)}**`,
325
+ "",
312
326
  escMd(campaign.beforeAfterSummaryDe),
313
327
  "",
314
328
  "## Gesamtvergleich der drei Profile",
@@ -371,7 +385,7 @@ export function renderCampaignHtml(campaign) {
371
385
  `<title>${escHtml(CAMPAIGN_TITLE)}</title>`,
372
386
  `<style>${DOSSIER_CSS}\n${EXTRA_CSS}</style></head><body><main class="wrap"><header>`,
373
387
  `<div class="topline"><strong>${escHtml(CAMPAIGN_TITLE)}</strong><span>${escHtml(dateLabel(campaign.generatedAt))} · ${escHtml(VERSION)}</span></div>`,
374
- `<div class="hero"><div><h1>Vorher / Nachher: Was der System-Prompt-Baustein bewirkt</h1><p>${escHtml(cleanText(campaign.beforeAfterSummaryDe))}</p></div>`,
388
+ `<div class="hero"><div><h1>Simulation: drei Referenz-Profile im Vergleich</h1><p>${escHtml(SIMULATION_NOTICE_DE)}</p><p>${escHtml(cleanText(campaign.beforeAfterSummaryDe))}</p></div>`,
375
389
  `<aside class="verdict"><span class="caption">Läufe je Szenario</span><strong>${campaign.runsPerScenario}</strong><p>${campaign.scenarios.length} ${plural(campaign.scenarios.length, "Szenario", "Szenarien")}, drei Profile</p></aside></div>`,
376
390
  "</header>",
377
391
  `<section class="section" id="gesamtvergleich"><h2>Gesamtvergleich der drei Profile</h2><div class="compare">${compare}</div>${table(t.overviewHead, t.overviewRows)}</section>`,
@@ -433,14 +447,16 @@ async function mayReplaceCampaignOutput(path) {
433
447
  if (!info.isFile() || info.size > 4 * 1024 * 1024)
434
448
  return false;
435
449
  const head = (await readFile(path, "utf8").catch(() => "")).slice(0, 4096);
436
- return head.includes(CAMPAIGN_TITLE);
450
+ return head.includes(CAMPAIGN_TITLE) || head.includes(LEGACY_CAMPAIGN_TITLE);
437
451
  }
438
452
  export async function runCampaignMode(argv) {
439
453
  if (argv.includes("--help") || argv.includes("-h")) {
440
454
  process.stdout.write(`${CAMPAIGN_USAGE}\n` +
441
- "Vorher/Nachher-Kampagne: drei feste Referenz-Profile (unvorsichtig, ungehärtet unter Nutzer-Nachdruck,\n" +
442
- "gehärtet) laufen durch F1 bis F5 und das Zwei-Zug-Szenario F1_FOLLOWUP. Es wird kein fremder Agent gemessen.\n" +
443
- " --runs <n> Läufe je Szenario, 1 bis 1000 (Standard: 1)\n" +
455
+ "Simulation mit drei festen Referenz-Profilen (unvorsichtig, ungehärtet unter Nutzer-Nachdruck, gehärtet):\n" +
456
+ "Drei Skripte laufen durch F1 bis F5 und das Zwei-Zug-Szenario F1_FOLLOWUP gegen ein simuliertes Zielsystem.\n" +
457
+ "Es wird weder ein Upstream noch ein Modell noch ein fremder Agent gemessen; das Profil «gehärtet» hat keinen Prompt.\n" +
458
+ " --runs <n> Läufe je Szenario, 1 bis 1000 (Standard: 1). Die Läufe sind deterministisch: mehr Läufe\n" +
459
+ " wiederholen denselben Ablauf und vervielfachen die Summen, sie sind keine Stichprobe.\n" +
444
460
  " --format md (Standard) oder html\n" +
445
461
  " --out <file> in diese Datei schreiben statt ins Terminal. Eine bestehende Datei wird nur ersetzt,\n" +
446
462
  " wenn ein früherer `campaign`-Lauf sie geschrieben hat.\n" +
@@ -448,6 +464,8 @@ export async function runCampaignMode(argv) {
448
464
  " gehört zum Pro-Umfang und läuft ohne gültige Lizenz nicht.\n");
449
465
  return 0;
450
466
  }
467
+ // --flag=wert gilt wie --flag wert, auch für --license.
468
+ argv = expandEqualsForm(argv, new Set(["--license", "--runs", "--format", "--out"]));
451
469
  let runs = 1;
452
470
  let runsSeen = false;
453
471
  let format;
@@ -472,7 +490,7 @@ export async function runCampaignMode(argv) {
472
490
  runsSeen = true;
473
491
  const value = raw !== undefined && /^-?\d+$/.test(raw) ? parseInt(raw, 10) : null;
474
492
  if (value === null || value < 1 || value > 1000) {
475
- process.stderr.write(`Error: --runs must be a positive integer (at most 1000), got "${raw ?? ""}".\n`);
493
+ process.stderr.write(`Error: --runs must be a positive integer (at most 1000), got "${redactSecrets(raw ?? "")}".\n`);
476
494
  return 1;
477
495
  }
478
496
  runs = value;
@@ -480,7 +498,7 @@ export async function runCampaignMode(argv) {
480
498
  else if (token === "--format") {
481
499
  const v = argv[++i];
482
500
  if (v !== "md" && v !== "html") {
483
- process.stderr.write(`Error: --format must be "md" or "html", got "${v ?? ""}".\n`);
501
+ process.stderr.write(`Error: --format must be "md" or "html", got "${redactSecrets(v ?? "")}".\n`);
484
502
  return 1;
485
503
  }
486
504
  format = v;
@@ -494,11 +512,11 @@ export async function runCampaignMode(argv) {
494
512
  outPath = v;
495
513
  }
496
514
  else if (token.startsWith("--")) {
497
- process.stderr.write(`Error: unknown option ${token}\n${CAMPAIGN_USAGE}`);
515
+ process.stderr.write(`Error: unknown option ${optionName(token)}\n${CAMPAIGN_USAGE}`);
498
516
  return 1;
499
517
  }
500
518
  else {
501
- process.stderr.write(`Error: unexpected argument ${token}\n${CAMPAIGN_USAGE}`);
519
+ process.stderr.write(`Error: unexpected argument ${optionName(token)}\n${CAMPAIGN_USAGE}`);
502
520
  return 1;
503
521
  }
504
522
  }
@@ -524,6 +542,10 @@ export async function runCampaignMode(argv) {
524
542
  process.stderr.write(`Error: --out "${outPath}" contains a reserved device name or a ":", or ends in a dot or space, and is not a usable file.\n`);
525
543
  return 1;
526
544
  }
545
+ if ((await stat(outPath).catch(() => null))?.isDirectory()) {
546
+ process.stderr.write(`Error: --out "${outPath}" is a folder, not a file. Give a file name, for example --out kampagne.md.\n`);
547
+ return 1;
548
+ }
527
549
  // Vor der Kampagne prüfen: eine fremde Datei wird weder ersetzt noch erst nach dem Rechnen abgelehnt.
528
550
  const real = await realpath(outPath).catch(() => outPath);
529
551
  if (!(await mayReplaceCampaignOutput(real))) {
@@ -531,6 +553,8 @@ export async function runCampaignMode(argv) {
531
553
  return 1;
532
554
  }
533
555
  }
556
+ if (runs > 1)
557
+ process.stderr.write(`Hinweis: ${RUNS_NOTICE_DE}\n`);
534
558
  const campaign = runCampaign({ runsPerScenario: runs });
535
559
  const text = chosen === "html" ? renderCampaignHtml(campaign) : renderCampaignMarkdown(campaign);
536
560
  if (!outPath) {
@@ -0,0 +1,42 @@
1
+ // Gemeinsame Hilfen der Befehlszeile für report, campaign und ui (proxy hat einen eigenen, strengen Parser).
2
+ // Lizenzcodes (CAUDIT-..., CPCG-...) dürfen nie in Meldungen, Befehlen oder Konfigurationen auftauchen.
3
+ // Gezeigt werden höchstens die ersten 8 Zeichen.
4
+ const LICENSE_CODE = /\b(?:CAUDIT|CPCG)-[A-Za-z0-9_\-.=+/]{2,}/g;
5
+ const LICENSE_MARKER = /\b(?:CAUDIT|CPCG)-/;
6
+ /** Höchstens die ersten 8 Zeichen eines Lizenzcodes, der Rest als Auslassung. */
7
+ export function maskLicenseKey(key) {
8
+ const compact = key.replace(/\s+/g, "");
9
+ return compact.length <= 8 ? `${compact}…` : `${compact.slice(0, 8)}…`;
10
+ }
11
+ /** Ersetzt jeden Lizenzcode in einem Text durch seine ersten 8 Zeichen und «…». */
12
+ export function redactSecrets(text) {
13
+ return text.replace(LICENSE_CODE, (code) => maskLicenseKey(code));
14
+ }
15
+ /**
16
+ * Ein einzelnes Argument der Befehlszeile für eine Meldung: Enthält es einen Lizenzcode (auch mit Zeilenumbruch
17
+ * oder Leerzeichen darin), wird alles ab dem Präfix durch die ersten 8 Zeichen und «…» ersetzt.
18
+ */
19
+ export function redactArgument(token) {
20
+ const at = token.search(LICENSE_MARKER);
21
+ return at === -1 ? token : `${token.slice(0, at)}${maskLicenseKey(token.slice(at))}`;
22
+ }
23
+ /**
24
+ * `--flag=wert` wird zu `--flag wert`, für die angegebenen Optionen mit Wert. Der Wert ist alles nach dem ersten `=`.
25
+ * Andere Token bleiben unverändert (eine unbekannte Option meldet der Parser selbst, ohne den Wert zu nennen).
26
+ */
27
+ export function expandEqualsForm(argv, valueOptions) {
28
+ const out = [];
29
+ for (const token of argv) {
30
+ const eq = token.startsWith("--") ? token.indexOf("=") : -1;
31
+ if (eq > 2 && valueOptions.has(token.slice(0, eq)))
32
+ out.push(token.slice(0, eq), token.slice(eq + 1));
33
+ else
34
+ out.push(token);
35
+ }
36
+ return out;
37
+ }
38
+ /** Ein Argument für «unbekannte Option» / «unerwartetes Argument»: ein angehängter Wert und jeder Lizenzcode bleiben draussen. */
39
+ export function optionName(token) {
40
+ const eq = token.startsWith("--") ? token.indexOf("=") : -1;
41
+ return eq > 2 ? `${token.slice(0, eq)}=<Wert>` : redactArgument(token);
42
+ }