references/durable-context.md
# Durable Context Preflight
Shared preamble for every skill that reads optional memory or prior-decision context. Each `SKILL.md` links to this file and then adds skill-specific guidance.
## Scope
Read durable context when the user names memory, a prior decision, or a memory path, or when the project exposes an obvious local memory summary (a `MEMORY.md` or a documented memory directory). List titles first and open at most one or two summaries; do not hard-code machine-specific memory roots, and do not read raw transcripts. Treat cross-project entries as transferable patterns, not as facts about this project.
## Current state wins
Current code, diff, screenshots, logs, tests, docs, CI, remote state, and live probes always override memory, including memory the runtime injects on its own. A remembered fact is a lead to re-verify, never evidence. When current state conflicts with a remembered claim, name the conflict and follow current state.
## Memory is not authorization
Memory may explain preferences, but it must never grant or broaden authorization for writes, commits, pushes, publishing, public replies, deletion, or other state changes. Current-turn instructions and current project rules decide authorization. Historical phrases such as `push` or `check` are context to re-evaluate, not reusable action tokens.
## Redaction gate
When turning prior chats, durable memory, or cross-project notes into reusable Waza guidance, promote only workflow rules. Strip raw transcript text, screenshots, local paths, project-specific commands, issue or PR numbers, release tags, commit hashes, private product boundaries, paid or license details, support routing, user names, and one-machine state.
If an example is necessary, use neutral placeholders such as `ExampleCLI`, `ExampleApp`, `<issue>`, `<release>`, or `<command>`. Do not copy a private answer, maintainer reply, screenshot observation, or project-specific incident as a durable rule.
The skill-specific overrides and constraints live in each `SKILL.md`, in the paragraph that follows its link to this file.
references/failure-patterns.md
# Failure Pattern Reference
Use this when a bug has repeated, a first fix did not hold, or the symptom smells like runtime state rather than local code syntax.
## Stale Verifier Or Tool Cache
Signals: verifier output points at deleted temp worktrees, old generated files, or paths outside the current repo; rerunning after a clean checkout changes the file path but not the current code.
Checks:
- Confirm the reported path exists.
- Clear the tool cache only after proving the path is stale.
- Re-run the same verifier from the current repo root.
## Worker Queue Or DB Boundary
Signals: UI says work is running but no worker processes it; logs show scheduler activity but no queued row; retry fixes one item but not the pipeline.
Checks:
- Trace request -> enqueue -> worker pickup -> persistence -> UI refresh.
- Inspect queue rows or job state directly.
- Add a regression test around the enqueue boundary, not only the worker body.
## Generated Rebuild Boundary
Signals: source changed but generated output, app bundle, CLI artifact, archive, checksum, or release package still contains old behavior.
Checks:
- Identify the source-to-artifact rule.
- Verify the build system watches the source path.
- Inspect the generated artifact contents, not just the source diff.
## Guard Lifetime Race
Signals: permission, auth, or state guard looks correct locally but a delayed callback, app relaunch, or alternate entry point bypasses it.
Checks:
- Trace guard creation, retention, invalidation, and every alternate entry point.
- Verify cold launch, warm launch, deep link/file open, and retry paths when applicable.
- Prefer explicit durable state over transient flags when the guard must survive relaunch.
## Atomic Temp Filename
Signals: concurrent runs collide, cleanup removes the wrong file, or a partially written output is observed.
Checks:
- Use unique temp directories or atomic rename.
- Keep cleanup scoped to files created by the current run.
- Test two concurrent or back-to-back runs when the tool supports it.
## Path, Cwd, Or Symlink Escape
Signals: an operation intended for one root touches a sibling directory, follows a symlink unexpectedly, or behaves differently from another working directory.
Checks:
- Resolve and compare canonical roots before writing or deleting.
- Reject paths outside the allowed root after symlink resolution.
- Reproduce from a non-default cwd and through any UI entry point that supplies paths.
## CLI Effect Scope Drift
Signals: preview, dry-run, size, count, or report output is computed from one predicate, but execution mutates a broader or different set.
Checks:
- Trace display, dry-run, and mutation predicates to the same source of truth.
- Compare planned paths or records with executor input in a regression test.
- Assert partial failures report the exact skipped and completed items.
## CLI Wrapper Or PATH Drift
Signals: source-tree invocation works, but the installed command, package wrapper, PATH shim, completion, or package-manager install path runs old code or a different binary.
Checks:
- Inspect built package contents, shebang, executable bit, and wrapper target.
- Reproduce through a temp prefix or package-manager install path, not only from source.
- Check PATH order and use absolute system-tool paths where wrappers should not intercept.
## Interactive Stdin Or TTY Hang
Signals: CI stalls, spinner never finishes, a subprocess reads from the script body, or an auth prompt appears in non-interactive mode.
Checks:
- Reproduce with stdin redirected and with TTY/non-TTY paths separated.
- Add test-mode or no-auth guards around real prompts and system changes.
- Stub external prompt tools through PATH when timeout wrappers exec real binaries.
## Subprocess Pipe Backpressure
Signals: a long-running child process hangs only on large output, small fixtures pass, or the parent waits for exit before reading stdout/stderr. The child may be blocked on a full pipe buffer while the parent is blocked on `wait`.
Checks:
- Drain stdout and stderr while the process runs, or explicitly inherit/redirect streams when output is not needed.
- Test with output larger than a typical pipe buffer, not only tiny fixtures.
- Preserve stderr tails or structured error output for diagnostics without holding the whole stream in memory.
## Signal Or Partial-Failure Mapping
Signals: cancel, timeout, SIGINT, or SIGTERM is reported as success or as a normal business failure; temp files, locks, or operation logs make retries look complete.
Checks:
- Classify interrupted execution separately from success and expected validation failures.
- Assert temp cleanup, lock release, and operation-log state after interruption.
- Test retry and idempotency after a partial write.
## CLI Stream Contract Regression
Signals: automation breaks after human logs, progress output, JSON shape, stdout/stderr routing, or exit-code behavior changes.
Checks:
- Assert exit code, stdout, and stderr separately in CLI tests.
- Keep human diagnostics off stdout for machine-readable modes.
- Snapshot or parse JSON/schema output and include non-interactive coverage.
## Snapshot Rebuild Drops Carried Field
Signals: live data shows up at the data source and on the wire but a downstream view sees it empty; the field has a default value (`var x: [T] = []`, `var y: Int? = nil`) that lets memberwise init compile without it; the symptom appears only on the path where the snapshot is rebuilt (icon resolution, decoration, redaction), not on a fresh fetch.
Checks:
- Trace whether every code path that constructs the snapshot type passes the field. The Swift compiler does not warn on default-value omission in memberwise init.
- Add a unit test that fetches the snapshot, runs the rebuild path, and asserts the carried field equals the input.
- Prefer `with(...)` mutating helpers or `inout` mutation over fresh memberwise init when only one field is changing.
## Multi-Sample Command Cold Start
Signals: a CLI tool that takes `-l N` / `--samples N` / `--repeat N` returns one block of zeros and one block of real data; aggregating all blocks yields zeros; only the second sample carries real measurements.
Checks:
- Read the tool's man page for cold-start semantics. `top -l 2`, `iostat -d 2`, `vm_stat 1 2`, etc. all share this shape.
- Slice the output to the latest sample (`.suffix(perSampleSize)` on parsed lines, or look for the second instance of the header row).
- When in doubt, raise `-l` to 3 and confirm sample 2 and 3 agree; sample 1 stays zero.
## Locale-Dependent Subprocess Output
Signals: numbers parse correctly for the author and come back zero, truncated, or wildly wrong for some users; a percentage, size, or duration is right in one region and broken in another; the same parser was already patched once for a different field.
Checks:
- Force a fixed locale on every subprocess whose output is parsed (`LC_ALL=C` or the platform equivalent) rather than repairing each parser for comma decimal separators, digit grouping, or translated field labels.
- Fix this at the spawn boundary, not per call site. This shape arrives as three or four separate reports (one metric, then another, then a rendered summary) and each pointwise patch hides how many parsers are still exposed.
- Treat translated output as a format change, not a string change: field order, units, and label names can all move.
## Single-Probe Existence Check
Signals: a "is it installed / running / registered / active" verdict is wrong for a subset of users, and the wrong verdict then drives a destructive or user-visible action (flagged as orphaned, offered for deletion, a feature silently disabled). The subset shares an install method, a packaging convention, or an OS feature the probe does not know about.
Checks:
- List every legitimate way the subject can exist, then confirm the probe sees all of them. One index query, one PATH lookup, one process name, or one interface-name prefix is a partial view: system indexes can be disabled or skip a packaging convention, nested or embedded components are not registered where top-level ones are, and OS-owned interfaces borrow the naming a third-party feature also uses.
- Distinguish "probe timed out" from "subject absent". A slow index is unknown, not proof of absence; a timed-out fast path must fall through to a direct check, never to a negative verdict.
- Weight the failure asymmetrically: when the verdict authorizes removal, a false "absent" destroys data while a false "present" only leaves something behind. Require corroboration from a second source before the destructive branch.
## Aggregation Key Variant
Signals: a count, log roll-up, event tally, or per-category breakdown is short by some entries; the missing items share a trait (a system-derived path, a localized string, a prefixed command name); the base-form key matches but a derived variant (`<base>-system`, a suffix, a prefix) is silently dropped.
Checks:
- Before adding a category, grep every write site that produces this class of key and enumerate the real variants, not just the base form.
- Match with `hasPrefix` / a regex / an explicit variant list rather than exact equality on the base key.
- Add a fixture row for each known variant so a future key shape that escapes the matcher fails the test instead of the aggregate.
## Whole-Buffer Decode Collapse
Signals: a parser that works on your machine returns nothing on someone else's; the affected user has an accented device name, a non-ASCII filename, or an unusual process argument; the failure is total (every row gone) rather than partial (one row garbled). Distinct from pipe backpressure: the bytes arrived, the decode threw them away.
Checks:
- Find every strict decode of bytes produced by a child process, a device, or the filesystem (`String(data:encoding:)`, `from_utf8`, `decode('utf-8')` with no `errors=`). One invalid byte nils the entire buffer, and callers that coalesce nil to empty turn it into "the command produced nothing".
- Decode leniently wherever the bytes are a report to parse; keep the strict decode only where the bytes are a signature or checksum being verified, and fail closed there. Both stances belong in one codebase for different call sites.
- Check what the empty result means downstream. A safety guard that reads an empty process list as "nothing is running" fails open, which is the dangerous direction on a destructive path.
- Real failure is reported by exit status and timeout, not by decode success. Point callers at those instead.
## Denied Read Returns A Plausible Value
Signals: a metric is right for some subjects and wrong for others, and the split follows ownership: your own processes/files are correct, root-owned or other-user ones read zero, stale, or absent. No error is logged because the API answered.
Checks:
- Measure the boundary instead of reading the docs: run the call across owned and non-owned subjects and count how many succeed in each group. Those two counts are the evidence; a guess is not.
- Check whether the fallback for a denied read carries the same meaning as the primary source. Two different meanings in one column is the bug, even when each value is individually defensible.
- Prefer a source that answers uniformly for every subject (a tool that reports all processes regardless of owner) over a precise one that silently degrades for some.
## Recovery Gated On The Artifact It Restores
Signals: a repair, reinstall, or self-heal path reports the same dead end no matter how many times it runs; the broken state persists across reinstalls; the repair "has always been there" but no evidence exists that it ever succeeded.
Checks:
- Read the repair path's own precondition and ask whether it is true in the broken state it exists to fix. A recovery gated on the file that is missing can never fire.
- Verify every absolute tool path in a repair command actually exists on the platform. A wrong path exits non-zero, an `&&` chain silently stops, and the repair no-ops on every machine forever. Add a test that walks each path in the command and asserts it is executable.
- Do not let a test assert the source shape of the repair command; that pins the broken form as correct. Assert the observable end state instead (the service is registered, the file exists, the probe answers).
- Make the repair own the outcome rather than assume it: write the artifact, then prove it by querying the system, and log the query's raw output instead of discarding it.
## Watchdog Tuned To The Fast Path
Signals: an operation is reported as failed, stalled, or "no progress" while it was actually healthy; the report comes from users on slow links, cold caches, network volumes, or large payloads; retrying makes it fail at the same elapsed time every run.
Checks:
- For each timeout constant, name the slowest *healthy* case (a several-hundred-MB download on a slow link, a first-of-day index rebuild, a tool rebuilding its cache after a cleanup) and confirm the constant clears it with margin. This is the inverse of magic-wait coupling: there the timer is too loose to be a real signal, here it is too tight to allow a healthy slow case.
- Replace "no output for N seconds" with a real liveness probe (a growing temp file, a byte counter, a heartbeat) and keep the timeout as the genuine stall guard.
- For each watchdog, enumerate every exit from the region it guards, including thrown errors and forks into an alternate path. A watchdog that survives a fork fires in the middle of the path that replaced it.
- Check whether a second bound already covers a genuinely hung run. If so, the extra timer can only ever fire early.
## Display-String Comparison
Signals: a comparison based on user-facing text produces a verdict that never resolves: a perpetual "update available" that installs nothing, a diff that always reports changed, a match that never fires. The two sides format the same underlying value differently.
Checks:
- Ask whether the compared value's *format* is part of a contract or something the producer restyles at will. Version display strings, filenames derived from a URL tail, and localized labels are all free-form.
- Find the machine-facing identity the platform intends for ordering or equality (a build number, a content hash, an id) and compare that, falling back to the display form only when the identity is absent on either side.
- When the fallback must stay, suppress the verdict where both strings carry the identical token sequence in a different arrangement; no genuinely newer or changed value can satisfy that.
- Fix every channel that repeats the comparison, not the one that produced the report. This shape is almost always duplicated.
references/ime-unicode.md
# IME / Unicode Debugging Reference
Recurring patterns in webview-hosted and native macOS apps. Check these before forming a hypothesis.
## IME State Desync
**Symptom**: Latin characters appear correctly but CJK input is dropped, doubled, or committed at the wrong time.
**Cause candidates**:
- Input method switch mid-composition: the IME commits the preedit with a stale target, then the new mode processes the same keystrokes again.
- `keydown` handler consuming events during active composition: suppress the confirmation event's bound submit/navigation action while preserving normal IME text commitment. Do not queue that action for `compositionend`; check the event-ordering section for cases where `isComposing` is already false.
- Webview + native frame split focus: in Tauri, the webview and the native window title bar can hold focus simultaneously. A click on a native control during IME composition triggers a focus-out, committing incomplete preedit text.
**Instruments**:
- Log `compositionstart`, `compositionupdate`, `compositionend` sequence; confirm they fire in order without gaps.
- Log the `data` field of each `compositionupdate`; a sudden empty string signals a forced commit.
## Cursor Position Drift After IME Commit
**Symptom**: After confirming a CJK word, the cursor jumps to the wrong position or the selection collapses.
**Cause candidates**:
- DOM mutation during composition: React/Svelte/Vue re-rendering while `isComposing` is true will reset the selection. Batch state updates and flush only on `compositionend`.
- Mixing offset units: JavaScript string lengths and text-node DOM offsets use UTF-16 code units; string iteration counts code points, and visible-character operations may need grapheme clusters. Identify the receiving API's unit before converting positions; replacing `str.length` with `[...str].length` can itself cause drift.
## Emoji ZWJ Sequence Splitting
**Symptom**: Multi-person or profession emoji (e.g. `👩🚒`) renders as two or three separate emoji, or the ZWJ (`U+200D`) appears as a visible character.
**Cause candidates**:
- String sliced at a UTF-16 code-unit offset: `str.slice(0, n)` splits a ZWJ sequence if `n` falls inside the sequence. Use `Intl.Segmenter` with `granularity: 'grapheme'` for visible-character truncation.
- Font does not support the sequence: the font renders each code point individually. Verify with `canvas.measureText` or by checking which font is actually used via `document.fonts`.
- Serialization strips ZWJ: some JSON encoders normalize or escape `U+200D`. Verify the raw bytes of the stored string.
**Test**: `[...'👩🚒'].length` is 3 code points; `[...new Intl.Segmenter(undefined, {granularity: 'grapheme'}).segment('👩🚒')].length` is 1 grapheme cluster. Test offsets separately against the consuming API.
## `compositionend` / `keydown` Event Ordering
**Symptom**: The action bound to Enter or Tab fires during IME confirmation, submitting incomplete input.
**Cause candidate**: `compositionend` can precede the confirmation `keydown`, leaving `isComposing` false, or follow it. Capture the actual order for the affected IME and host rather than inferring it from the OS.
**Verification target**: IME confirmation commits text without submitting; a subsequent deliberate Enter submits once. A flag cleared on `compositionend` has the same ordering gap as `isComposing`. Log the key events and composition boundaries, derive the confirmation-key guard from the observed host behavior, and replay both orderings plus a normal Enter in regression tests. Do not substitute an arbitrary timeout for that evidence.
## macOS Text System vs Webview Conflict
**Symptom**: Undo (`Cmd+Z`) reverts individual IME preedit characters instead of committed words, or system text shortcuts (Cmd+Shift+Left for word selection) behave differently inside vs outside the webview.
**Cause**: WKWebView has its own text system that partially overlaps with NSTextView conventions. The webview host's key-handling config can suppress system shortcuts (Tauri's `preventDefaultFor` in `tauri.conf.json` or `app.json`, or the equivalent in other hosts); check it for `preventDefault` rules that are too broad.
## Quick Checklist
- [ ] `isComposing` checked before acting on keyboard events?
- [ ] No DOM mutation while `isComposing` is true?
- [ ] Offset units match the receiving API, with grapheme boundaries for visible-character operations?
- [ ] ZWJ sequences verified with `Intl.Segmenter`?
- [ ] Confirmation-key guard tested against both event orderings and a subsequent deliberate action?
- [ ] `tauri.conf.json` `preventDefaultFor` not too broad?
references/logging-techniques.md
# Logging Techniques for Debugging
Every log answers a yes/no question about a hypothesis: "if this prints X before Y, hypothesis A holds; otherwise A is dead." A log that cannot rule a hypothesis in or out is noise.
## Discriminating Content
Log what discriminates between hypotheses: ordering (sequence number or timestamp), input identity key, branch taken, old-vs-new state transition, and error code plus context. Place logs at boundaries where behavior should be predictable (handler entry/exit, cache hit or miss with key, state setter with old value and caller, async callback entry, external API result) rather than in tight-loop interiors. Never log credentials, PII, or full request/response bodies.
For race conditions, flicker, or intermittent failures, also capture event identity, monotonic ordering, start and end (not just "it ran"), and thread/task/queue identity. If adding a log changes the behavior, that is evidence of a timing, lifecycle, or concurrency problem, not "logging side effects" to dismiss.
## Runner-Only Failures
When a script fails only under a specific runner (make target, CI job, test harness, cron) but passes standalone, do not edit the script with debug hacks you might forget to remove. Inject tracing from the outside via the environment the runner already passes through:
```bash
# xtrace-env.sh: sourced by every non-interactive bash via BASH_ENV
exec 19>>/path/to/persistent/xtrace.log
export BASH_XTRACEFD=19
export PS4='+ [$0:$LINENO] '
set -x
```
Run the failing pipeline as `BASH_ENV=/path/to/xtrace-env.sh make test` (or the runner's equivalent). Every bash the runner spawns appends `file:line`-stamped traces to one persistent file, surviving the runner's temp-dir cleanup, so the exact dying line is on record even when the failure needs the full pipeline to reproduce. Guard the injection with a sentinel variable if nested shells would re-source it, and delete the env file when done.
## Native App Freeze Mode
Activate when a desktop or mobile native app reports beachball, not responding, tab-switch freeze, first-open lag, idle wake stall, overlay lockup, or a screenshot shows a frozen app.
Evidence to collect before changing code:
1. Exact user path and version: first launch versus warm launch, the tab or window transition, idle duration, permissions, display count, and any setting that makes the freeze disappear.
2. Runtime capture while frozen: `sample <process>`, recent app logs, CPU and memory footprint, thread count, and whether the main thread is blocked, spinning, or allocating.
3. First-frame surface: view body work, first `.task`, synchronous icon or metadata lookup, filesystem scans, URL parent walks, notification callbacks, and app/window wake handlers.
4. Blast search after the fix: grep the same API shape across the repo, especially path parent walks, synchronous icon loading, metadata reads in render paths, and callbacks that run on the main thread.
Common native freeze traps:
- Launch, terminate, permission, audio, display, or workspace notifications doing path walks, icon lookup, filesystem scans, or process enumeration on the main thread.
- First paint hydrating a full app list, directory tree, media thumbnail set, or system status table before showing an interactive shell.
- An input-lock or full-screen overlay without a guaranteed teardown path for Escape, app deactivation, permission denial, process termination, and window close.
- Timer or sampler work that survives hidden windows, long idle periods, sleep/wake, or app reactivation.
Compile-only and source-only checks are insufficient for this mode. The outcome must include the runtime capture, the root-cause frame or state transition, the focused regression guard, and any sibling matches that were fixed or explicitly left safe.
references/rendering-debug.md
# Rendering Bug Debug Reference
## Rendering Bug Mode
Activate when: "PDF looks wrong", "page break issue", "font not rendering", broken PDF output, print layout wrong.
Static analysis first (CSS review), then reproduce if needed.
### WeasyPrint
- `rgba()` causes double-rectangle bug: use solid hex colors instead
- `page-break-inside: avoid` is often ignored: use explicit breaks
- Float-based layouts often break at page boundaries: prefer flexbox or block
- External font URLs blocked at render time: embed fonts as base64 or host locally
### Font Loading
- Check `@font-face` src paths (relative vs. absolute)
- CORS headers must allow the render origin for external fonts
- Format support: WeasyPrint prefers WOFF/TTF; WOFF2 support depends on version
- Missing font fallback = invisible text or system fallback glyph
### Page Overflow
- Calculate content height vs. page height before debugging line-by-line
- Reduce `line-height`, `padding`, or `margin` to reclaim space
- Orphan/widow control: `orphans: 3; widows: 3` in `@page` body rule
### Browser Print CSS
- Confirm `@media print` rules are present and not overridden
- `@page` margin must account for printer unprintable area (~6mm minimum)
- `break-before: page` / `break-after: page` on section dividers
- Test with `window.print()` in browser DevTools, not just visual preview
SKILL.md
---
name: hunt
description: "Finds root cause before applying fixes for errors, crashes, regressions, failing tests, broken behavior, and screenshot-reported defects. Use when users report in any language errors, crashes, broken behavior, regressions, failing tests, screenshot evidence, or something that used to work and now fails. Not for code review or new features."
when_to_use: "排查, 查查, 报错, 崩溃, 不工作, 不对, 跑不通, 以前是好的, 回归, 截图回归, 判断错误原因, 判断为什么报错, 反复修不好, debug, regression, used to work, broke after update, why broken, not working, what's wrong, fix error, stack trace"
dispatch_intent: "Error, crash, regression, screenshot-reported defect, test failure, stale cache, runtime boundary, why broken"
---
# Hunt: Diagnose Before You Fix
Prefix your first line with 🥷 inline, not as its own paragraph.
A patch applied to a symptom creates a new bug somewhere else.
## Outcome Contract
- Outcome: the root cause is identified before any fix is applied.
- Done when: one sentence explains the cause, every observed symptom fits it, and the fix or handoff is verified against a reproducible check.
- Evidence: source trace, repro command or UI path, logs or state, targeted test/build output, and runtime evidence for UI or native defects.
- Output: root cause, fix or handoff, verification result, and any unswept sibling risks.
- Authorization: "diagnose", "investigate", "why", "look into", "排查", "看看", or equivalent is report-only. Apply a fix only when the current turn explicitly asks to fix, change, implement, or optimize; root-cause proof is still required first.
**Do not touch code until you can state the root cause in one sentence:**
> "I believe the root cause is [X] because [evidence]."
Name a specific file, function, line, or condition. "A state management issue" is not testable. "Stale cache in `useUser` at `src/hooks/user.ts:42` because the dependency array is missing `userId`" is testable. If you cannot be that specific, you do not have a hypothesis yet.
## Diagnosis Signals
Hypothesis quality gate: the hypothesis must explain every observable symptom, not just the one reported first; partial coverage is a symptom-level guess, not a root cause. A symptom the reporter waves off as unrelated is still a symptom the hypothesis has to cover. For timing-dependent issues (flicker, intermittent failure, race), reproduce reliably before diagnosing.
Rationalization smells: "I'll just try this" = no hypothesis, write it first. "I'm confident" = run the instrument that proves it. "Probably the same issue" = re-read the execution path from scratch. "It works on my machine" = enumerate env differences before dismissing. "One more restart" = read the last error verbatim; never restart more than twice without new evidence.
## Durable Context Preflight
See [references/durable-context.md](references/durable-context.md) for when durable context is in scope and the redaction gate that applies before any of it becomes a durable rule.
For `/hunt`: durable context is hypothesis fuel only, and current code, logs, and repro evidence override memory. It never replaces a fresh root-cause sentence or a reproducible symptom list.
## Fix Scope Discipline
If the bug needs a prerequisite refactor (e.g. a shared interface must change), state why it is necessary and check the authorized scope. Continue if that work is covered; ask before expanding the scope or choosing an unresolved behavior tradeoff. Keep unrelated refactors separate.
## Bisect Mode
Activate when: "以前是好的", "之前是好的", "used to work", "上一次提交还是对的", "broke after update", or the user remembers a specific good commit or version.
- Protect the user's worktree first: `git status --short --branch -uall`. Any modified, staged, or untracked files mean no bisect in the current checkout: run it in a temporary detached worktree and remove that worktree when done. If a temporary worktree is impossible, stop and ask for explicit cleanup/stash approval.
- If the last-good version is only a few releases back, `git diff <last-good>..HEAD -- <suspect path>` and read the delta first. The regression is usually visible there at a fraction of a bisect's cost; fall through to bisect only when the diff is too large or the culprit is not obvious.
- Bisect only with a non-interactive pass/fail command defined up front, and keep the bookkeeping in git (`git bisect good/bad`), including when you test a suspect commit directly. When it names the culprit, read only that diff down to the specific line, then run `git bisect reset` before removing the temporary worktree.
## Repeated Regression / Screenshot Reference Mode
Activate when the user says the same issue is still wrong after a fix, provides a "good" screenshot/version/file, or describes a visual result as previously correct.
Treat the reference as evidence, not decoration: list every reported and visible symptom in the user's concrete words; identify the reference oracle (last-good commit, old build, fixture, screenshot, described expected state); define the pass/fail check before editing; then name the exact current-vs-reference delta. Do not generalize a visual defect into "style polish" when the evidence points to a broken render, race, font pipeline, or state path.
If the issue is purely subjective UI taste, route to `/ui`. If it is rendering, state, timing, build output, font generation, or a regression from a known-good version, stay in `/hunt`.
## Scope Blast Mode
Activate after fixing a root-cause pattern, before declaring the bug done; also when the user says "举一反三", "举一反三深入看看", or "其他地方有没有同样问题". The same shape often hides in N other places; one local fix that ignores the blast leaves N - 1 bugs in the tree.
Extract the pattern signature (the specific function, regex, API call, CSS selector, lock acquisition, validation skip, or input boundary that produced the bug) and `grep -rn` it across the repo, excluding generated dirs, build output, and vendored deps; for class-of-bug patterns ("any handler missing the lock"), grep the surrounding shape, not just the literal text. For every match, answer in writing: same bug / safe to leave (why) / unsure (ask the user). Do not silently skip a match, and do not claim "fixed" until the blast report is in the Output block. Unrelated bugs the sweep surfaces get listed, not fixed in this PR, unless the user agrees.
## Confirm or Discard
Run the one probe that would fail if the hypothesis were wrong, then read it. If the evidence contradicts the hypothesis, discard it completely and re-orient on what the probe just showed. Do not stack a fix onto a disproven hypothesis, and do not keep one just because the code "looks like" the cause.
## Runtime Evidence Ladder
Use this ladder before claiming a bug is fixed:
1. Source trace: name the exact function, state transition, file, line, or condition that can produce the symptom.
2. Deterministic repro: run or write the smallest command, fixture, UI path, or scenario that produces it.
3. Logs/state/cache: inspect the runtime state that proves the path was reached, including queues, DB rows, caches, temp files, generated outputs, or external tool logs.
4. Build/test: run the narrow test or build that exercises the fix.
5. Real runtime check: for UI, native app, browser, rendering, or visual bugs, open the app/page/artifact and verify the visible result with a screenshot or concrete checklist.
Compile-only is not enough for UI, native-app, visual, rendering, or generated-artifact bugs. If the runtime check is impossible in the environment, say why and hand off the exact screen, command, or artifact to verify.
When the reporter's environment is the missing rung and it cannot be reproduced locally, the next artifact is a read-only probe they can paste and run, not another hypothesis. Have it print the environment, the disputed measurement, and the state of whatever the hypothesis turns on, and nothing that could carry a secret or a private path. Assume none of your own layout: their install method, directory conventions, locale, shell, and version all differ, so discover rather than hardcode. Ship it as plain copyable text with one command to run and one block to paste back.
For recurring classes of failures, load `references/failure-patterns.md` before adding a second fix.
## Native App Freeze Mode
For beachballs, not responding, tab-switch freezes, first-open lag, idle wake stalls, overlay lockups, or frozen-app screenshots, load `references/logging-techniques.md` (Native App Freeze Mode) before changing code.
## Targeted Logging
Every log is a yes/no question: "if this prints X before Y, hypothesis A survives; otherwise A is dead." A log that cannot rule a hypothesis in or out is noise. Remove temporary logs before finishing; gate persistent diagnostics behind the project's debug flag. If adding a log changes the behavior, that is itself evidence of a timing, lifecycle, or concurrency problem. Full playbook: `references/logging-techniques.md`.
## Rendering Bug Mode
For PDF output, page breaks, font rendering, or print layout defects, load `references/rendering-debug.md`; it carries the activation triggers and the diagnosis checklist (WeasyPrint quirks, font loading, page overflow, browser print CSS).
## IME / Unicode Issues
For input method, character rendering, or text encoding bugs (IME state, cursor drift, emoji splitting, composition events), check `references/ime-unicode.md` first before forming a hypothesis.
## Hard Rules
- **Same symptom after a fix is a hard stop; so is "let me just try this."** Both mean the hypothesis is unfinished. Re-read the execution path from scratch before touching code again.
- **After three failed hypotheses, stop.** Use the Handoff format below to surface what was checked, what was ruled out, and what is unknown. Ask how to proceed.
- **External tool failure: diagnose before switching.** When an MCP tool or API fails, determine why first (server running? API key valid? Config correct?) before trying an alternative.
- **System/tooling symptoms need a lower-layer baseline.** Before blaming the visible app, generated file, or top-level feature, measure the raw lower layer first: OS capture versus post-processing, runtime service versus UI, compiler/toolchain versus test assertion, network/API versus client handling. Retire hypotheses that the baseline disproves instead of circling them.
- **Visual/rendering bugs: static analysis first.** Trace paint layers, stacking contexts, and layer order in DevTools before adding console.log or visual debug overlays. Logs cannot capture what the compositor does. Only add instrumentation after static analysis fails.
- **Behavioral / lifecycle / async bugs: instrument while forming the hypothesis.** Window lifecycle, event delivery, navigation, focus, timer, state-machine, and async-ordering bugs almost never yield to static reading alone. The moment the hypothesis involves "this callback fires before/after that one", "this state should be X when Y runs", or "this object should still be alive here", add the log before writing any fix (anti-pattern 28); two guesses in a row is the hard-stop signal. Compositor behavior needs DevTools, not logs; pure-logic bugs (wrong formula, off-by-one) need only static analysis.
- **Tuning magic numbers past round three: stop, unify.** When a spacing / sizing / threshold value has been adjusted three times and still looks wrong, the bug is structural, not numeric. Replace the N independent values with one named token (`Spacing.s4`, `--gap-content`, etc.) and verify the asymmetry was hiding a missing constraint. Asymmetry that survives tuning is structural; more tuning will not converge.
- **Performance complaints need numbers.** For "slow", "laggy", or memory-growth reports outside Native App Freeze Mode, measure the baseline first (wall-clock time, profile sample, memory footprint), fix, then re-measure and report before/after numbers. "Feels faster" is not evidence.
- **Fix the cause, not the symptom.** Continue necessary fixes within the user's authorized scope. Ask only when the fix expands that scope or requires a user decision; file count alone is not an approval boundary.
## Gotchas
| What happened | Rule |
|---------------|------|
| Patched the wrong copy of a duplicated surface | Trace the execution path backward to the instance that actually renders before touching any file |
| Orchestrator reported RUNNING while a downstream stage was misconfigured | In multi-stage pipelines, test each stage in isolation |
| Race condition diagnosed as a stale-state bug | For timing-sensitive issues, inspect event timestamps and ordering before state |
| Reproduced locally but failed in CI | Align the environment first (runtime version, env vars, timezone), then chase the code |
| Stack trace points deep into a library | Walk back 3 frames into your own code; the bug is almost always there, not in the dependency |
| Worked when launched from app, broke when opened via file association / drag-drop / deep link / external proxy | Reproduce using the exact entry point the user described. App-internal init differs from cold-launch-with-file init; state may not be ready when the document arrives. |
| Fix matched the reporter's setup but changed nothing for everyone else, or regressed the default | A defect report is evidence, not the full scope. State whether the fix changes the default experience for all users or only the reporter's configuration, and prefer fixing the default path. |
| Broke after toggling theme / mode / locale, fine after restart | State not re-applied on the toggle path. Trace the toggle's recompute or invalidation route first; do not tune styles pixel by pixel while the state path is broken. |
| Changed the algorithm but the output stayed wrong | The reader may be hitting persisted output written by the old code (scan results, analysis cache, snapshot with a TTL). Changing generated-then-persisted data requires invalidating or version-bumping the old cache in the same change; before re-diagnosing, confirm the runtime is not reading stale data. |
| Fixed the one cause that reproduced, shipped, and the same gate blocked the next user for a different reason | A guard that refuses has a set of causes, not one. Enumerate every branch that can refuse before shipping, and give each a distinguishable code, a one-line reason, and a next command. |
| The user's observation and the log disagreed, and the log won | Trust the observation and treat the gap as an un-instrumented path. A probe that passes on the happy path says nothing about the failing one; a probe that cannot reproduce is an invalid probe, not an absent defect. |
| Patched a capability-gated feature on a surface that never offered the capability | Confirm the run surface (simulator, device, sandbox, restricted entitlement) supports it before writing a fix. If it does not, say so and stop; no source change makes it appear. |
## Output
### Success Format
Open the wrap-up with one plain line stating the outcome and whether the changes are committed; the block below supports that line, it does not replace it.
```
Root cause: [what was wrong, file:line]
Fix: [what changed, file:line]
Sibling sweep: [N same-shape sites checked, N fixed / none found / not run, why]
Confirmed: [evidence or test that proves the fix]
Tests: [pass/fail count, regression test location]
Regression guard: [test file:line] or [none, reason]
```
Status: **resolved**, **resolved with caveats** (state them), or **blocked** (state what is unknown).
**Regression guard rule**: for any bug that recurred or was previously "fixed", the fix is not done until:
1. A regression test exists that fails on the unfixed code and passes on the fixed code.
2. The test lives in the project's test suite, not a temporary file.
3. The commit message states why the bug recurred and why this fix prevents it.
4. Red-green was **run**, not assumed: revert the fix (or stash it), watch the new test fail, restore the fix, watch it pass. A regression test that has only ever been observed passing pins nothing. State the red run in the output. Two shapes make this fail silently: a framework or syntax where a failing assertion mid-test does not fail the test, so only the last one gates (in shell suites this can hinge on the bracket form alone, with one keyword swallowed and the other caught, so confirm which by running a two-line minimal repro rather than reasoning about it); and an assertion that the wrong string is absent, which passes forever because that string was never emitted under any code version. Any negative assertion ("output must not contain X") also needs a paired positive case in the same test proving the assertion can fail at all.
### Handoff Format (after 3 failed hypotheses)
```
Symptom:
[Original error description, one sentence]
Hypotheses Tested:
1. [Hypothesis 1] → [Test method] → [Result: ruled out because...]
2. [Hypothesis 2] → [Test method] → [Result: ruled out because...]
3. [Hypothesis 3] → [Test method] → [Result: ruled out because...]
Evidence Collected:
- [Log snippets / stack traces / file content]
- [Reproduction steps]
- [Environment info: versions, config, runtime]
Ruled Out:
- [Root causes that have been eliminated]
Unknowns:
- [What is still unclear]
- [What information is missing]
Suggested Next Steps:
1. [Next investigation direction]
2. [External tools or permissions that may be needed]
3. [Additional context the user should provide]
```
Status: **blocked**