ADR-v2-132 — Assisted bug reporting: what a "send this as an issue" button may do¶
Status: (b) accepted and implemented (2026-08-18); (a) superseded; © still undecided and still needs its own ADR. Superseded the original "nothing decided" status below. (b) was implementable without a decision from the decider because it adds no egress path — the browser makes the request, not YazSes — so accepting it commits nothing that ADR-011 or ADR-019 would have to be reopened to undo. © remains open and is the only part that is a decision; see the open questions, where question 3 is now answered. Originally drafted (2026-08-17) in response to an inbox request: "improve the error detection of yazses and create notification for error it detects for the user and say what user should do, also capabilities of the automatic error report — can you do it with github issues, user just accepts and allows and it automatically sends error as bug" Deciders: Mohsen Seyedkazemi Ardebili Context links: [[adr-011]] (zero telemetry — the promise this must not break), [[adr-019]] (the egress inventory and the rule for adding an eighth), [[adr-012]] (the learning corpus is machine-bound and encrypted)
Context¶
Two of the three things asked for already exist, and it is worth being precise about which, because it changes the size of the decision.
Error detection exists. The daemon already classifies every burst's outcome, and as of this cycle summarises it (typed: 6 of 14 recent bursts). doctor runs eleven checks, verify walks the real chain and names the first broken link, report builds a redacted local bundle.
Telling the user what to do exists. system/notify.py sends actionable desktop toasts with buttons, and the mic guard uses them. Every diagnostic message added this cycle names a command to run.
Filing the issue does not exist, and it is the only part that is a decision rather than work. Everything above is local. Filing crosses the machine boundary.
Why this cannot be a small feature¶
ADR-011 promises nothing leaves the machine. ADR-019 makes that checkable: seven outbound paths are enumerated, tests/test_egress_inventory.py fails the build when an eighth appears unregistered, and of the seven exactly two can carry what the user said — llm_cleanup (confined to loopback) and remote/local_proxy (the host named on the command line).
An issue filer would be the eighth, and unlike the model-weight fetchers it uploads content from this machine to a third party. It is the first path whose payload is assembled from the user's own logs and configuration rather than being a request for a public artifact.
That is not a reason to refuse it. It is a reason for it to be designed rather than added.
Decision¶
Not yet taken. Three options, with what each costs.
(a) Do nothing beyond today¶
yazses report already writes a redacted bundle and prints the issue URL. The user attaches it themselves.
For: zero new egress, zero new failure modes, and the user sees exactly what is sent because they send it. Against: it is the step where most people stop. A bug that is easy to hit and tedious to report is a bug that gets hit repeatedly and reported once.
(b) Prepare, never send — recommended¶
A notification offers "Prepare a bug report". YazSes builds the bundle, opens the browser at GitHub's issue form with the title and body pre-filled, and attaches nothing automatically. The user reads the filled form and presses submit — in GitHub's UI, with their own account, having seen every word.
For: removes the tedium, not the consent. Adds no egress path at all — the browser makes the request, not YazSes, so ADR-019's inventory is unchanged and the eighth entry is never created. Works with no token, no account setup, and no new dependency. Against: the body must fit in a URL (~8 kB practical limit), so the bundle is summarised rather than complete; large logs still need a manual attachment.
© File directly via the GitHub API¶
YazSes holds a token and POSTs the issue.
For: one click, complete payload. Against: the eighth egress path, and the worst-shaped one — it needs a credential, it can file without the user seeing the final text, a bug in redaction becomes a public disclosure with no human in the loop, and "user just accepts" is consent to a category rather than to a specific payload. It also invites the failure mode where a crash loop files a hundred issues, which is precisely the shape of the #309 flood in the tracker today.
Consequences if (b) is chosen¶
report.pygains asummarise_for_issue()— pure, size-bounded, reusing the existing redaction rather than a second one. Redaction must be one implementation or the two will drift, which is this repo's most frequent defect.- The toast gains one button.
notify.pyalready supports actions and already degrades to a plain toast, then to log-only, and never raises. - Reuses
system/browser.py::open_url, the single URL opener, which returns a bool and never raises. - ADR-019's inventory does not change, and its test should keep passing untouched. If implementing this requires editing that test, the implementation has gone wrong.
- Needs a guard that the prepared body contains no
[REDACTED]-eligible pattern — the existingredact_texttests extended to the issue body, not duplicated.
Open questions for the decider¶
- Still open. Is (b) enough, or is the one-click of © the point of the request? (b) is now built and can be used; nothing about it forecloses ©.
- Still open. If © is ever wanted, it needs its own ADR: token storage, rate limiting, and an explicit answer to "what stops a crash loop filing a hundred issues".
Should the offer appear only on a repeated fault, the way the mic guard waits for a streak?Answered by the implementation, with a better rule than repetition.
The offer appears only when YazSes could not identify the failure — Diagnosis.slug starting with unknown-. A recognised fault already carries the command that fixes it, and an issue about a missing ydotool helps nobody, least of all the person who now has two things to do instead of one. Repetition would have gated on the wrong axis: a recognised fault repeated ten times still needs no issue, and an unrecognised one is worth reporting the first time.
Dismissal-fatigue is handled separately and unconditionally, by system/diagnosis.py::should_notify: any given diagnosis is shown at most once per five minutes, keyed by slug, so a fault that recurs on every burst is explained once. Two different faults never silence each other.
What was built for (b)¶
system/diagnosis.py— the classifier that makes a failure actionable, and the source of theunknown-signal that gates the offer. Pure, total, and advice-only.system/report.py::summarise_for_issue/issue_url— size-bounded body reusingcollect's single redaction implementation, then a pre-filled form URL.core/daemon.py::_report_failure/_prepare_bug_report— the toast, and the button.
One consequence worth recording, because it looked at first like the ADR's own tripwire firing. ADR-019's guard rejected from urllib.parse import urlencode in report.py: its root list contains urllib, deliberately conservatively, even though urllib.parse cannot open a socket. The two available responses were both wrong — registering report.py in the egress inventory would state that a module which sends nothing sends something, and relaxing the guard would trade a real protection for a convenience in the one module whose first line is "Nothing is ever sent anywhere". So the percent-encoder is written by hand there and pinned against urllib.parse.quote in the tests, which are outside the scanned tree. The inventory is unchanged and tests/test_egress_inventory.py was not edited, which is the condition this ADR set.