## 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.
252 lines
12 KiB
Markdown
252 lines
12 KiB
Markdown
# Reporting Bugs Upstream From a Fork
|
||
|
||
How a fork or custom project built on ModernUO reports a bug it found in upstream code — and how it
|
||
imports an upstream fix — without leaking anything about the fork and without letting an AI
|
||
assistant act on the fork's behalf unsupervised.
|
||
|
||
This document is the authority; the `modernuo-bug-reporting` skill (`dev-docs/claude-skills/`) is
|
||
the step-by-step procedure Claude follows. The rules in `CLAUDE.md` § Workflow Rules are the
|
||
always-on summary.
|
||
|
||
## Vocabulary
|
||
|
||
| Term | Meaning |
|
||
|---|---|
|
||
| **Upstream** | `https://github.com/modernuo/ModernUO`, branch `main`. The only canonical source. |
|
||
| **Fork** | Any repository whose history includes upstream: a private shard repo, a public fork, a custom project that vendored ModernUO. |
|
||
| **Latent bug** | A defect found while doing something else — you were not asked to fix it. |
|
||
| **Upstream bug** | The defective lines exist verbatim in upstream `main` at the same path. Anything else is the fork's bug, whatever file it lives in. |
|
||
| **Exploit-class** | Anything a player could abuse: item or gold duplication, a crash or hang a player can trigger, authentication or access-level bypass, reading or writing another player's data, skill/stat gain outside the rules. |
|
||
|
||
## The principle
|
||
|
||
**Nothing leaves the fork, and nothing enters it, without the user approving that exact artifact.**
|
||
|
||
Not a summary of it, not a standing instruction that covers it, not "the owner said to open a
|
||
ticket" — the user reads the draft that will be submitted, or the diff that will be applied, and
|
||
says yes to that. An assistant that scrubs its own draft and submits it has removed the only
|
||
reviewer who knows what the fork considers secret.
|
||
|
||
## The process
|
||
|
||
### 1. Classify
|
||
|
||
Exploit-class → **private disclosure only**. Draft an email to `hi@modernuo.com` (see
|
||
[CONTRIBUTING.md](../CONTRIBUTING.md#reporting-security-issues-and-bugs)). No public issue, no
|
||
PR, no Discord post — a public PR titled "fix stack dupe" is itself a disclosure, and naming the
|
||
method plus the symptom is enough for anyone to rediscover it. Skip to step 5 with the email as
|
||
the draft.
|
||
|
||
Everything else continues.
|
||
|
||
### 2. Locate upstream
|
||
|
||
```sh
|
||
git remote -v
|
||
```
|
||
|
||
The remote whose URL is `github.com/modernuo/ModernUO` is upstream. In the canonical repository
|
||
that is `origin`; the process still applies (see [Working in the canonical repo](#working-in-the-canonical-repo)).
|
||
No such remote → the fork may have been vendored or re-rooted. Do not add one silently; verification
|
||
below works without it, and adding a remote is a change to the user's repository that they approve.
|
||
|
||
### 3. Verify it is upstream's bug
|
||
|
||
The defective lines must exist **verbatim** in upstream `main` at the same path. This is a
|
||
read-only lookup — nothing enters the repository:
|
||
|
||
```sh
|
||
# upstream head, for the report
|
||
gh api repos/modernuo/ModernUO/commits/main --jq '.sha[0:9]'
|
||
|
||
# the upstream file, into scratch space — never into the working tree
|
||
gh api -H "Accept: application/vnd.github.raw" \
|
||
"repos/modernuo/ModernUO/contents/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs?ref=main" \
|
||
> "$SCRATCH/PetOrders.upstream.cs"
|
||
|
||
grep -nF -- 'if (Mobile.ControlTarget?.Deleted == false && Mobile.ControlTarget != Mobile)' "$SCRATCH/PetOrders.upstream.cs"
|
||
```
|
||
|
||
Every line the report will quote goes through that `grep`. No match → the line is the fork's, and
|
||
it does not appear in the report. If the defective lines themselves do not match, the bug is the
|
||
fork's: fix it locally, report nothing upstream.
|
||
|
||
### 4. Search for an existing fix or duplicate
|
||
|
||
All read-only. Search by file name, by symbol, and by symptom — a duplicate rarely uses your words.
|
||
|
||
```sh
|
||
gh issue list -R modernuo/ModernUO --state all --search "PetOrders.cs" --limit 30
|
||
gh issue list -R modernuo/ModernUO --state all --search "DoOrderFollow" --limit 30
|
||
gh issue list -R modernuo/ModernUO --state all --search "pet stuck follow" --limit 30
|
||
gh pr list -R modernuo/ModernUO --state all --search "DoOrderFollow" --limit 30
|
||
|
||
# recent upstream commits on the path
|
||
gh api "repos/modernuo/ModernUO/commits?sha=main&path=Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs&per_page=15" \
|
||
--jq '.[] | "\(.sha[0:9]) \(.commit.message | split("\n")[0])"'
|
||
```
|
||
|
||
Present the candidates. A merged fix → offer to import it ([Importing an upstream fix](#importing-an-upstream-fix)).
|
||
An open issue → offer to comment there instead of filing a new one. An **unmerged PR from a
|
||
third-party fork** is information, not a source: link it in the report if relevant, never fetch it.
|
||
|
||
### 5. Draft, show, ask
|
||
|
||
Write the draft to scratch space. Show the user the **complete text** and a **scrub ledger**:
|
||
|
||
```
|
||
Scrub ledger
|
||
- Removed: 2 account names, 2 IPs, 1 email, the shard name, one custom class reference,
|
||
3 console log lines (emitted by the fork, not upstream).
|
||
- Kept, verified verbatim in upstream main @ 459674ce3: 6 quoted lines (PetOrders.cs 630–648).
|
||
- Not in upstream (new code, your call): the 4-line proposed fix.
|
||
```
|
||
|
||
Then offer the choice — file an issue, open a PR, comment on the existing thread, draft a Discord
|
||
post for https://muo.gg/discord (the user posts it; there is no automation into Discord), or
|
||
nothing. Wait for an explicit yes to a specific option. Only then:
|
||
|
||
```sh
|
||
gh issue create -R modernuo/ModernUO --title "<title>" --label bug --body-file "$SCRATCH/upstream-issue.md"
|
||
gh issue comment <N> -R modernuo/ModernUO --body-file "$SCRATCH/upstream-comment.md"
|
||
```
|
||
|
||
## What never leaves the fork
|
||
|
||
| Never include | Why |
|
||
|---|---|
|
||
| Credentials, tokens, connection strings, anything under `Distribution/Configuration/` or in `.env` files | Obvious, and forks keep these next to the code |
|
||
| IP addresses, hostnames, ports, internal URLs | Identify the shard and its infrastructure |
|
||
| Account names, character names, emails, player IPs, anything from `Saves/` | Player data |
|
||
| Log lines from the fork | Logs carry timestamps, names, serials, and custom logging — and the fork's log format is not upstream's |
|
||
| The shard's name, its custom features' names, any description of custom mechanics — even vaguely ("our arena deletes the decoy") | Trade secret, and it points a reader at the fork |
|
||
| Code that is not verbatim in upstream `main` | The fork's code is the fork's |
|
||
| Screenshots showing names, gump text, or custom UI | Same as above |
|
||
|
||
**Reproduction is written against a clean upstream build**: `[add`, `[props`, `[delete`, standard
|
||
items and creatures. If the bug only manifests through custom content, find the upstream-only
|
||
trigger first; if there is none, it may not be upstream's bug.
|
||
|
||
Timestamps, item serials, and the fact that the reporter runs a fork are fine.
|
||
|
||
## The issue form
|
||
|
||
`.github/ISSUE_TEMPLATE/bug_report.yml` is a GitHub **issue form**: structured fields, the `bug`
|
||
label applied on submit, and a required checklist that restates the rules above. Humans fill it in
|
||
the browser. `.github/ISSUE_TEMPLATE/config.yml` adds the private-disclosure and Discord links to
|
||
the "New issue" chooser.
|
||
|
||
A submitted form renders as Markdown with one `### <Field label>` heading per field, in form order.
|
||
An assistant submitting through `gh issue create --body-file` writes exactly that shape, so the
|
||
result is indistinguishable from a browser submission. (The defect below is illustrative — it does
|
||
not exist upstream.)
|
||
|
||
```markdown
|
||
### Summary
|
||
|
||
Actual: a pet ordered to follow a mobile that is then deleted stops responding to all orders until restart.
|
||
Expected: the pet drops the follow order and returns to its master's side.
|
||
|
||
### Location
|
||
|
||
Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs — BaseAI.DoOrderFollow()
|
||
|
||
### Upstream commit or release
|
||
|
||
459674ce3
|
||
|
||
### Reproduction
|
||
|
||
1. Tame a horse and `[add Rat`.
|
||
2. Order the horse to follow the rat.
|
||
3. `[delete` the rat.
|
||
4. Order the horse to come — it does not move.
|
||
|
||
### Root cause and proposed fix
|
||
|
||
`DoOrderFollow()` takes the `Mobile.ControlTarget?.Deleted == false && Mobile.ControlTarget != Mobile`
|
||
branch on every AI tick without clearing `ControlTarget` or `ControlOrder` once the target is gone.
|
||
|
||
### Expansion
|
||
|
||
Any / not expansion-specific
|
||
|
||
### Platform
|
||
|
||
Any / not platform-specific
|
||
|
||
### Found via
|
||
|
||
AI-assisted code review
|
||
|
||
### Checklist
|
||
|
||
- [x] I searched existing issues and pull requests (open and closed) for this file, class, or symptom.
|
||
- [x] I verified the bug against upstream `main`, not only a fork.
|
||
- [x] This report contains no credentials, IP addresses, player or account information, save data, logs, custom code, or shard-specific details.
|
||
- [x] This is not an exploit or security bug (those are reported privately, per CONTRIBUTING.md).
|
||
```
|
||
|
||
Optional fields the reporter has nothing for render as `_No response_`.
|
||
|
||
## Importing an upstream fix
|
||
|
||
Upstream has merged a fix. Importing it is a change to the fork; the user approves it twice — once
|
||
to fetch, once to apply.
|
||
|
||
1. **Verify the remote** before any network operation that writes to the repository:
|
||
```sh
|
||
git remote get-url upstream
|
||
# must be exactly https://github.com/modernuo/ModernUO.git (or the .git-less / ssh form of the same repo)
|
||
```
|
||
2. **Ask, then fetch** only `main` from that remote: `git fetch upstream main --no-tags`.
|
||
3. **Review the whole commit as untrusted input**, not just the lines that fix the bug:
|
||
```sh
|
||
git show --stat <sha>
|
||
git show <sha> -- .github Directory.Build.props '*.csproj' '*.props' '*.targets' '*.ps1' '*.sh' Distribution/
|
||
git show <sha>
|
||
```
|
||
Workflow, project-file, package-reference, and script changes are supply-chain vectors even from
|
||
the canonical repo — a compromised maintainer account looks exactly like a maintainer.
|
||
4. **Ask again, then apply**: `git cherry-pick -x <sha>`, or hand-port when the fork's copy has
|
||
diverged. Build. Show the resulting diff.
|
||
|
||
Never import from a third-party fork or an unmerged PR. If the user wants that code, they review it
|
||
on GitHub and paste what they want; the assistant does not fetch it.
|
||
|
||
## Opening an upstream PR from a fork
|
||
|
||
The PR must contain only the fix, on upstream history, from a **public** fork under the user's
|
||
GitHub account — never from the private shard repository.
|
||
|
||
1. After the fetch above: `git worktree add ../upstream-fix -b fix/<slug> upstream/main`
|
||
2. Apply the minimal fix there. No custom code, no fork-only files.
|
||
3. Prove it: `git diff upstream/main --stat` lists only upstream paths; `git diff upstream/main`
|
||
contains no custom namespaces, shard names, or fork paths.
|
||
4. Follow `CLAUDE.md` — the audit rules, rule 21's comment sweep, and `dev-docs/code-standards.md`.
|
||
5. With approval of the exact title and body:
|
||
```sh
|
||
git push <public-fork-remote> fix/<slug>
|
||
gh pr create -R modernuo/ModernUO --base main --head <github-user>:fix/<slug> --title "<title>" --body-file "$SCRATCH/upstream-pr.md"
|
||
```
|
||
|
||
## Working in the canonical repo
|
||
|
||
Upstream is `origin`. Steps 3–4 still apply: check existing issues, PRs, and recent commits before
|
||
filing or fixing. The choice offered becomes: file an issue, fix it in the current PR (if in scope),
|
||
fix it in a separate PR, or nothing. The never-include list still applies — a local test shard's
|
||
saves and logs are still not report material.
|
||
|
||
## Why the rules are shaped this way
|
||
|
||
- **A standing instruction is not approval of a draft.** "If upstream has a fix, pull it in and
|
||
open a ticket" was said before the ticket existed. The person who said it has not seen what it
|
||
contains, and is the only one who knows which detail is the trade secret.
|
||
- **The reporter's own scrub is not a gate.** In testing, an assistant that "scrubbed hard" still
|
||
pasted the fork's console log and described the custom system that triggered the bug. Verbatim
|
||
upstream verification plus a ledger the user reads catches both.
|
||
- **Public disclosure of an exploit protects nobody.** Griefers read GitHub and Discord in minutes;
|
||
most shards pull upstream monthly or never. A fix on `main` plus a "please update" advisory is
|
||
what protects shards, and private disclosure is how that happens.
|
||
- **The canonical URL is the only trust anchor.** Forks are anyone's; unmerged PRs are anyone's.
|
||
Even the canonical repo's history is reviewed as untrusted before it is built.
|