Fehlerausgabe trägt die Abhilfe: ERROR-Zeile plus Hinweis aus dem Kommando-Datensatz #143

Closed
opened 2026-09-25 20:36:00 +00:00 by torben · 2 comments
Owner

Erledigt (2026-09-26). Umgesetzt und veröffentlicht: 63f566f (Code, Tests, AGENTS.md, tools/CONTRACT.md, Version) und 16c911f (Prosa-Nachzug in README.md). Stand im laufenden Kandidaten: 7.1.0-beta.22. Folgebefunde: #147, #148, Nachtrag in #146.

Problem (Ausgangslage)

Den Fehlervertrag eines Kommandos (Ursache → Reaktion) braucht ein Agent genau dann, wenn das Kommando scheitert. Bis zu diesem Paket gab _util.fail() nur eine ERROR-Zeile aus, auf stdout über die Rich-console. Die Reaktion stand im Abschnitt ON FAILURE von wikitool <cmd> -h, also nur über einen zweiten Aufruf erreichbar. Genau dieses Nachschlagen wird in der Hitze eines Fehlschlags am ehesten übersprungen.

Exit 42 war nicht betroffen: Die Gates drucken ihre Wiederholungszeile selbst.

Ergebnis

Endet ein Aufruf über _util.fail() mit Exit 1, gibt er die Ausgabe in dieser Form aus:

ERROR <msg>                                 ← stdout, byte-gleich wie vorher
ON FAILURE (wikitool <path> -h):            ← stderr, Klartext
    <cause> -> <reaction>
    <label>: <cause> -> <reaction>

Hat der Datensatz keine Exit-1-Ursache mit Reaktion, erscheint nur see: wikitool <path> -h. Ohne Click-Kontext, ohne Datensatz oder bei einem Fehler beim Rendern erscheint kein Hinweis, und das Verhalten ist dasselbe wie vorher.

