Bug Check
Systematic hunt for bugs that survive intuitive code review. This skill applies
pattern-based checks derived from what Qodo and CodeRabbit consistently find that
human and AI reviewers miss.
How this differs from pr-review: pr-review reads the diff holistically and reasons
about what could go wrong. Bug-check applies a checklist of known miss patterns
to every changed file. It is systematic where pr-review is intuitive.
Before starting
- Load AGENTS.md for project conventions.
- Load code standards + preference recall — discover the standards notes first
(the set grows; hardcoded lists go stale), read the pass-relevant results,
then recall the dated evidence trail for the change's domain (surfaces
preferences newer than the notes):
vault_search({ query: "code standards", filters: { tags: ["code-standards"], type: "reference", properties: { lifecycle: "living" } } })
vault_read_note the results (currently typescript and docs), plus any newer
note matching the repo's language/stack
vault_memory_recall({ query: "<change domain>" })
- Identify all files changed in the branch vs main:
git diff main --name-only
- Scope check: Production code (
.ts, .js), infrastructure (.yml, .yaml,
Dockerfile, docker-compose*), environment config (.env, .env.*,
.env.example), and config files (sst.config.*, *.json) are in scope. CI/workflow files ARE in scope — dimension 1
(description-vs-implementation) applies to workflow step descriptions, job names,
and conditional logic. User-facing documentation (.md guides, READMEs) is in
scope for dimension 1 when the docs make factual claims about the system —
privacy/security guarantees, architecture, data flow, storage locations, capability
claims, or tool categorization. Verify each claim against the implementation. Pure
formatting/prose changes to docs are out of scope — but a docs PR that
says "no external communication" when the system has an outbound sync service is a
D1 bug, not a style issue. Structural changes (reordering sections, changing which
method leads, folding content into asides) are NOT out of scope: they trigger the
D1 structural self-reference check and the D5 alternative-command-path check.
Only report "docs only — nothing to check" when no factual claims about the
system are present AND no doc was restructured.
- Skip test files — test-audit handles those.
- Read each changed file in full — not just the diff. Bugs hide in how new
code interacts with surrounding context.
Dimensions
Run each dimension against every changed production file. Dimension 1 gets the most
time — it is the highest-yield check (40%+ of bot findings fall here).
1. Description-vs-implementation verification
The #1 source of missed bugs. The description says one thing; the code does another.
For every function, tool definition, or doc comment in changed files:
- Quote each sentence of the description verbatim before checking it. This is
mandatory proof-of-work — it surfaces truncated, garbled, or incomplete text that
looks fine at a glance but fails on close read. A sentence like "even if a folder
can't be." is obviously incomplete when quoted, but easy to skip over when skimming.
- Extract every factual claim from the quoted sentences:
- "Returns results sorted by X" → requires ORDER BY X
- "Detectable via tool_Y" → tool_Y must actually do that
- "Creates X if not found" → creation logic must exist
- "Requires parameter Z" → Z must be validated as required
- "Returns empty array, not an error" → verify no throw on empty
- Trace each claim to the implementation. Read the actual code path.
- Flag any mismatch: claim not implemented, implemented differently,
references the wrong function/tool/concept, or is truncated/garbled text.
- Decide which side is wrong before fixing. A mismatch has two possible
fixes, and rewriting the description to match the code is the cheaper one — so
it is the one this check drifts toward, and it hides the bug instead of fixing
it. The description states the intent. If the intent is what the project wants,
the code is the bug and the fix goes in the code. The code is the wrong side
when: the description matches a user-facing promise (a setting is honored, a
copy is preserved), the code's actual behavior is the destructive or less
capable branch, or the mismatch comes from an implementation shortcut such as
an env var standing in for a state the code should detect directly. Every D1
finding names which side changed and why. Examples of the trap: a description
says the tool honors a vault setting and the code skips the setting whenever
one env var is present — qualifying the description leaves every deployment
that sets the state another way behaving wrong; a doc comment says a fallback
value is not cached and the code caches it — matching the comment to the code
means a later config change is never seen. In both, the code was the bug.
Cross-references are a known hot spot. When a description mentions another tool
or function by name:
- Read the referenced tool/function's description and implementation — don't
just check the name exists. Confirm it actually does what the referencing
description claims it does.
- Verify directionality — the most common error is naming the wrong sibling
(e.g., "use findOrphans to find broken links" when orphans are nodes with no
incoming links, not nodes with broken outgoing links; or referencing
getOutgoingLinks when getBacklinks is the correct direction).
- Verify workflows end-to-end — when a description prescribes a multi-step
procedure ("after doing X, use Y to find Z"), trace the full workflow. Each
step must produce the output the next step expects. A cross-reference that is
individually correct can still be wrong in context if the workflow logic is
flawed.
Conditional capabilities described unconditionally: When a description claims a
capability (e.g., "Hybrid search combining FTS and vector similarity"), check whether
that capability is gated behind a feature flag, env var, or optional dependency
(embedder, API key, external service). If it is, the description must either qualify
the claim ("when embeddings are enabled") or describe the degraded mode ("falls back
to keyword-only search"). A description that presents a conditional capability as
always-available is a D1 bug — the LLM reading it will instruct users about features
that may not exist in their deployment. This applies to tool descriptions, README
capability lists, and API documentation. Trace the code path: if there's an early
return or if (!dependency) guard that skips the advertised behavior, the description
is wrong for that branch.
Mechanism mischaracterization: When a description uses lifecycle or state language
— "starts X then switches to Y", "caches results", "batches requests", "streams
results", "lazy-loads on first use", "queues work" — verify the code actually
implements that mechanism, not just the outcome. A per-query fallback is not a state
transition. A synchronous call is not a queue. The behavior may be correct (users get
the right result) but the mechanism claim teaches a false mental model that misleads
debugging and integration. Signal words to check: "starts", "transitions", "switches",
"initializes once" in descriptions of stateless per-call logic; "batches", "queues",
"pools" in descriptions of sequential per-item processing. Example: a README that says
"search starts FTS-only while the vector index builds, then switches to hybrid
automatically" when the code actually checks vectorHits.length === 0 per-query with
no global mode state — the fallback is stateless, not a one-time transition.
Stale claims: Check that descriptions still match after refactoring. A renamed
function, moved parameter, or changed return type can leave the description accurate
for the old code but wrong for the new.
Structural self-references in docs: prose that references the document's own
structure — "shown below", "the manual setup above", "the following section", "as
described earlier" — is a claim about the document, and restructuring breaks it the
same way refactoring breaks code claims. For each such reference in a changed doc,
resolve it against the CURRENT document: the target content must still exist, still
sit in the stated position, and still be what the sentence says it is. A reference
to content that was replaced, moved, or demoted into a collapsible aside is a D1
bug even when every command in the document still works. References that still
resolve correctly need no change.
CI/workflow files (.yml): Apply dimension 1 to step names, job names, and
conditional logic. A step named "Configure Tailscale" that is always skipped due to
an if: scoping bug is a description-vs-implementation mismatch. Also check:
- Multi-trigger event guards: when a workflow has multiple triggers in
on:
(e.g., pull_request + issue_comment), check whether each job or step should
run for ALL triggers or only specific ones. Steps that make sense only for PRs
(review phases, diff analysis, CI checks) need if: github.event_name == 'pull_request' guards — otherwise a comment-triggered run executes them too.
The trigger: a workflow with >1 event in on: and steps/jobs without if:
guards on github.event_name. Trace each step's purpose and ask "does this
make sense when triggered by [other event]?"
if: conditions reference variables that are actually visible at evaluation time
(GitHub evaluates if: before the step runs — step-level env: is not visible)
- Action pinning inconsistency: if some actions in the workflow are pinned to
commit SHAs and others use mutable tags (
v2, @main), the unpinned ones are
a D1 mismatch — the workflow's security posture is inconsistent. Check all
uses: lines; flag any third-party action on a tag when siblings are on SHAs
- Permissions wider than usage: compare
permissions: grants against what the
job's steps actually use. If every step authenticates via a GitHub App token and
never uses GITHUB_TOKEN for writes, contents: write or pull-requests: write
on the default token is unnecessary privilege
- Secrets are scoped to the narrowest level (step, not job) to limit exposure
persist-credentials: false on checkout steps
- Deploy workflows have
concurrency: blocks to prevent overlapping runs
2. SQL correctness
For every SQL query in changed files:
- Description-query alignment: Does the query implement what the tool description
promises? "Sorted by modification date" needs
ORDER BY mtime DESC.
- Aggregation accuracy: COUNT(*) vs COUNT(DISTINCT column) — overcounting is
common when JOINs multiply rows.
- Determinism: Any
LIMIT without ORDER BY returns nondeterministic results.
If the function's contract implies stable ordering, this is a bug.
- WHERE completeness: Could rows leak through that shouldn't? Check that all
documented filter conditions appear in the query.
- GROUP BY correctness: Every non-aggregated SELECT column must be in GROUP BY
(SQLite is lenient here, but the results are undefined for missing columns).
3. Type safety and coercion
Look for these patterns in changed code:
- Truthiness bugs:
if (!x) where x could legitimately be 0, "", or
false. Replace with x === undefined or x === null for absence checks.
- Loose equality:
== 0 or == "" almost always wants strict ===.
- Type assertions:
as casts and ! non-null assertions bypass the compiler.
Verify each is safe, or replace with a runtime guard.
- Missing narrowing: After a type check (
typeof x === 'string'), using x
outside the narrowed block where it is still the union type.
- Buffer/ArrayBuffer view aliasing: When code accesses
.buffer on a
TypedArray (Float32Array, Uint8Array, etc.) and passes it to Buffer.from(),
new DataView(), or another constructor — check that byteOffset and
byteLength are also passed. typedArray.buffer returns the entire
backing ArrayBuffer, which is larger than the view when the typed array
is a subarray or slice. The 1-arg form Buffer.from(arr.buffer) silently
produces the wrong data with no error. Fix: use the 3-arg form
Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength). This pattern is
especially common in embedding/ML pipelines where providers may return
Float32Array views over shared buffers.
4. Boundary and off-by-one
For each function with numeric parameters (limit, offset, index, count), and for
each comparison against a sentinel or a bound:
- Empty input: What happens with an empty array, string, or result set?
- Truncation indicators: If showing "N+" for overflow, does the code fetch
limit + 1 to detect truncation? Showing "5+" when exactly 5 results exist
is a known bug pattern.
- Inclusive vs exclusive: Is the range
[start, end] or [start, end)?
Off-by-one in slice, substring, and SQL LIMIT/OFFSET.
- Zero and one: These values expose special-case bugs. Does the function
handle
limit: 0 or offset: 0 correctly?
- Sentinel-returning searches:
indexOf, lastIndexOf, findIndex, and
search return -1 for absent and 0 for found-at-start. A > 0 guard treats
found-at-start as absent; the check is >= 0 or !== -1. Boundary: "the
input can never start with the delimiter because it is validated upstream" is
not a boundary — the guard is wrong on its own terms, and when the string has a
formal structure the fix is the stdlib parser (path.parse, URL) that removes
the guard entirely.
5. Behavioral consistency
Look for asymmetric handling of similar constructs:
- Parallel code paths: If function A handles one input variant one way and
a sibling variant another way, verify the difference is intentional.
Common: one path handles an edge case that the other path misses. Also check
merge/fusion points — when parallel data sources (e.g., FTS + vector
search, cache + DB, local + remote) are combined into a single result set,
verify that constraints (filters, access controls, limits) are applied
equivalently to each source BEFORE the merge. Asymmetric filtering distorts
the merged ranking — one source contributes unfiltered items that dilute or
displace filtered results from the other source. Flag even if intentional —
the reviewer decides scope, you decide what to report.
- Overly broad transformations: An operation applied everywhere when it should
be scoped to a specific context (e.g., stripping escape characters globally
instead of only in table cells where they appear). Concrete instance:
trim() vs trimEnd() — when only trailing whitespace cleanup is intended
but leading whitespace carries semantic meaning (indentation, list nesting
level), trim() silently destroys structure. Flag trim() on any string
where leading whitespace encodes hierarchy or formatting.
- Shared helpers not used: Before accepting a language primitive or stdlib
call in new code, grep
src/utils/ and nearby modules for an existing helper
that does the same job. This includes bounded alternatives to unbounded
primitives — if mapWithConcurrency exists and new code uses bare
Promise.all for the same fan-out pattern, that's a miss even though
Promise.all isn't "reimplementing" the helper. The trigger is: new code does
concurrent work, file manipulation, error formatting, or string processing →
grep for helpers first, then justify why the primitive is correct if one exists.
- Inconsistent defaults: Same parameter with different defaults in different
functions — one uses
true, another uses false, with no documented reason.
- Config default divergence across file types: When a PR adds or modifies
environment variables, check that defaults agree across all layers —
docker-compose (
${VAR:-value}), .env.example, CLI-generated .env
templates (e.g., env.ts), and the config parser in code. Check ALL
config surfaces — a var added to docker-compose and .env.example but
missing from CLI templates means npx <tool> init users never see it.
Also check that .env.example comments match their values: a comment
saying "Disable X" with a value of # X=true is a D1 bug — uncommenting
enables X, not disables it. A common trap: compose uses ${VAR:-} (empty
string when unset) but the config parser uses .default("true") which
only applies when the var is absent, not when it's empty. Empty-string
defaults bypass code-level defaults and can cause startup failures or
silent misconfiguration.
- Alternative command paths in docs: when documentation offers more than one
literal command sequence for the same task (quick-start one-liner vs config
file vs installer/CLI tool), treat them as parallel code paths. Compare the
resource identifiers they create or reference — data volume/directory names,
container/service names, project names, ports, generated file paths. Mismatched
identifiers mean a user who switches methods silently loses access to existing
data or state; either the identifiers must match across methods or the doc must
state that the methods are not interchangeable. Also compare configuration
flags: an alternative that silently omits behavior the other methods set up
(health checks, restart policy, log limits) needs a note saying what it omits.
Skip when the methods are explicitly documented as independent of each other.
Example: a manual
docker run one-liner mounting -v vault_data:/vault while
the CLI and Compose paths both use the prefixed vault-cortex_vault_data
volume — a user who starts with the one-liner and later runs the CLI gets
empty volumes and a full re-sync.
6. Input validation and error paths
For each function that accepts parameters:
- Parameter combinations: What happens with unexpected combos? (e.g.,
section without file, heading_level without heading). Look for silent
fallthrough where an error or early return is needed.
- Input normalization: Case-sensitive comparisons on user input that could
arrive in mixed case (URLs, file extensions, header values).
- Error message safety: Do error messages include internal file paths,
internal state, or implementation details that shouldn't reach clients?
- Guard correctness: Is a guard condition (
if (value !== "")) the right
check? Could it be unconditional, or does it need a different condition?
- Silent catches:
.catch(() => {}) and catch (e) {} swallow errors with
no trace. Every catch must either log or re-throw. Catching without logging is
worse than not catching — it actively hides failures that affect debuggability.
When fixing a silent catch, always add a log call with the error and enough
context (file path, operation name) to diagnose the failure from the log alone.
- Widened eligibility: When a filter or eligibility check is broadened
(new condition added via
||, new file type accepted, new input source
supported), trace what guarantees the old filter implicitly provided.
The new branch must provide equivalent guarantees — or add explicit
validation for the ones it loses. Common: isFile() → isFile() || isSymbolicLink() without adding realpath() containment, stat().isFile(),
or broken-symlink handling. Also: widening a query scope, accepting a new
auth method, or supporting a new content type without corresponding validation.
- Fallback direction on failure: When a read, parse, or lookup fails and
the code falls back to a default, check which branch that default selects. If
the default is the destructive or irreversible branch (permanent delete,
overwrite, skip a validation, grant access), a transient failure silently does
the worst thing. Separate "not configured" (ENOENT, key absent) from "could
not read" (EACCES, EISDIR, EIO, a parse error): the first may take the
default; the second must fail or take the conservative branch. A catch that
logs at debug level and returns the destructive default is a finding even
though it is not a silent catch.
- I/O error paths: For each filesystem or network call in changed code
(
rename, mkdir, unlink, link, fetch), list the errors it raises in
the intended scenario — EEXIST, ENOTDIR, EISDIR, EACCES, EXDEV — and
trace what the caller reports for each. A raw errno message that names an
absolute path, an operation left half-done (the source still present after a
"delete"), or an operation that previously always succeeded turned into a hard
failure by the new path are each findings. Existing call sites in the same
module show what the error contract is; the new call must meet it.
7. Platform and encoding
Check for assumptions that break on real-world input:
- CRLF: Does text processing assume
\n? Content from Windows or mixed
sources may contain \r\n. Check line splitting, blank-line detection, and
regex anchors.
- Timezone: Are date operations consistent? Hardcoded UTC when the rest of the
codebase uses local time (or vice versa) is a subtle bug.
- Unicode: String length/slice operations on multi-byte characters. Regex
patterns that assume ASCII.
- Symlinks and filesystem indirection: Does code that walks directories
or reads files follow symlinks without validating targets? Check for:
(a) containment —
realpath() to resolve the target, then verify it stays
within the allowed root (lexical prefix checks on the original path are
insufficient); (b) type verification — after resolving, stat().isFile()
to reject directories masquerading as files (e.g., a directory named *.md);
(c) broken links — realpath() throws ENOENT for dangling symlinks, catch
and skip gracefully; (d) defense in depth — validate at every entry point
(search root, directory listing root, individual entries), not just the
innermost level. A symlinked search root can bypass all per-entry checks.
- TOCTOU (time-of-check-time-of-use): Is there a gap between checking a
file's properties (stat, readdir) and acting on it (readFile, index)? In
concurrent environments, the file can change between check and use.
- Snapshot staleness in background tasks: When a PR introduces background
processing using a startup snapshot (e.g., "snapshot all notes, then embed
them in the background"), check whether the live code path (file watcher,
API handler) can invalidate entries in the snapshot while it's being consumed.
Content-hash gating protects against stale updates (the hash won't match) but
NOT against deletes — if an entry is deleted after the snapshot but before the
background loop reaches it, the loop recreates stale data for a deleted entity.
Check: does the background loop verify the entity still exists before writing?
Rules that override intuition
These rules exist because agents consistently rationalize skipping findings that
turn out to be real and fixable. Each rule addresses a specific failure pattern.
No silent skipping
Every finding must appear in the output — including pre-existing gaps discovered
during analysis. "Pre-existing, not introduced by this PR" is context for
categorization, not a reason to omit the finding. If you noticed it, report it.
The orchestrator and user cannot act on findings they never see. A bug you found
and silently dismissed is worse than a bug you missed — you confirmed the problem
exists and then hid it.
Pre-existing analog gaps
When the PR correctly implements a pattern (e.g., adds EMBEDDING_ENABLED to all
6 docker-compose files), and you notice an analogous existing feature is missing
from those same files (e.g., MEMORY_ENABLED absent from all of them) — that is a
finding. Report it. The PR's correct implementation revealed the gap; the fix is
typically mechanical (copy the pattern the PR already established).
Categorize as: pre-existing gap with a note on whether the fix is trivial.
The orchestrator decides scope; you decide what to report.
No environment-specific dismissals
Don't use one deployment's specs to dismiss resource, performance, or scaling
concerns. "On Lightsail with 4GB RAM and 772 notes, this is negligible" is not a
valid dismissal for an OSS project where users may have 10x the data on half the
RAM. Evaluate concerns against the worst reasonable use case for the project's
audience — not the maintainer's current setup.
The same rule covers deployment shape and upstream validation. "One vault per
process", "the input is always .md because it was validated earlier", and "this
can't happen in practice" describe today's callers, not the code. A guard that is
wrong on its own terms, a module-level cache that is not keyed by the thing it
caches for, or a fallback that picks the destructive branch is a finding whether
or not the current deployment can reach it. Report it, categorize the reach as
context, and let the orchestrator decide scope.
And regardless of impact assessment: if the fix is trivial, fix it. A one-line
null-out after data is consumed costs nothing and eliminates the concern entirely.
Verify effort before claiming it
Before calling any fix "high lift," "would change every call site," or "complex
refactor" — grep for actual usages. The difference between "every call site
across the codebase" and "one call site" is the difference between deferring a
finding and fixing it in 30 seconds. Never estimate effort from intuition when a
10-second grep gives the real answer.
Your fixes must follow AGENTS.md
You loaded AGENTS.md in "Before starting." Apply it to the code you WRITE, not
just the code you review. If AGENTS.md bans as assertions, your fix uses a
runtime guard instead. If it requires early returns, your fix uses early returns.
Bug-check runs after code-quality in the pipeline — no convention pass follows
yours. Convention violations in your fixes ship unchecked.
Procedure
- For each changed production file, read the full file content (not just the
diff). Log that you read it — cite what you checked as proof of work:
vault-crud-tools.ts (650 lines) — 3 tool defs, 5 cross-refs, 0 SQL, 2 guards
"0 findings" after 60 seconds across multiple files means the checks were
skipped, not that the code is clean.
- Weight effort by dimension yield: dimension 1 (description-vs-code) gets
the most scrutiny. For each description, quote every sentence verbatim (step 1 of
dimension 1) then extract and trace claims. This is not optional — it's the
mechanism that catches truncated text and garbled prose that skimming misses.
Dimensions 6-7 are quicker but still require reading the code.
- One line per finding, then fix it:
[D1] file.ts:612 — description refs vault_find_orphans, should be vault_get_backlinks → description fixed (code matches intent)
[D1] file.ts:940 — description says setting is honored, code skips it when an env var is set → code fixed (env var was a proxy for state)
[D4] file.ts:88 — off-by-one in LIMIT, fetches N not N+1 for truncation → fixed (low confidence, trivial fix)
[D5] file.ts:200 — async init race if called concurrently → flagged (complex fix — needs mutex or queue)
Don't describe the planned fix — the diff speaks for itself. A D1 line always
says which side changed, description or code, and why that side was wrong.
Decide fix vs. flag on two axes — diagnosis confidence and fix complexity.
Call sequentialthinking before each disposition decision — input the finding,
confidence level, and fix complexity; output which matrix cell it falls in and why:
- High/medium confidence → fix directly.
- Low confidence + trivial fix (< 5 lines, no interface change) → fix it.
The cost of a safe no-op is near zero; the cost of missing a real bug is not.
"Low-risk" is a reason TO fix, not a reason to defer.
- Low confidence + complex/risky fix → flag with category.
When flagging, categorize as:
uncertain diagnosis, complex fix,
needs design decision, or pre-existing gap. The orchestrator uses
these categories to triage.
- Stage, commit, and push all fixes.
- Report:
Bug check complete:
- Files checked: N
- Reviewed at: <PR head SHA>
- Bugs found: N (M fixed, K flagged for review)
- By dimension:
- Description mismatch: A
- SQL correctness: B
- Type safety: C
- Boundary/off-by-one: D
- Behavioral consistency: E
- Input validation: F
- Platform/encoding: G
- Confidence: N high, M medium, K low
- Dismissed: N (proof-of-dismissal one-liners follow — or "none")
Output honesty (both modes):
- State what you reviewed. The summary names the PR head SHA actually
reviewed — a review that doesn't say what it checked is indistinguishable
from one that checked nothing. It also lets the orchestrator cross-check
what this phase actually saw against the delta-review baseline it records
itself at Phase 4 close.
- Close with proof of dismissal. One line per suspicion you seriously
considered and dropped, with the reason it doesn't bite — or "none". This
extends "No silent skipping" to the negative space: findings you confirmed
go in the report, and suspicions you cleared go in the dismissal list —
without them, "no findings" could mean a clean diff or an unexamined one,
and the reader can't tell which.
Comment mode
When the dispatch prompt says COMMENT MODE, do not edit files, commit, or push.
Instead, collect all findings and post them as a single GitHub PR review with inline
comments.
Procedure
- Hunt normally — read every changed file in full, apply all 7 dimensions, use
sequential thinking for disposition decisions. The only difference is the output path.
- Collect findings as you go. Each finding needs: file path (relative to repo root),
line number, dimension tag, confidence level, and description.
- Still categorize on two axes — confidence × complexity. In comment mode,
fixed
becomes would fix (high/medium confidence, trivial change) and flagged findings keep
their category.
- Post a single PR review with all findings as inline comments:
gh api "repos/OWNER_REPO/pulls/PR_NUMBER/reviews" \
--method POST --input - <<'REVIEW'
{
"event": "COMMENT",
"body": "## Phase 4: Bug Check\n\nN bugs found across M files. Reviewed at <HEAD_SHA>.\nConfidence: A high, B medium, C low\n\nDismissed: <proof-of-dismissal one-liners — or \"none\">\n\n---\n*🔍 ship-check · bug-check · MODEL_ID*",
"comments": [
{
"path": "src/file.ts",
"line": 612,
"body": "**[D1]** Description refs `vault_find_orphans`, should be `vault_get_backlinks`\n\nThe description says \"find notes with broken links\" but `vault_find_orphans` finds notes with no *incoming* links. `vault_get_backlinks` is the correct tool for outgoing link validation.\n\n*Would fix (trivial, high confidence)*\n\n---\n*🔍 ship-check · bug-check · MODEL_ID*"
}
]
}
REVIEW
Replace OWNER_REPO and PR_NUMBER with values from the dispatch prompt. Replace
MODEL_ID with your own model ID (from your system prompt).
- If 0 findings and no dismissals, skip the API call — report "0 findings"
to the orchestrator only. With 0 findings but cleared suspicions, post a
body-only review carrying the dismissal list — that is the artifact that lets
a PR reader tell a clean diff from an unexamined one.
- Footer on every comment. Append
\n\n---\n*🔍 ship-check · bug-check · MODEL_ID*
to the review body AND each inline comment body.
- Format each inline comment body as:
- Bold dimension tag:
**[D1]**, **[D3]**, etc.
- One-line description of the bug
- Evidence: quote the description sentence, trace the code path, explain the mismatch
- Suggested fix (code snippet when possible)
- Disposition:
Would fix (trivial, <confidence>) or Flagged: <category>
- Footer (see above)
1---2name: bug-check3description: Systematic bug hunt focused on patterns that survive code review and test audit — description-vs-implementation mismatches, SQL correctness, type coercion bugs, boundary/off-by-one errors, behavioral asymmetry, and input validation gaps. Derived from analysis of 40+ bugs found by Qodo and CodeRabbit that the ship-check pipeline (pr-review, code-quality, test-audit) missed. Use when asked to "bug check", "check for bugs", "deep correctness check", "look for subtle bugs", or as part of the ship-check pipeline. NOT for: code style (use code-quality), test design (use test-audit), security review (use security-review), or high-level correctness review (use pr-review).4---56# Bug Check78Systematic hunt for bugs that survive intuitive code review. This skill applies9**pattern-based checks** derived from what Qodo and CodeRabbit consistently find that10human and AI reviewers miss.1112How this differs from pr-review: pr-review reads the diff holistically and reasons13about what could go wrong. Bug-check applies a **checklist of known miss patterns**14to every changed file. It is systematic where pr-review is intuitive.1516## Before starting17181. Load AGENTS.md for project conventions.192. Load code standards + preference recall — discover the standards notes first20 (the set grows; hardcoded lists go stale), read the pass-relevant results,21 then recall the dated evidence trail for the change's domain (surfaces22 preferences newer than the notes):23 - `vault_search({ query: "code standards", filters: { tags: ["code-standards"], type: "reference", properties: { lifecycle: "living" } } })`24 - `vault_read_note` the results (currently typescript and docs), plus any newer25 note matching the repo's language/stack26 - `vault_memory_recall({ query: "<change domain>" })`273. Identify all files changed in the branch vs main:28 ```29 git diff main --name-only30 ```314. **Scope check**: Production code (`.ts`, `.js`), infrastructure (`.yml`, `.yaml`,32 `Dockerfile`, `docker-compose*`), environment config (`.env`, `.env.*`,33 `.env.example`), and config files (`sst.config.*`, `*.json`) are in scope. CI/workflow files ARE in scope — dimension 134 (description-vs-implementation) applies to workflow step descriptions, job names,35 and conditional logic. User-facing documentation (`.md` guides, READMEs) is in36 scope for dimension 1 when the docs make factual claims about the system —37 privacy/security guarantees, architecture, data flow, storage locations, capability38 claims, or tool categorization. Verify each claim against the implementation. Pure39 formatting/prose changes to docs are out of scope — but a docs PR that40 says "no external communication" when the system has an outbound sync service is a41 D1 bug, not a style issue. Structural changes (reordering sections, changing which42 method leads, folding content into asides) are NOT out of scope: they trigger the43 D1 structural self-reference check and the D5 alternative-command-path check.44 Only report "docs only — nothing to check" when no factual claims about the45 system are present AND no doc was restructured.465. Skip test files — test-audit handles those.476. **Read each changed file in full** — not just the diff. Bugs hide in how new48 code interacts with surrounding context.4950## Dimensions5152Run each dimension against every changed production file. Dimension 1 gets the most53time — it is the highest-yield check (40%+ of bot findings fall here).5455### 1. Description-vs-implementation verification5657The #1 source of missed bugs. The description says one thing; the code does another.5859**For every function, tool definition, or doc comment in changed files:**60611. **Quote each sentence of the description verbatim** before checking it. This is62 mandatory proof-of-work — it surfaces truncated, garbled, or incomplete text that63 looks fine at a glance but fails on close read. A sentence like "even if a folder64 can't be." is obviously incomplete when quoted, but easy to skip over when skimming.652. Extract every **factual claim** from the quoted sentences:66 - "Returns results sorted by X" → requires ORDER BY X67 - "Detectable via tool_Y" → tool_Y must actually do that68 - "Creates X if not found" → creation logic must exist69 - "Requires parameter Z" → Z must be validated as required70 - "Returns empty array, not an error" → verify no throw on empty713. **Trace each claim to the implementation.** Read the actual code path.724. **Flag any mismatch**: claim not implemented, implemented differently,73 references the wrong function/tool/concept, or is truncated/garbled text.745. **Decide which side is wrong before fixing.** A mismatch has two possible75 fixes, and rewriting the description to match the code is the cheaper one — so76 it is the one this check drifts toward, and it hides the bug instead of fixing77 it. The description states the intent. If the intent is what the project wants,78 the code is the bug and the fix goes in the code. The code is the wrong side79 when: the description matches a user-facing promise (a setting is honored, a80 copy is preserved), the code's actual behavior is the destructive or less81 capable branch, or the mismatch comes from an implementation shortcut such as82 an env var standing in for a state the code should detect directly. Every D183 finding names which side changed and why. Examples of the trap: a description84 says the tool honors a vault setting and the code skips the setting whenever85 one env var is present — qualifying the description leaves every deployment86 that sets the state another way behaving wrong; a doc comment says a fallback87 value is not cached and the code caches it — matching the comment to the code88 means a later config change is never seen. In both, the code was the bug.8990**Cross-references are a known hot spot.** When a description mentions another tool91or function by name:921. **Read the referenced tool/function's description and implementation** — don't93 just check the name exists. Confirm it actually does what the referencing94 description claims it does.952. **Verify directionality** — the most common error is naming the wrong sibling96 (e.g., "use findOrphans to find broken links" when orphans are nodes with no97 *incoming* links, not nodes with broken *outgoing* links; or referencing98 `getOutgoingLinks` when `getBacklinks` is the correct direction).993. **Verify workflows end-to-end** — when a description prescribes a multi-step100 procedure ("after doing X, use Y to find Z"), trace the full workflow. Each101 step must produce the output the next step expects. A cross-reference that is102 individually correct can still be wrong in context if the workflow logic is103 flawed.104105**Conditional capabilities described unconditionally**: When a description claims a106capability (e.g., "Hybrid search combining FTS and vector similarity"), check whether107that capability is gated behind a feature flag, env var, or optional dependency108(embedder, API key, external service). If it is, the description must either qualify109the claim ("when embeddings are enabled") or describe the degraded mode ("falls back110to keyword-only search"). A description that presents a conditional capability as111always-available is a D1 bug — the LLM reading it will instruct users about features112that may not exist in their deployment. This applies to tool descriptions, README113capability lists, and API documentation. Trace the code path: if there's an early114return or `if (!dependency)` guard that skips the advertised behavior, the description115is wrong for that branch.116117**Mechanism mischaracterization**: When a description uses lifecycle or state language118— "starts X then switches to Y", "caches results", "batches requests", "streams119results", "lazy-loads on first use", "queues work" — verify the code actually120implements that mechanism, not just the outcome. A per-query fallback is not a state121transition. A synchronous call is not a queue. The behavior may be correct (users get122the right result) but the mechanism claim teaches a false mental model that misleads123debugging and integration. Signal words to check: "starts", "transitions", "switches",124"initializes once" in descriptions of stateless per-call logic; "batches", "queues",125"pools" in descriptions of sequential per-item processing. Example: a README that says126"search starts FTS-only while the vector index builds, then switches to hybrid127automatically" when the code actually checks `vectorHits.length === 0` per-query with128no global mode state — the fallback is stateless, not a one-time transition.129130**Stale claims:** Check that descriptions still match after refactoring. A renamed131function, moved parameter, or changed return type can leave the description accurate132for the old code but wrong for the new.133134**Structural self-references in docs**: prose that references the document's own135structure — "shown below", "the manual setup above", "the following section", "as136described earlier" — is a claim about the document, and restructuring breaks it the137same way refactoring breaks code claims. For each such reference in a changed doc,138resolve it against the CURRENT document: the target content must still exist, still139sit in the stated position, and still be what the sentence says it is. A reference140to content that was replaced, moved, or demoted into a collapsible aside is a D1141bug even when every command in the document still works. References that still142resolve correctly need no change.143144**CI/workflow files** (`.yml`): Apply dimension 1 to step names, job names, and145conditional logic. A step named "Configure Tailscale" that is always skipped due to146an `if:` scoping bug is a description-vs-implementation mismatch. Also check:147- **Multi-trigger event guards**: when a workflow has multiple triggers in `on:`148 (e.g., `pull_request` + `issue_comment`), check whether each job or step should149 run for ALL triggers or only specific ones. Steps that make sense only for PRs150 (review phases, diff analysis, CI checks) need `if: github.event_name ==151 'pull_request'` guards — otherwise a comment-triggered run executes them too.152 The trigger: a workflow with >1 event in `on:` and steps/jobs without `if:`153 guards on `github.event_name`. Trace each step's purpose and ask "does this154 make sense when triggered by [other event]?"155- `if:` conditions reference variables that are actually visible at evaluation time156 (GitHub evaluates `if:` before the step runs — step-level `env:` is not visible)157- **Action pinning inconsistency**: if some actions in the workflow are pinned to158 commit SHAs and others use mutable tags (`v2`, `@main`), the unpinned ones are159 a D1 mismatch — the workflow's security posture is inconsistent. Check all160 `uses:` lines; flag any third-party action on a tag when siblings are on SHAs161- **Permissions wider than usage**: compare `permissions:` grants against what the162 job's steps actually use. If every step authenticates via a GitHub App token and163 never uses `GITHUB_TOKEN` for writes, `contents: write` or `pull-requests: write`164 on the default token is unnecessary privilege165- Secrets are scoped to the narrowest level (step, not job) to limit exposure166- `persist-credentials: false` on checkout steps167- Deploy workflows have `concurrency:` blocks to prevent overlapping runs168169### 2. SQL correctness170171**For every SQL query in changed files:**1721731. **Description-query alignment**: Does the query implement what the tool description174 promises? "Sorted by modification date" needs `ORDER BY mtime DESC`.1752. **Aggregation accuracy**: COUNT(*) vs COUNT(DISTINCT column) — overcounting is176 common when JOINs multiply rows.1773. **Determinism**: Any `LIMIT` without `ORDER BY` returns nondeterministic results.178 If the function's contract implies stable ordering, this is a bug.1794. **WHERE completeness**: Could rows leak through that shouldn't? Check that all180 documented filter conditions appear in the query.1815. **GROUP BY correctness**: Every non-aggregated SELECT column must be in GROUP BY182 (SQLite is lenient here, but the results are undefined for missing columns).183184### 3. Type safety and coercion185186**Look for these patterns in changed code:**187188- **Truthiness bugs**: `if (!x)` where `x` could legitimately be `0`, `""`, or189 `false`. Replace with `x === undefined` or `x === null` for absence checks.190- **Loose equality**: `== 0` or `== ""` almost always wants strict `===`.191- **Type assertions**: `as` casts and `!` non-null assertions bypass the compiler.192 Verify each is safe, or replace with a runtime guard.193- **Missing narrowing**: After a type check (`typeof x === 'string'`), using `x`194 outside the narrowed block where it is still the union type.195- **Buffer/ArrayBuffer view aliasing**: When code accesses `.buffer` on a196 `TypedArray` (Float32Array, Uint8Array, etc.) and passes it to `Buffer.from()`,197 `new DataView()`, or another constructor — check that `byteOffset` and198 `byteLength` are also passed. `typedArray.buffer` returns the *entire*199 backing `ArrayBuffer`, which is larger than the view when the typed array200 is a subarray or slice. The 1-arg form `Buffer.from(arr.buffer)` silently201 produces the wrong data with no error. Fix: use the 3-arg form202 `Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength)`. This pattern is203 especially common in embedding/ML pipelines where providers may return204 Float32Array views over shared buffers.205206### 4. Boundary and off-by-one207208**For each function with numeric parameters (limit, offset, index, count), and for209each comparison against a sentinel or a bound:**2102111. **Empty input**: What happens with an empty array, string, or result set?2122. **Truncation indicators**: If showing "N+" for overflow, does the code fetch213 `limit + 1` to detect truncation? Showing "5+" when exactly 5 results exist214 is a known bug pattern.2153. **Inclusive vs exclusive**: Is the range `[start, end]` or `[start, end)`?216 Off-by-one in slice, substring, and SQL LIMIT/OFFSET.2174. **Zero and one**: These values expose special-case bugs. Does the function218 handle `limit: 0` or `offset: 0` correctly?2195. **Sentinel-returning searches**: `indexOf`, `lastIndexOf`, `findIndex`, and220 `search` return -1 for absent and 0 for found-at-start. A `> 0` guard treats221 found-at-start as absent; the check is `>= 0` or `!== -1`. Boundary: "the222 input can never start with the delimiter because it is validated upstream" is223 not a boundary — the guard is wrong on its own terms, and when the string has a224 formal structure the fix is the stdlib parser (`path.parse`, `URL`) that removes225 the guard entirely.226227### 5. Behavioral consistency228229**Look for asymmetric handling of similar constructs:**2302311. **Parallel code paths**: If function A handles one input variant one way and232 a sibling variant another way, verify the difference is intentional.233 Common: one path handles an edge case that the other path misses. Also check234 **merge/fusion points** — when parallel data sources (e.g., FTS + vector235 search, cache + DB, local + remote) are combined into a single result set,236 verify that constraints (filters, access controls, limits) are applied237 equivalently to each source BEFORE the merge. Asymmetric filtering distorts238 the merged ranking — one source contributes unfiltered items that dilute or239 displace filtered results from the other source. Flag even if intentional —240 the reviewer decides scope, you decide what to report.2412. **Overly broad transformations**: An operation applied everywhere when it should242 be scoped to a specific context (e.g., stripping escape characters globally243 instead of only in table cells where they appear). Concrete instance:244 `trim()` vs `trimEnd()` — when only trailing whitespace cleanup is intended245 but leading whitespace carries semantic meaning (indentation, list nesting246 level), `trim()` silently destroys structure. Flag `trim()` on any string247 where leading whitespace encodes hierarchy or formatting.2483. **Shared helpers not used**: Before accepting a language primitive or stdlib249 call in new code, grep `src/utils/` and nearby modules for an existing helper250 that does the same job. This includes bounded alternatives to unbounded251 primitives — if `mapWithConcurrency` exists and new code uses bare252 `Promise.all` for the same fan-out pattern, that's a miss even though253 `Promise.all` isn't "reimplementing" the helper. The trigger is: new code does254 concurrent work, file manipulation, error formatting, or string processing →255 grep for helpers first, then justify why the primitive is correct if one exists.2564. **Inconsistent defaults**: Same parameter with different defaults in different257 functions — one uses `true`, another uses `false`, with no documented reason.2585. **Config default divergence across file types**: When a PR adds or modifies259 environment variables, check that defaults agree across all layers —260 docker-compose (`${VAR:-value}`), `.env.example`, CLI-generated `.env`261 templates (e.g., `env.ts`), and the config parser in code. Check ALL262 config surfaces — a var added to docker-compose and `.env.example` but263 missing from CLI templates means `npx <tool> init` users never see it.264 Also check that `.env.example` comments match their values: a comment265 saying "Disable X" with a value of `# X=true` is a D1 bug — uncommenting266 enables X, not disables it. A common trap: compose uses `${VAR:-}` (empty267 string when unset) but the config parser uses `.default("true")` which268 only applies when the var is absent, not when it's empty. Empty-string269 defaults bypass code-level defaults and can cause startup failures or270 silent misconfiguration.2716. **Alternative command paths in docs**: when documentation offers more than one272 literal command sequence for the same task (quick-start one-liner vs config273 file vs installer/CLI tool), treat them as parallel code paths. Compare the274 resource identifiers they create or reference — data volume/directory names,275 container/service names, project names, ports, generated file paths. Mismatched276 identifiers mean a user who switches methods silently loses access to existing277 data or state; either the identifiers must match across methods or the doc must278 state that the methods are not interchangeable. Also compare configuration279 flags: an alternative that silently omits behavior the other methods set up280 (health checks, restart policy, log limits) needs a note saying what it omits.281 Skip when the methods are explicitly documented as independent of each other.282 Example: a manual `docker run` one-liner mounting `-v vault_data:/vault` while283 the CLI and Compose paths both use the prefixed `vault-cortex_vault_data`284 volume — a user who starts with the one-liner and later runs the CLI gets285 empty volumes and a full re-sync.286287### 6. Input validation and error paths288289**For each function that accepts parameters:**2902911. **Parameter combinations**: What happens with unexpected combos? (e.g.,292 `section` without `file`, `heading_level` without `heading`). Look for silent293 fallthrough where an error or early return is needed.2942. **Input normalization**: Case-sensitive comparisons on user input that could295 arrive in mixed case (URLs, file extensions, header values).2963. **Error message safety**: Do error messages include internal file paths,297 internal state, or implementation details that shouldn't reach clients?2984. **Guard correctness**: Is a guard condition (`if (value !== "")`) the right299 check? Could it be unconditional, or does it need a different condition?3005. **Silent catches**: `.catch(() => {})` and `catch (e) {}` swallow errors with301 no trace. Every catch must either log or re-throw. Catching without logging is302 worse than not catching — it actively hides failures that affect debuggability.303 When fixing a silent catch, always add a log call with the error and enough304 context (file path, operation name) to diagnose the failure from the log alone.3056. **Widened eligibility**: When a filter or eligibility check is broadened306 (new condition added via `||`, new file type accepted, new input source307 supported), trace what guarantees the old filter implicitly provided.308 The new branch must provide equivalent guarantees — or add explicit309 validation for the ones it loses. Common: `isFile()` → `isFile() ||310 isSymbolicLink()` without adding `realpath()` containment, `stat().isFile()`,311 or broken-symlink handling. Also: widening a query scope, accepting a new312 auth method, or supporting a new content type without corresponding validation.3137. **Fallback direction on failure**: When a read, parse, or lookup fails and314 the code falls back to a default, check which branch that default selects. If315 the default is the destructive or irreversible branch (permanent delete,316 overwrite, skip a validation, grant access), a transient failure silently does317 the worst thing. Separate "not configured" (ENOENT, key absent) from "could318 not read" (EACCES, EISDIR, EIO, a parse error): the first may take the319 default; the second must fail or take the conservative branch. A catch that320 logs at debug level and returns the destructive default is a finding even321 though it is not a silent catch.3228. **I/O error paths**: For each filesystem or network call in changed code323 (`rename`, `mkdir`, `unlink`, `link`, `fetch`), list the errors it raises in324 the intended scenario — `EEXIST`, `ENOTDIR`, `EISDIR`, `EACCES`, `EXDEV` — and325 trace what the caller reports for each. A raw errno message that names an326 absolute path, an operation left half-done (the source still present after a327 "delete"), or an operation that previously always succeeded turned into a hard328 failure by the new path are each findings. Existing call sites in the same329 module show what the error contract is; the new call must meet it.330331### 7. Platform and encoding332333**Check for assumptions that break on real-world input:**334335- **CRLF**: Does text processing assume `\n`? Content from Windows or mixed336 sources may contain `\r\n`. Check line splitting, blank-line detection, and337 regex anchors.338- **Timezone**: Are date operations consistent? Hardcoded UTC when the rest of the339 codebase uses local time (or vice versa) is a subtle bug.340- **Unicode**: String length/slice operations on multi-byte characters. Regex341 patterns that assume ASCII.342- **Symlinks and filesystem indirection**: Does code that walks directories343 or reads files follow symlinks without validating targets? Check for:344 (a) containment — `realpath()` to resolve the target, then verify it stays345 within the allowed root (lexical prefix checks on the original path are346 insufficient); (b) type verification — after resolving, `stat().isFile()`347 to reject directories masquerading as files (e.g., a directory named `*.md`);348 (c) broken links — `realpath()` throws ENOENT for dangling symlinks, catch349 and skip gracefully; (d) defense in depth — validate at every entry point350 (search root, directory listing root, individual entries), not just the351 innermost level. A symlinked search root can bypass all per-entry checks.352- **TOCTOU (time-of-check-time-of-use)**: Is there a gap between checking a353 file's properties (stat, readdir) and acting on it (readFile, index)? In354 concurrent environments, the file can change between check and use.355- **Snapshot staleness in background tasks**: When a PR introduces background356 processing using a startup snapshot (e.g., "snapshot all notes, then embed357 them in the background"), check whether the live code path (file watcher,358 API handler) can invalidate entries in the snapshot while it's being consumed.359 Content-hash gating protects against stale updates (the hash won't match) but360 NOT against deletes — if an entry is deleted after the snapshot but before the361 background loop reaches it, the loop recreates stale data for a deleted entity.362 Check: does the background loop verify the entity still exists before writing?363364## Rules that override intuition365366These rules exist because agents consistently rationalize skipping findings that367turn out to be real and fixable. Each rule addresses a specific failure pattern.368369### No silent skipping370371Every finding must appear in the output — including pre-existing gaps discovered372during analysis. "Pre-existing, not introduced by this PR" is context for373categorization, not a reason to omit the finding. If you noticed it, report it.374375The orchestrator and user cannot act on findings they never see. A bug you found376and silently dismissed is worse than a bug you missed — you confirmed the problem377exists and then hid it.378379### Pre-existing analog gaps380381When the PR correctly implements a pattern (e.g., adds `EMBEDDING_ENABLED` to all3826 docker-compose files), and you notice an analogous existing feature is missing383from those same files (e.g., `MEMORY_ENABLED` absent from all of them) — that is a384finding. Report it. The PR's correct implementation revealed the gap; the fix is385typically mechanical (copy the pattern the PR already established).386387Categorize as: `pre-existing gap` with a note on whether the fix is trivial.388The orchestrator decides scope; you decide what to report.389390### No environment-specific dismissals391392Don't use one deployment's specs to dismiss resource, performance, or scaling393concerns. "On Lightsail with 4GB RAM and 772 notes, this is negligible" is not a394valid dismissal for an OSS project where users may have 10x the data on half the395RAM. Evaluate concerns against the worst reasonable use case for the project's396audience — not the maintainer's current setup.397398The same rule covers deployment shape and upstream validation. "One vault per399process", "the input is always `.md` because it was validated earlier", and "this400can't happen in practice" describe today's callers, not the code. A guard that is401wrong on its own terms, a module-level cache that is not keyed by the thing it402caches for, or a fallback that picks the destructive branch is a finding whether403or not the current deployment can reach it. Report it, categorize the reach as404context, and let the orchestrator decide scope.405406And regardless of impact assessment: if the fix is trivial, fix it. A one-line407null-out after data is consumed costs nothing and eliminates the concern entirely.408409### Verify effort before claiming it410411Before calling any fix "high lift," "would change every call site," or "complex412refactor" — **grep for actual usages**. The difference between "every call site413across the codebase" and "one call site" is the difference between deferring a414finding and fixing it in 30 seconds. Never estimate effort from intuition when a41510-second grep gives the real answer.416417### Your fixes must follow AGENTS.md418419You loaded AGENTS.md in "Before starting." Apply it to the code you WRITE, not420just the code you review. If AGENTS.md bans `as` assertions, your fix uses a421runtime guard instead. If it requires early returns, your fix uses early returns.422Bug-check runs after code-quality in the pipeline — no convention pass follows423yours. Convention violations in your fixes ship unchecked.424425## Procedure4264271. **For each changed production file**, read the full file content (not just the428 diff). Log that you read it — cite what you checked as proof of work:429 ```430 vault-crud-tools.ts (650 lines) — 3 tool defs, 5 cross-refs, 0 SQL, 2 guards431 ```432 "0 findings" after 60 seconds across multiple files means the checks were433 skipped, not that the code is clean.4342. **Weight effort by dimension yield**: dimension 1 (description-vs-code) gets435 the most scrutiny. For each description, quote every sentence verbatim (step 1 of436 dimension 1) then extract and trace claims. This is not optional — it's the437 mechanism that catches truncated text and garbled prose that skimming misses.438 Dimensions 6-7 are quicker but still require reading the code.4393. **One line per finding, then fix it:**440 ```441 [D1] file.ts:612 — description refs vault_find_orphans, should be vault_get_backlinks → description fixed (code matches intent)442 [D1] file.ts:940 — description says setting is honored, code skips it when an env var is set → code fixed (env var was a proxy for state)443 [D4] file.ts:88 — off-by-one in LIMIT, fetches N not N+1 for truncation → fixed (low confidence, trivial fix)444 [D5] file.ts:200 — async init race if called concurrently → flagged (complex fix — needs mutex or queue)445 ```446 Don't describe the planned fix — the diff speaks for itself. A D1 line always447 says which side changed, description or code, and why that side was wrong.448 **Decide fix vs. flag on two axes** — diagnosis confidence and fix complexity.449 **Call `sequentialthinking` before each disposition decision** — input the finding,450 confidence level, and fix complexity; output which matrix cell it falls in and why:451 - **High/medium confidence** → fix directly.452 - **Low confidence + trivial fix** (< 5 lines, no interface change) → fix it.453 The cost of a safe no-op is near zero; the cost of missing a real bug is not.454 "Low-risk" is a reason TO fix, not a reason to defer.455 - **Low confidence + complex/risky fix** → flag with category.456 When flagging, categorize as: `uncertain diagnosis`, `complex fix`,457 `needs design decision`, or `pre-existing gap`. The orchestrator uses458 these categories to triage.4594. Stage, commit, and push all fixes.4605. Report:461462```463Bug check complete:464- Files checked: N465- Reviewed at: <PR head SHA>466- Bugs found: N (M fixed, K flagged for review)467- By dimension:468 - Description mismatch: A469 - SQL correctness: B470 - Type safety: C471 - Boundary/off-by-one: D472 - Behavioral consistency: E473 - Input validation: F474 - Platform/encoding: G475- Confidence: N high, M medium, K low476- Dismissed: N (proof-of-dismissal one-liners follow — or "none")477```478479**Output honesty (both modes):**480481- **State what you reviewed.** The summary names the PR head SHA actually482 reviewed — a review that doesn't say what it checked is indistinguishable483 from one that checked nothing. It also lets the orchestrator cross-check484 what this phase actually saw against the delta-review baseline it records485 itself at Phase 4 close.486- **Close with proof of dismissal.** One line per suspicion you seriously487 considered and dropped, with the reason it doesn't bite — or "none". This488 extends "No silent skipping" to the negative space: findings you confirmed489 go in the report, and suspicions you cleared go in the dismissal list —490 without them, "no findings" could mean a clean diff or an unexamined one,491 and the reader can't tell which.492493## Comment mode494495When the dispatch prompt says **COMMENT MODE**, do not edit files, commit, or push.496Instead, collect all findings and post them as a single GitHub PR review with inline497comments.498499### Procedure5005011. **Hunt normally** — read every changed file in full, apply all 7 dimensions, use502 sequential thinking for disposition decisions. The only difference is the output path.5032. **Collect findings** as you go. Each finding needs: file path (relative to repo root),504 line number, dimension tag, confidence level, and description.5053. **Still categorize on two axes** — confidence × complexity. In comment mode, `fixed`506 becomes `would fix` (high/medium confidence, trivial change) and flagged findings keep507 their category.5084. **Post a single PR review** with all findings as inline comments:509510```bash511gh api "repos/OWNER_REPO/pulls/PR_NUMBER/reviews" \512 --method POST --input - <<'REVIEW'513{514 "event": "COMMENT",515 "body": "## Phase 4: Bug Check\n\nN bugs found across M files. Reviewed at <HEAD_SHA>.\nConfidence: A high, B medium, C low\n\nDismissed: <proof-of-dismissal one-liners — or \"none\">\n\n---\n*🔍 ship-check · bug-check · MODEL_ID*",516 "comments": [517 {518 "path": "src/file.ts",519 "line": 612,520 "body": "**[D1]** Description refs `vault_find_orphans`, should be `vault_get_backlinks`\n\nThe description says \"find notes with broken links\" but `vault_find_orphans` finds notes with no *incoming* links. `vault_get_backlinks` is the correct tool for outgoing link validation.\n\n*Would fix (trivial, high confidence)*\n\n---\n*🔍 ship-check · bug-check · MODEL_ID*"521 }522 ]523}524REVIEW525```526527Replace `OWNER_REPO` and `PR_NUMBER` with values from the dispatch prompt. Replace528`MODEL_ID` with your own model ID (from your system prompt).5295305. **If 0 findings and no dismissals**, skip the API call — report "0 findings"531 to the orchestrator only. With 0 findings but cleared suspicions, post a532 body-only review carrying the dismissal list — that is the artifact that lets533 a PR reader tell a clean diff from an unexamined one.5346. **Footer on every comment.** Append `\n\n---\n*🔍 ship-check · bug-check · MODEL_ID*`535 to the review body AND each inline comment body.5367. **Format each inline comment body** as:537 - Bold dimension tag: `**[D1]**`, `**[D3]**`, etc.538 - One-line description of the bug539 - Evidence: quote the description sentence, trace the code path, explain the mismatch540 - Suggested fix (code snippet when possible)541 - Disposition: `Would fix (trivial, <confidence>)` or `Flagged: <category>`542 - Footer (see above)