cli(doctor --issue): cap body at GitHub's 64KB limit + dedicated errors section
GitHub issue bodies have a hard ~64KB limit. The pre-fix bundlers dumped the full 400-line log tail unconditionally, so long-running sessions produced reports that exceeded the limit. Worse, when an issue body was over budget the GitHub API rejected the whole payload — the bundler completed, but the issue couldn't be opened. Two changes: 1. New "## Recent errors (last 20, sanitized)" section above the main log tail. The body cap (point 2 below) only shrinks the main log block, so promoting recent errors into their own section guarantees they survive truncation. We scan the last 4000 log lines for ERROR-shaped patterns: - `failed:` (Magic Context's sessionLog convention) - `Error:` / `TypeError:` / typed-error rendering - `EMERGENCY` (95% abort marker) - `exception` (generic throw text) - V8/JSC stack-trace frames (` at SomeFn (file:line:col)`) The `failed:` colon suffix is deliberate — it avoids the common false positive where a sessionLog message includes "failed" as past-tense status (e.g. `historian: 12 published; 0 failed` is telemetry, not an error). Stack frames are kept so the agent reading the issue sees enough context to identify the call site. 2. New `capBodyToGithubLimit(body, maxBytes = 60_000)` helper that enforces the byte budget. Budget is 60KB (4KB headroom below GH's 64KB cap for URL encoding via `gh issue create --web` and future minor section growth). When the body exceeds budget, the helper rewrites the main `## Log (last N lines, sanitized)` fenced block — drops oldest lines first (keeps newest) until the body fits, prefixes a visible `[truncated for GitHub 64KB limit — older log lines dropped]` marker. Diagnostics, configuration, historian failure signals, and recent-errors sections are preserved intact. UTF-8 byte length is the budget. The fallback path (no log heading found) uses a code-point-aware byte truncator so cutting mid-character doesn't produce U+FFFD replacement bytes that push the output back over budget. Both helpers live in a new shared `issue-body.ts` so OpenCode and Pi share the same budget, same truncation marker, and same precision/false-positive tradeoff on what counts as an error line. Tests: 14 new in `issue-body.test.ts` covering: - Documented sessionLog shapes + "0 failed" false-positive - V8 stack-trace frame matching - Chronological-order output / newest-first selection - Limit cap behavior - Pass-through under budget - Truncation when over budget (correct marker, oldest dropped first) - Recent errors section survives truncation - Description/Environment sections preserved - Default MAX_GITHUB_BODY_BYTES applied - UTF-8-safe fallback truncation CLI suite: 138 pass / 2 skip / 0 fail (+14 from 124 baseline). Typecheck, lint, format, build all clean across all 3 packages.
U
ualtinok committed
1bf4a641bf968a09d851a8ee82abf0f88c040609
Parent: aa1356b