Entscheidungen

  • D1 – c: ON FAILURE inline. Der Hinweis zeigt die Failures des laufenden Kommandos mit code == 1 und nicht leerer reaction, jeweils in der Form von -h. Das ging erst, seit #142 je Ursache einen eigenen Failure-Eintrag eingeführt hat.
    • Verworfen a (nur Verweis): Das Nachschlagen, um das es in diesem Paket geht, bliebe bestehen.
    • Verworfen b (ursachengenau): Alle 231 fail()-Aufrufe in 26 Dateien hätten einer Ursache zugeordnet werden müssen, dazu ein neuer Schlüssel an Failure, der driften kann, und eine eigene docs verify-Prüfung. b bleibt als Ausbau möglich, falls Messung zeigt, dass Agenten die Ursache falsch zuordnen oder abgelehnte Aufrufe unverändert wiederholen (Trajektorien-Regel „refused call repeated unchanged“, siehe #52).
    • Umfang, gemessen über alle 61 Datensätze: Median 2 Zeilen / 242 Zeichen, Maximum 9 Zeilen (dist upgrade) bzw. 1811 Zeichen (raw accept). Es wird nicht gekürzt.
  • D2 – stderr, Klartext. Der Hinweis wird mit sys.stderr.write geschrieben, nicht über Rich: Reaktionstexte enthalten [--flag], und Rich würde das als Markup verschlucken. Die ERROR-Zeile bleibt unverändert auf stdout.
  • D3 – Rückfall. Ein Datensatz ohne Exit-1-Ursache bekommt die see:-Zeile; heute betrifft das nur docs contract, siehe #146. Ohne Click-Kontext gibt es keinen Hinweis; das betrifft die Budget-Gate- und Loop-Breaker-Verweigerung in cli.main(), deren Traceback ist #147.

Wie gebaut

  • B1 – cli_contract.py:
    • path_of(ctx) ist öffentlich und ersetzt das bisherige cli._contract_path; cli.py ruft es jetzt dort auf.
    • render_failure_hint(rec) ist neu. render_on_failure_lines blieb unverändert, weil -h und die Markdown-Region weiterhin alle Codes zeigen; der Filter auf Code 1 steht in der neuen Funktion.
  • B2 – _util.fail():
    • Reihenfolge: _declined = True → ERROR → console.file.flush() → Hinweis auf stderr → typer.Exit(1).
    • Den Kontext holt typer._click.globals.get_current_context(silent=True), dieselbe private Modulquelle wie beim Hilfe-Patch in cli.py.
    • Import, Lookup und Rendern stehen in try/except Exception.
  • B3 – Tests (13 neu, volle Suite 1535):
    • test_cli_contract.py: Filter auf Code 1 mit Reaktion, see:-Rückfall, path_of.
    • test_util.py: Hinweis nur auf stderr bei unverändertem stdout, Rückfall, kein Kontext, kein Datensatz, Renderfehler wird geschluckt.
    • test_cli.py: echte Kommandos touch und xref add über die Kommando-Fixture; ein Subprozess mit gemergten Streams (docs contract gegen ein leeres CHEMENU_ROOT) für Reihenfolge und Rückfall; Refund über _run_traced.
    • Das befürchtete Risiko trat nicht ein: Keine bestehende CliRunner-Assertion vergleicht result.output exakt.
  • B4 – Doku:
    • AGENTS.md § Tool error contract, Fall 2.
    • tools/CONTRACT.md § Usage, ohne Issue-Nummer, weil die Datei ausgeliefert wird.
    • Docstrings von Failure und fail().
    • README.md (Nachzug in stack-close).
    • tools/README.md beschreibt die Fehlerausgabe nicht und blieb unverändert.
  • B5 – Version: version bump --minor --impact medium → 7.1.0-beta.22, mit Changeset-Eintrag in CHANGES.md.

Invarianten

  • Exit-Codes ändern sich nicht, auch nicht bei einem Renderfehler oder ohne Kontext (test_fail_swallows_a_hint_rendering_failure, test_fail_without_a_click_context_prints_no_hint).
  • Budget-Refund unverändert (test_declined_call_is_still_refunded_after_the_new_stderr_hint).
  • stdout bleibt byte-gleich; der Hinweis steht nur auf stderr.
  • Unter 2>&1 steht die ERROR-Zeile zuerst (Subprozess-Test).

Akzeptanzkriterien

  • Jeder Aufruf, der innerhalb eines Kommandos über _util.fail() mit Exit 1 endet, gibt nach der ERROR-Zeile den Hinweis bzw. die see:-Zeile aus. Belegt über ein Fixture-Kommando und drei echte Kommandos (touch, xref add, docs contract).
  • Einträge mit code 0 oder 42 und Einträge ohne reaction erscheinen nicht im Hinweis (test_render_failure_hint_shows_only_exit_1_causes_with_a_reaction, dazu test_fail_prints_the_hint_on_stderr_leaving_stdout_untouched).
  • stdout ist im Fehlerfall byte-gleich; der Hinweis steht nur auf stderr (test_util.py prüft stdout exakt).
  • Unter 2>&1 steht die ERROR-Zeile vor dem Hinweis (test_stderr_hint_follows_the_error_line_when_streams_are_merged).
  • Der Exit-Code bleibt 1 bei einem Renderfehler und ohne Click-Kontext (Tests oben).
  • Der Budget-Refund nach fail() ist unverändert (Test oben).
  • cli.py hat keine eigene Pfadauflösung mehr und nutzt cli_contract.path_of.
  • AGENTS.md § Tool error contract und tools/CONTRACT.md § Usage beschreiben den Hinweis. docs verify, instructions verify und die volle pytest-Suite (1535) waren lokal grün. CI war für 63f566f grün (Läufe 403 und 404) und ebenso für 16c911f (siehe Abschlusskommentar).

Außerhalb dieses Pakets

  • #147 – Die Budget-Gate- und Loop-Breaker-Verweigerung druckt nach der ERROR-Zeile einen Python-Traceback, und ihre Meldungen verweisen auf Abschnitte, die es nicht gibt.
  • #148 – cli.py setzt den Hilfe-Patch ein zweites Mal außerhalb des try und hebt damit den dokumentierten Rückfall auf.
  • #146 (Nachtrag) – Der Datensatz von docs contract nennt keine Exit-1-Ursache, obwohl der Code eine kennt.
  • instructions/gates.md sagt in der Exit-Tabelle weiter „Read the ERROR line“. Das ist nicht falsch, weil die Regel in AGENTS.md steht; die Zeile wurde deshalb bewusst nicht angefasst.

Ablauf

  • Entwurf, D1-Entscheidung mit dem Betreiber und Bauplan: Claude Opus 5.5.
  • Umsetzung (Code, Tests, Doku, Version, Publish): Claude Sonnet 5.
  • Abschluss (README-Nachzug, Body, Schließen): Claude Opus 5.5.
  • Der Publish von 63f566f lief bei genau 10 Dateien ins Mass-Update Gate und ging nach Freigabe durch den Betreiber durch.
**Erledigt (2026-09-26).** Umgesetzt und veröffentlicht: `63f566f` (Code, Tests, AGENTS.md, `tools/CONTRACT.md`, Version) und `16c911f` (Prosa-Nachzug in README.md). Stand im laufenden Kandidaten: 7.1.0-beta.22. Folgebefunde: #147, #148, Nachtrag in #146. ## Problem (Ausgangslage) Den Fehlervertrag eines Kommandos (Ursache → Reaktion) braucht ein Agent genau dann, wenn das Kommando scheitert. Bis zu diesem Paket gab `_util.fail()` nur eine `ERROR`-Zeile aus, auf stdout über die Rich-`console`. Die Reaktion stand im Abschnitt ON FAILURE von `wikitool <cmd> -h`, also nur über einen zweiten Aufruf erreichbar. Genau dieses Nachschlagen wird in der Hitze eines Fehlschlags am ehesten übersprungen. Exit 42 war nicht betroffen: Die Gates drucken ihre Wiederholungszeile selbst. ## Ergebnis Endet ein Aufruf über `_util.fail()` mit Exit 1, gibt er die Ausgabe in dieser Form aus: ``` ERROR <msg> ← stdout, byte-gleich wie vorher ON FAILURE (wikitool <path> -h): ← stderr, Klartext <cause> -> <reaction> <label>: <cause> -> <reaction> ``` Hat der Datensatz keine Exit-1-Ursache mit Reaktion, erscheint nur `see: wikitool <path> -h`. Ohne Click-Kontext, ohne Datensatz oder bei einem Fehler beim Rendern erscheint kein Hinweis, und das Verhalten ist dasselbe wie vorher. ## Entscheidungen - **D1 – c: ON FAILURE inline.** Der Hinweis zeigt die `Failure`s des laufenden Kommandos mit `code == 1` und nicht leerer `reaction`, jeweils in der Form von `-h`. Das ging erst, seit #142 je Ursache einen eigenen `Failure`-Eintrag eingeführt hat. - Verworfen **a (nur Verweis)**: Das Nachschlagen, um das es in diesem Paket geht, bliebe bestehen. - Verworfen **b (ursachengenau)**: Alle 231 `fail()`-Aufrufe in 26 Dateien hätten einer Ursache zugeordnet werden müssen, dazu ein neuer Schlüssel an `Failure`, der driften kann, und eine eigene `docs verify`-Prüfung. **b** bleibt als Ausbau möglich, falls Messung zeigt, dass Agenten die Ursache falsch zuordnen oder abgelehnte Aufrufe unverändert wiederholen (Trajektorien-Regel „refused call repeated unchanged“, siehe #52). - Umfang, gemessen über alle 61 Datensätze: Median 2 Zeilen / 242 Zeichen, Maximum 9 Zeilen (`dist upgrade`) bzw. 1811 Zeichen (`raw accept`). Es wird nicht gekürzt. - **D2 – stderr, Klartext.** Der Hinweis wird mit `sys.stderr.write` geschrieben, nicht über Rich: Reaktionstexte enthalten `[--flag]`, und Rich würde das als Markup verschlucken. Die `ERROR`-Zeile bleibt unverändert auf stdout. - **D3 – Rückfall.** Ein Datensatz ohne Exit-1-Ursache bekommt die `see:`-Zeile; heute betrifft das nur `docs contract`, siehe #146. Ohne Click-Kontext gibt es keinen Hinweis; das betrifft die Budget-Gate- und Loop-Breaker-Verweigerung in `cli.main()`, deren Traceback ist #147. ## Wie gebaut - **B1 – `cli_contract.py`:** - `path_of(ctx)` ist öffentlich und ersetzt das bisherige `cli._contract_path`; `cli.py` ruft es jetzt dort auf. - `render_failure_hint(rec)` ist neu. `render_on_failure_lines` blieb unverändert, weil `-h` und die Markdown-Region weiterhin alle Codes zeigen; der Filter auf Code 1 steht in der neuen Funktion. - **B2 – `_util.fail()`:** - Reihenfolge: `_declined = True` → `ERROR` → `console.file.flush()` → Hinweis auf stderr → `typer.Exit(1)`. - Den Kontext holt `typer._click.globals.get_current_context(silent=True)`, dieselbe private Modulquelle wie beim Hilfe-Patch in `cli.py`. - Import, Lookup und Rendern stehen in `try/except Exception`. - **B3 – Tests** (13 neu, volle Suite 1535): - `test_cli_contract.py`: Filter auf Code 1 mit Reaktion, `see:`-Rückfall, `path_of`. - `test_util.py`: Hinweis nur auf stderr bei unverändertem stdout, Rückfall, kein Kontext, kein Datensatz, Renderfehler wird geschluckt. - `test_cli.py`: echte Kommandos `touch` und `xref add` über die Kommando-Fixture; ein Subprozess mit gemergten Streams (`docs contract` gegen ein leeres `CHEMENU_ROOT`) für Reihenfolge und Rückfall; Refund über `_run_traced`. - Das befürchtete Risiko trat nicht ein: Keine bestehende `CliRunner`-Assertion vergleicht `result.output` exakt. - **B4 – Doku:** - AGENTS.md § Tool error contract, Fall 2. - `tools/CONTRACT.md` § Usage, ohne Issue-Nummer, weil die Datei ausgeliefert wird. - Docstrings von `Failure` und `fail()`. - README.md (Nachzug in `stack-close`). - `tools/README.md` beschreibt die Fehlerausgabe nicht und blieb unverändert. - **B5 – Version:** `version bump --minor --impact medium` → 7.1.0-beta.22, mit Changeset-Eintrag in `CHANGES.md`. ## Invarianten - [x] Exit-Codes ändern sich nicht, auch nicht bei einem Renderfehler oder ohne Kontext (`test_fail_swallows_a_hint_rendering_failure`, `test_fail_without_a_click_context_prints_no_hint`). - [x] Budget-Refund unverändert (`test_declined_call_is_still_refunded_after_the_new_stderr_hint`). - [x] stdout bleibt byte-gleich; der Hinweis steht nur auf stderr. - [x] Unter `2>&1` steht die `ERROR`-Zeile zuerst (Subprozess-Test). ## Akzeptanzkriterien - [x] Jeder Aufruf, der innerhalb eines Kommandos über `_util.fail()` mit Exit 1 endet, gibt nach der `ERROR`-Zeile den Hinweis bzw. die `see:`-Zeile aus. Belegt über ein Fixture-Kommando und drei echte Kommandos (`touch`, `xref add`, `docs contract`). - [x] Einträge mit `code` 0 oder 42 und Einträge ohne `reaction` erscheinen nicht im Hinweis (`test_render_failure_hint_shows_only_exit_1_causes_with_a_reaction`, dazu `test_fail_prints_the_hint_on_stderr_leaving_stdout_untouched`). - [x] stdout ist im Fehlerfall byte-gleich; der Hinweis steht nur auf stderr (`test_util.py` prüft stdout exakt). - [x] Unter `2>&1` steht die `ERROR`-Zeile vor dem Hinweis (`test_stderr_hint_follows_the_error_line_when_streams_are_merged`). - [x] Der Exit-Code bleibt 1 bei einem Renderfehler und ohne Click-Kontext (Tests oben). - [x] Der Budget-Refund nach `fail()` ist unverändert (Test oben). - [x] `cli.py` hat keine eigene Pfadauflösung mehr und nutzt `cli_contract.path_of`. - [x] AGENTS.md § Tool error contract und `tools/CONTRACT.md` § Usage beschreiben den Hinweis. `docs verify`, `instructions verify` und die volle `pytest`-Suite (1535) waren lokal grün. CI war für `63f566f` grün (Läufe 403 und 404) und ebenso für `16c911f` (siehe Abschlusskommentar). ## Außerhalb dieses Pakets - #147 – Die Budget-Gate- und Loop-Breaker-Verweigerung druckt nach der `ERROR`-Zeile einen Python-Traceback, und ihre Meldungen verweisen auf Abschnitte, die es nicht gibt. - #148 – `cli.py` setzt den Hilfe-Patch ein zweites Mal außerhalb des `try` und hebt damit den dokumentierten Rückfall auf. - #146 (Nachtrag) – Der Datensatz von `docs contract` nennt keine Exit-1-Ursache, obwohl der Code eine kennt. - `instructions/gates.md` sagt in der Exit-Tabelle weiter „Read the `ERROR` line“. Das ist nicht falsch, weil die Regel in AGENTS.md steht; die Zeile wurde deshalb bewusst nicht angefasst. ## Ablauf - Entwurf, D1-Entscheidung mit dem Betreiber und Bauplan: Claude Opus 5.5. - Umsetzung (Code, Tests, Doku, Version, Publish): Claude Sonnet 5. - Abschluss (README-Nachzug, Body, Schließen): Claude Opus 5.5. - Der Publish von `63f566f` lief bei genau 10 Dateien ins Mass-Update Gate und ging nach Freigabe durch den Betreiber durch.
torben added the prio/plannedsize/Marea/processkind/decisionstatus/blocked labels 2026-09-25 20:36:00 +00:00
torben added kind/build and removed kind/decisionstatus/blocked labels 2026-09-26 11:46:53 +00:00
Author
Owner

Changelog:

  • Blocker entfallen (#121, #142 geschlossen): status/blocked ist entfernt, kind/decision wurde zu kind/build.
  • D1 ist entschieden, und zwar für die neue Variante c, nicht für den bisherigen Vorschlag a: fail() druckt die Exit-1-Zeilen aus ON FAILURE inline. Das ist erst seit #142 möglich (ein Failure je Ursache). a und b sind mit Begründung verworfen.
  • D2 ist entschieden: stderr, als Klartext ohne Rich. Neu ist D3 (Rückfall).
  • Neu: Ausgabeform, Bauplan B1–B5 und endgültige Akzeptanzkriterien.
  • Nebenbefunde ausgelagert: #147 (Traceback bei Gate-Verweigerung), #148 (Hilfe-Patch außerhalb von try) und ein Nachtrag in #146 (docs contract).
**Changelog:** - Blocker entfallen (#121, #142 geschlossen): `status/blocked` ist entfernt, `kind/decision` wurde zu `kind/build`. - D1 ist entschieden, und zwar für die neue Variante **c**, nicht für den bisherigen Vorschlag **a**: `fail()` druckt die Exit-1-Zeilen aus ON FAILURE inline. Das ist erst seit #142 möglich (ein `Failure` je Ursache). **a** und **b** sind mit Begründung verworfen. - D2 ist entschieden: stderr, als Klartext ohne Rich. Neu ist D3 (Rückfall). - Neu: Ausgabeform, Bauplan B1–B5 und endgültige Akzeptanzkriterien. - Nebenbefunde ausgelagert: #147 (Traceback bei Gate-Verweigerung), #148 (Hilfe-Patch außerhalb von `try`) und ein Nachtrag in #146 (`docs contract`).
Author
Owner

Changelog: Body auf den Endstand umgeschrieben: Bauplan → „Wie gebaut“, alle Invarianten und Akzeptanzkriterien abgehakt, die Testnamen als Belege genannt, Modellwahl je Phase unter „Ablauf“. Neu ist der README-Nachzug 16c911f. CI war grün für 63f566f (Läufe 403, 404) und 16c911f (Lauf 405). Geschlossen.

**Changelog:** Body auf den Endstand umgeschrieben: Bauplan → „Wie gebaut“, alle Invarianten und Akzeptanzkriterien abgehakt, die Testnamen als Belege genannt, Modellwahl je Phase unter „Ablauf“. Neu ist der README-Nachzug `16c911f`. CI war grün für `63f566f` (Läufe 403, 404) und `16c911f` (Lauf 405). Geschlossen.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#143