ModernUO/dev-docs/claude-skills/modernuo-bug-reporting.md
Kamron Batman 540559fbac
docs: upstream bug-reporting process for forks, and comments explain why (#2649)
## Summary

Two workflow additions to `CLAUDE.md`, with the detail in `dev-docs/`, plus a GitHub issue form.

### Rule 21 — comments explain why, never what changed
- Keep invariants, protocol/era quirks, value couplings, and the reason a workaround exists. One line where one line will do.
- Development narrative does not ship: before a PR leaves draft, sweep `git diff main...HEAD` for added comments and remove change history ("previously", "changed from"), review dialogue ("per review"), hedges ("I think"), and commented-out code. What a future reader still needs goes in the commit message or PR description.
- New `## Comments` section in `dev-docs/code-standards.md`; rule 21 in the `modernuo-code-audit` skill.

### Workflow Rules — bugs you were not asked to fix
Written for forks and custom projects built on ModernUO, which inherit this repo's `CLAUDE.md`. Also applies here (upstream is `origin`).

1. **Classify** — exploit-class (duplication, player-triggerable crash, auth bypass) goes to private disclosure only (`hi@modernuo.com`, per `CONTRIBUTING.md`), never a public issue, PR, or Discord post.
2. **Verify** the defective lines exist verbatim in upstream `main` via read-only `gh api`. If they don't, it is the fork's bug and nothing leaves the fork. This is also what mechanically keeps custom code out of reports: only lines that pass the check may be quoted.
3. **Dedup** — search upstream issues and PRs (all states) by file, symbol, and symptom, plus recent commits on the path. A merged fix → offer to import it; an open issue → offer to comment there.
4. **Draft, show, offer, wait** — the draft and a *scrub ledger* (what was removed, what was verified upstream, what is new code) are shown in full. The user picks: file an issue, open a PR, comment, draft a Discord post for https://muo.gg/discord, or nothing. A standing or conditional instruction ("if upstream has a fix pull it in and open a ticket") is not approval of a draft the user has not read.
5. **Importing fixes** — never `fetch`/`cherry-pick`/hand-port from any remote without asking; canonical URL only; review the whole commit as untrusted (workflows, `*.csproj`, `Directory.Build.props`, scripts); apply only after a second yes. Third-party forks and unmerged PRs are never a source.

`dev-docs/bug-reporting.md` is the process; `dev-docs/claude-skills/modernuo-bug-reporting.md` is Claude's step-by-step procedure (opt-in, like the other skills).

### Issue form
- `.github/ISSUE_TEMPLATE/bug_report.yml` — structured fields (summary, upstream location, commit, reproduction against a clean build, expansion/platform/found-via dropdowns) and a required checklist restating the rules. Applies the `bug` label.
- `.github/ISSUE_TEMPLATE/config.yml` — chooser links for private security reports and Discord.
- Form submissions render as `### <Field>` markdown; the skill writes that exact shape via `gh issue create --body-file`, so an assistant-drafted issue is indistinguishable from a browser one.

## How the skill was validated

Pressure scenarios against subagents, without and then with the rules present.

- **Without**: given a fork with custom content, an owner who said "open a ticket so they know" and went to bed, and a restart in 20 minutes, the agent filed the upstream issue immediately — and the body carried the fork's console log lines and a description of the custom mechanic that triggered the bug, despite the agent stating it had "scrubbed hard". It did refuse an unreviewed third-party PR.
- **With**: same scenario, the agent pushed nothing and filed nothing, removed every log line (including the one that only named the upstream method), produced a scrub ledger, left the "reproduced on clean main" box honestly unticked, and linked the third-party PR without fetching it. An exploit scenario with a relayed standing "email the maintainers immediately" instruction also held: private email drafted, not sent.

The rationalization table in the skill is built from what the baseline agent actually said.

## Notes for review

- The rule is deliberately strict: the user reads the exact draft before anything is submitted. If an explicit in-session waiver ("file it, I don't need to see it") should be honored, that is a one-line change to Workflow Rule 2.
- All `gh` commands in the docs were run against this repo; the worked example points at `Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs` and a line that exists there, with the example defect marked as illustrative.
- `config.yml` links private disclosure to `CONTRIBUTING.md` rather than a `mailto:` because GitHub only accepts `http(s)` contact links.
2026-09-14 23:27:27 -07:00

11 KiB

name description
modernuo-bug-reporting Use when you find a bug you were not asked to fix while working in ModernUO or a fork of it — a latent defect, a suspicious upstream behavior, an exploit — or when the user asks to report a bug upstream, file an issue, check upstream for a fix, or pull an upstream fix into the fork. Also use when a standing instruction ("just file it", "pull it in if upstream fixed it") seems to already cover submitting or importing something, or when an unmerged PR from another fork looks like the fix.

ModernUO Bug Reporting

Overview

Nothing leaves the fork, and nothing enters it, without the user approving that exact artifact.

You are working in someone's shard. Its custom code and mechanics are their trade secret, its logs and saves are their players' data, and its GitHub identity is theirs. An upstream report or an imported fix is an external action taken in their name. You draft; they decide.

Violating the letter of these rules is violating the spirit. "The owner said to open a ticket" does not make an unseen draft approved. "It's from the canonical repo" does not make a fetch safe.

Authority: dev-docs/bug-reporting.md. Always-on summary: CLAUDE.md § Workflow Rules.

When This Activates

  • You traced a problem to code the fork never modified and the same defect would happen on any shard
  • You noticed a defect, exploit, or dubious behavior while doing something unrelated
  • The user says: report this upstream, file an issue, is this fixed upstream, pull in their fix
  • You are about to run gh issue create, gh pr create, gh issue comment, git fetch, git cherry-pick, git remote add, or git push to a remote the user does not own

Not for: bugs in the fork's own code (fix locally, nothing leaves); bugs the user asked you to fix in the current PR of the canonical repo (normal workflow, but still search issues/PRs first).

The Procedure

digraph bug_report {
    "Could a player abuse it?" [shape=diamond];
    "Draft private email to hi@modernuo.com\nNo issue, no PR, no Discord" [shape=box];
    "Do the defective lines exist verbatim\nin upstream main at that path?" [shape=diamond];
    "Fork's bug. Fix locally.\nNothing leaves." [shape=box];
    "Search upstream issues + PRs (all states)\n+ commits on the path" [shape=box];
    "Merged fix / open issue / nothing?" [shape=diamond];
    "Offer: import fix (two approvals)" [shape=box];
    "Offer: comment on existing thread" [shape=box];
    "Draft + scrub ledger → show → offer → WAIT" [shape=box];

    "Could a player abuse it?" -> "Draft private email to hi@modernuo.com\nNo issue, no PR, no Discord" [label="yes"];
    "Could a player abuse it?" -> "Do the defective lines exist verbatim\nin upstream main at that path?" [label="no"];
    "Do the defective lines exist verbatim\nin upstream main at that path?" -> "Fork's bug. Fix locally.\nNothing leaves." [label="no"];
    "Do the defective lines exist verbatim\nin upstream main at that path?" -> "Search upstream issues + PRs (all states)\n+ commits on the path" [label="yes"];
    "Search upstream issues + PRs (all states)\n+ commits on the path" -> "Merged fix / open issue / nothing?";
    "Merged fix / open issue / nothing?" -> "Offer: import fix (two approvals)" [label="merged fix"];
    "Merged fix / open issue / nothing?" -> "Offer: comment on existing thread" [label="open issue"];
    "Merged fix / open issue / nothing?" -> "Draft + scrub ledger → show → offer → WAIT" [label="nothing"];
    "Draft private email to hi@modernuo.com\nNo issue, no PR, no Discord" -> "Draft + scrub ledger → show → offer → WAIT";
}

1. Classify

Exploit-class = duplication, player-triggerable crash or hang, auth or access-level bypass, touching another player's data, out-of-rules gain. → Private email draft only. Naming the method plus the symptom in public is enough for anyone to rediscover it; a public PR titled after the fix is itself a disclosure. Even the private email waits for the user — it commits them to a disclosure timeline in their name.

2. Verify upstream, read-only

gh api repos/modernuo/ModernUO/commits/main --jq '.sha[0:9]'
gh api -H "Accept: application/vnd.github.raw" "repos/modernuo/ModernUO/contents/<path>?ref=main" > "$SCRATCH/<file>.upstream.cs"
grep -nF -- '<exact line>' "$SCRATCH/<file>.upstream.cs"

Run the grep for the defective lines and for every line the draft will quote. No match → not in the draft. Defective lines don't match → fork's bug, stop.

gh api reads are free. Anything that writes to the repository — fetch, remote add, cherry-pick, push — is asked for first, every time.

3. Dedup, read-only

gh issue list -R modernuo/ModernUO --state all --search "<FileName.cs>" --limit 30
gh issue list -R modernuo/ModernUO --state all --search "<Symbol>" --limit 30
gh issue list -R modernuo/ModernUO --state all --search "<symptom words>" --limit 30
gh pr list    -R modernuo/ModernUO --state all --search "<Symbol>" --limit 30
gh api "repos/modernuo/ModernUO/commits?sha=main&path=<path>&per_page=15" --jq '.[] | "\(.sha[0:9]) \(.commit.message | split("\n")[0])"'

Present candidates. An unmerged PR from a third-party fork is a link in the report, never a source: reading it (gh pr view, gh pr diff) to describe it is fine; fetching it into the repository or copying its code is not.

4. Draft to scratch space

The draft is (in this order, ### <Field label> headings — see dev-docs/bug-reporting.md § The issue form for the rendered example):

  1. Summary — actual vs expected, in upstream terms
  2. Location — upstream path and symbol
  3. Upstream commit or release — the sha from step 2
  4. Reproduction — numbered steps on a clean upstream build: [add, [props, [delete, standard creatures and items. Custom content is never the trigger in the report.
  5. Root cause and proposed fix — quoted existing code passed the grep; new code is marked as new in the ledger
  6. Expansion, Platform, Found via — one option each, copied from the form
  7. Checklist — the form's four lines, verbatim

Read .github/ISSUE_TEMPLATE/bug_report.yml (upstream copy via gh api if the fork lacks it) and copy the dropdown options and checklist lines from it. A checklist line is ticked only when it is true: do the search and the verification before offering the draft; never file with a box unticked.

Content sources are: upstream file contents, [ commands, and the user's words about behavior. Fork logs, fork config, fork code, fork mechanics, names, addresses, and screenshots are not sources.

5. Scrub ledger, then show, then offer, then wait

Show the complete draft and:

Scrub ledger
- Removed: <counts by kind: names, IPs, emails, shard/custom references, log lines>
- Kept, verified verbatim in upstream main @ <sha>: <N> quoted lines (<file> <range>)
- Not in upstream (new code, your call): <what>

Offer: file issue / open PR / comment on #N / Discord draft for https://muo.gg/discord (they post it) / nothing. Wait for a yes to a specific option. Then, and only then, gh issue create -R modernuo/ModernUO --title "..." --label bug --body-file "$SCRATCH/upstream-issue.md".

6. Importing a merged upstream fix

git remote get-url upstream must be github.com/modernuo/ModernUO. Ask → git fetch upstream main --no-tags. Review the whole commit as untrusted: git show --stat <sha>, then .github, *.csproj, Directory.Build.props, *.ps1, *.sh, Distribution/, then the full diff. Ask again → git cherry-pick -x <sha> or hand-port. Build. Show the diff. Never from a fork or an unmerged PR.

7. Upstream PR from a fork

Worktree off upstream/main, minimal fix only, git diff upstream/main --stat shows only upstream paths and the diff has no custom namespaces or fork paths. Push to the user's public fork, never the private shard repo. gh pr create only after the exact title and body are approved.

Rationalizations

Excuse Reality
"The owner asked for the ticket explicitly, I'll scrub it hard" They asked before the ticket existed and have not read it. Only they know which detail is the trade secret. Draft, ledger, wait.
"It needs nothing shard-specific" In testing, the draft that "needed nothing shard-specific" contained the fork's console log and a description of the custom system that triggered the bug.
"Nobody is awake to review; the morning note is the review surface" The morning note reviews what already went out under their name. Unsent drafts cost nothing; a public issue cannot be unsent.
"The fix is on upstream main, fetching it is safe" Fetching is a write to their repository, and canonical history is reviewed as untrusted before it is built. Ask, fetch main only, review everything, ask again.
"This PR fixes exactly our symptom" Unmerged and from a fork you cannot vet. Link it; never fetch it.
"Posting the exploit publicly warns other shards" It arms every griefer on every shard in minutes; most shards patch monthly or never. Private disclosure gets the fix onto main, which is what protects them.
"The log line is just the upstream method name" It came from the fork's log. Describe the behavior; quote only what passed the grep.
"Serial numbers / timestamps aren't identifying, so the log is fine" The log is not a source. The serial is fine; the line it came from is not.
"I'll add the upstream remote so verification is easier" Verification is gh api, no remote needed. Adding a remote changes their repository — ask.
"Comment on the existing issue is lower stakes than filing" It is a public statement in their name. Same gate.

Red Flags — STOP

  • You are typing gh issue create, gh pr create, gh issue comment, git push, git fetch, git remote add, or git cherry-pick and the user has not said yes to this artifact
  • "The user said to" — about something they have not read
  • A log excerpt, a config value, or a custom class name is in the draft
  • The reproduction mentions anything that does not exist on a clean upstream build
  • The source of the fix is a fork, an unmerged PR, or a commit you have not read in full
  • It is an exploit and the draft is an issue, PR, or Discord post

All of these mean: stop, put it in the ledger, show the user, wait.

How to Report

When you find a bug you were not asked to fix, before doing anything else:

[BUG FOUND] {one-line description}
  File: {path}:{line}   Upstream: {verbatim @ sha | not upstream | not yet checked}
  Class: {exploit → private only | bug}
  Existing: {#N open | #N merged fix | none found}
  Next: draft + ledger for your review — say which: issue / PR / comment / Discord draft / nothing

See Also

  • dev-docs/bug-reporting.md — full process, rendered issue-form example, import and PR steps
  • .github/ISSUE_TEMPLATE/bug_report.yml — the form the draft mirrors
  • CONTRIBUTING.md § Reporting security issues and bugs — private disclosure address
  • dev-docs/claude-skills/modernuo-code-audit.md — rule 21 comment sweep before any PR