Short orientation for Copilot / Claude / similar agents. Read first before making changes.
Never include a ? character in prose addressed to the user. Any
sentence ending in ? that asks them to decide, choose, confirm,
clarify, or weigh in goes in an ask_user tool call — even when the
answer seems obvious. When the set of plausible answers is discrete,
pass them via the tool's choices array. This rule applies in every
runtime mode and regardless of whether the runtime reports the user as
available.
A single WPF desktop application: a side-by-side Git diff viewer for
Windows (with an inline mode toggle). Compares two DiffSide values —
CommitIsh or WorkingTree — so the supported pairings are working
tree vs HEAD, working tree vs a commit, or commit vs commit. The repo
contains two projects at the root:
DiffViewer/— the WPF app (DiffViewer.csproj).net8.0-windows, WPF,Nullableenabled,ImplicitUsingsenabled,LangVersionset inline in the csproj.DiffViewer.Tests/— xUnit unit tests covering view-models, services, utilities, rendering helpers, and recent-contexts state (DiffViewer.Tests.csproj). ReferencesDiffViewer.csprojand reachesinternaltypes viaInternalsVisibleTo("DiffViewer.Tests")declared inDiffViewer.csproj. The test project has no other internal dependencies.
Load-bearing third-party libraries (do not propose replacements without
discussion): CommunityToolkit.Mvvm (MVVM source generators),
AvalonEdit (text / diff rendering), DiffPlex (diff algorithm),
LibGit2Sharp (in-process Git operations; ships native git2-*.dll).
There is no shared library, no Directory.Build.props /
Directory.Packages.props / global.json / NuGet.config at the repo
root. Each csproj is fully self-contained: target framework, nullable
settings, and package versions all live in the project file.
- Make surgical changes that fully address the request. Do not refactor unrelated code, rename files, or reformat untouched areas.
- If you find a tightly-coupled bug caused by the code you're changing, fix it. Otherwise, note it and move on.
- Prefer ecosystem tooling (
dotnet new,dotnet add package) over manual file or csproj edits.
- Ask rather than guessing on behavioral choices, defaults, limits, or scope (see §0). See §4 for the plan-and-approval flow.
- Discussion is not approval. When you ask a clarifying question or offer the user a choice among options, their answer is input to your plan, not a command to execute. Picking option B from a multiple-choice you offered is the user choosing a direction, not authorizing the change. Wait for an explicit go-ahead — phrases like "implement", "go ahead", "ship it" — before touching code.
- Prefer the simpler, convention-aligned option over a clever alternative. "Simpler" means less mechanical complexity — fewer special cases, less hidden state, less reader cognitive load — not smaller diff. A clean refactor with more lines changed is often simpler than a patch that layers a workaround on a workaround. See §4 on not defaulting to minimal change.
- Plan before changing code, every time. Before writing or modifying any code — even a one-line typo fix or "obvious" bug fix — propose a short plan: what you will change, in which files, with what verification, and at least one viable alternative with tradeoffs (or an explicit note that no meaningful alternative exists). Surface open questions and wait for the user's explicit approval before touching code. Approval covers only the plan as presented; any material scope change or newly discovered work requires a revised plan and fresh approval.
- Direct user commands are self-approving (narrow exception). When the user issues an unambiguous, scoped command (e.g., "delete file X", "revert commit abc123", "rerun the tests"), the request itself is the plan and the approval. Echo back a one-line confirmation of exactly what you are about to do, then proceed. This bypass applies to the bounded command portion only; any adjacent question or implied cleanup still goes through the standard plan-and-approve flow. Phrases like "continue", "finish it", "take care of the rest", or "do the obvious cleanup" are not unambiguous commands.
- Bug reports and feature ideas are plan-triggers, not execute-triggers. A user message that describes a problem ("X is broken", "this looks weird", "the spacing is off") or proposes an idea ("we should add Y", "what if we...") is a request to investigate and propose a plan — it is not authorization to edit, test, or commit. The execute step requires an unambiguous command verb in the same or a later turn ("fix it", "implement that", "ship it").
- CI failures are plan-triggers, not retry-triggers. Never re-run a failed CI job or push a speculative fix on the agent's own judgment when CI is red. The default response to any CI failure is: tell the user which job failed, summarize what broke (test, step, assertion, what the changeset touched), and ask what to do. Silent retries hide both flakes and genuine regressions.
- Use
ask_user; never "make a best guess on autopilot". If the user is unavailable, ask anyway and stop; do not start implementing. An autonomous runtime is not authorization to guess. - Treat user suggestions as proposals, not orders. Evaluate each one critically before acting. If you spot a flaw, a missed case, a simpler alternative, or a risk the user may not have weighed, say so before implementing. Be direct: a concrete recommendation with a reason beats vague hedging. Disagree when you have a reason to; do not silently comply with a request you believe is wrong or risky. After the user decides, follow their decision unless they ask you to push back again.
- Don't default to the minimal change. Recommending the smaller of two competing options because it has less churn is risk-aversion masquerading as pragmatism. Judge options on long-term architectural fit, not diff size. If your reasoning for the smaller option leans on phrases like "less diff," "less churn," "less risk of breakage," or "captures most of the benefit," those are tactical considerations — stop and re-evaluate on architectural grounds. One-time refactor cost is finite and reviewable; ongoing cost of structural compromise is not. Present the architecturally-correct option as the recommendation; the user can still choose the conservative path, but they should choose it knowingly, not because the recommendation pre-baked the bias. This rule applies to the option-weighing / recommendation phase only; once a plan is approved, §2 Scope discipline still governs execution.
Before declaring a task done:
dotnet build -c Releasesucceeds with no new warnings.dotnet testpasses.- If you touched anything inside the
<PropertyGroup Condition="'$(Configuration)' == 'Release'">block ofDiffViewer.csproj,.github/workflows/release.yml, or the publish-related dependency surface (LibGit2Sharp version, native library handling), verify the release publish works:dotnet publish DiffViewer\DiffViewer.csproj -c Release -o publishmust produce a workingpublish\DiffViewer.exe.IncludeNativeLibrariesForSelfExtract=trueis load-bearing for LibGit2Sharp'sgit2-*.dll; removing it breaks the single-file release silently at runtime. .editorconfigis honored; no new diagnostic suppressions beyond the existingCS8618softening.- Package versions for the four load-bearing libraries (CommunityToolkit.Mvvm, AvalonEdit, DiffPlex, LibGit2Sharp) were not bumped without an explicit reason recorded in the commit body.
- Runtime:
net8.0-windows, WPF,Nullable enable,ImplicitUsings enable,LangVersion latest. - MVVM: CommunityToolkit.Mvvm 8.4.0. New view-models inherit
ObservableObject, mark the classpartial, and use[ObservableProperty]for bindable fields and[RelayCommand]for commands. Do not hand-rollINotifyPropertyChangedin new code. - Diff algorithm: DiffPlex.
- Editor / syntax rendering: AvalonEdit.
- Git access: LibGit2Sharp (in-process). Do not shell out to
git.exefrom production code without explicit discussion — the in-process path is what makes the single-file publish self-contained. - Tests: xUnit 2.9 + FluentAssertions 6.12, run via
dotnet test.
Discuss before introducing new MVVM frameworks, DI containers, UI toolkits (Avalonia, MAUI, WinUI 3, etc.), logging libraries, or test frameworks.
Likewise do not bump major versions of the four load-bearing libraries on a whim — each has known integration quirks (single-file publish for LibGit2Sharp, theming for AvalonEdit, source-generator behavior for CommunityToolkit.Mvvm).
DiffViewer/
ViewModels/ # All view-models live here; reachable from tests
# via InternalsVisibleTo
Models/
Services/
Utility/
Assets/ # diffviewer.ico etc.
*.xaml / *.xaml.cs # Views and minimal code-behind
DiffViewer.Tests/ # xUnit; mirrors DiffViewer namespace structure
.github/workflows/ # build.yml (CI), release.yml (tag-driven)
- Build:
dotnet build -c Release - Test:
dotnet test - Run from source:
dotnet run --project DiffViewer\DiffViewer.csproj - Produce a release-equivalent single-file exe (matches the CI release):
dotnet publish DiffViewer\DiffViewer.csproj -c Release -o publish
- New view-models inherit
ObservableObjectfrom CommunityToolkit.Mvvm, are declaredpartial, and use[ObservableProperty]/[RelayCommand]source generators. Reference examples:DiffViewer/ViewModels/MainViewModel.cs,DiffViewer/ViewModels/DiffPaneViewModel.cs. - View-models avoid
System.Windows.*types where reasonable so xUnit can host them without spinning up a WPF dispatcher. When a type fromSystem.Windowsis genuinely needed (e.g., a binding-compatible enum), isolate it behind an interface that tests can stub. - Code-behind (
*.xaml.cs) stays thin — view wiring only, no business logic. Logic belongs in a view-model or service.
- Never block the UI thread on LibGit2Sharp calls or other I/O. Do the
work on a background thread (
Task.Run/async) and marshal results back viaDispatcher.InvokeAsync— or rely onawait's capturedSynchronizationContextif the caller was on the UI thread. - View-model property setters raise
PropertyChangedon the calling thread; bound writes must happen on the dispatcher. If a background task writes a bindable property directly, that is a bug.
.editorconfigsoftens onlyCS8618(non-nullable field) tosuggestion, to avoid noise on XAML-bound view-model fields that the binder initializes. This is the only allowlisted softening. Do not add new#pragma warning disabledirectives or per-filedotnet_diagnostic.*.severityoverrides without discussion.
-
Files, types, members follow standard C#:
PascalCasefor types and members,camelCasefor locals and parameters,_camelCasefor private fields. -
Use descriptive whole-word names. Prefer
indexoveridx,errorovererr,requestoverreq,lengthoverlen,previousoverprev,currentovercur,temporaryovertmp,valueoverval. Well-known abbreviations (url,id,db,api,http,json,lhs/rhs,min/max) are fine. Single-letter names are acceptable only in:- numeric loop counters (
for (int i = 0; ...)), - sort comparators (
list.Sort((a, b) => ...)), - destructured domain components (
var (y, m, d) = ...), - trivial one-liner identity lambdas (
items.Select(p => p.Id)).
Anywhere else, including persistent locals and parameters, use a real word.
- numeric loop counters (
- Tests are required for all logic changes. The only carve-outs
are XAML (
*.xaml) and code-behind (*.xaml.cs) doing pure view wiring — constructors, event-handler delegation, framework overrides. This carve-out only works because §9 forbids logic in those layers; if you find yourself wanting it for actual logic, the fix is to move the logic into a view-model or service (§9 MVVM discipline), not to skip the test. No test = not done. - Tests live in
DiffViewer.Tests/, mirror the production namespace structure (ViewModels/,Services/,Utility/,Rendering/,RecentContexts/, etc.), and run viadotnet test. - Test names describe behavior, not mechanics:
LoadCommit_WhenRepoIsEmpty_ShowsEmptyState, notTestLoadCommit1. - Use FluentAssertions for assertions
(
result.Should().Be(...),act.Should().Throw<...>()). - Internal types are reachable via
[InternalsVisibleTo("DiffViewer.Tests")]inDiffViewer.csproj. Do not bump a type's visibility topublicpurely to make it testable.
- Small, focused commits with imperative subject lines (e.g.,
Fix race in MainViewModel watcher suspend). - Default to committing directly to
master. This repo's history is direct-to-master; open a pull request only when the user explicitly asks ("open a PR", "send a PR for review", etc.). PR workflow is opt-in, not the default. Section 11 rules that reference PRs (CHANGELOG[Unreleased]updates, Session block in PR descriptions, review-feedback handling) apply when a PR is open; they do not imply that a PR must be opened. - Stage files explicitly by path. Never run
git add -A,git add ., orgit add --all. This prevents committing unrelated edits, generated files (bin/,obj/,publish/), or session-state artifacts. - Never run
git rebaseorgit pull --rebase. When branches diverge, usegit pull --no-rebase(merge), or stop and ask. The rule below ("do not rewrite or force-push") already prohibits the destructive form; this rule prohibits the local form too. - Never commit secrets,
.env,bin/,obj/,publish/, or editor files beyond what.gitignorealready covers. - Do not rewrite or force-push shared branches.
- Never push a release tag on the agent's own judgment. Tagging
ships a Windows
.exeto users via the release workflow and requires an unambiguous user command. See §12 "When to release" for the cadence rules and the agent-tagging prohibition.
When the Copilot CLI runtime authors a commit or opens a PR, record the session IDs that produced the work so a future session can resume the same conversation. Two surfaces, both required when applicable:
1. Commit trailers (every agent-authored commit). Add the following
trailers at the end of the commit body. Trailers are the canonical Git
convention for end-of-commit metadata (the same mechanism
Signed-off-by uses), are parseable by git interpret-trailers, and
are greppable via git log --grep "AI-Local-Session:":
AI-Local-Session: <local-session-id>
AI-Cloud-Session: <cloud-session-id>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2. PR description Session block (PRs only, additive to the trailers). When the runtime opens a PR — or pushes commits to an open PR that does not yet have a Session block for the current session — append a Session block to the end of the PR description so reviewers see the metadata without clicking into individual commits:
---
**Session**
- AI-Local-Session: `<local-session-id>`
- AI-Cloud-Session: `<cloud-session-id>`
The key names match the commit-trailer keys so the same grep string
works on both surfaces (git log --grep "AI-Local-Session:" and a PR
text search both find the same needle). The Markdown chrome (---,
Session, bullets, backtick-monospace IDs) is purely for human
readability in the PR UI.
The PR block is additive, not a replacement for the commit
trailers; PR descriptions are more visible to reviewers, commit
trailers cover the post-merge git log view.
For the Copilot CLI runtime, both IDs come from workspace.yaml at
the root of the agent's session folder (typically
~/.copilot/session-state/<local-session-id>/):
workspace.yaml->idis the local session ID (key on both surfaces:AI-Local-Session).workspace.yaml->mc_session_idis the cloud session ID (key on both surfaces:AI-Cloud-Session).
Never invent values. If workspace.yaml is missing, unreadable, or
lacks either id or mc_session_id, stop and surface the problem
to the user (see §0) before committing or opening a PR. Do not
commit with partial or omitted session attribution: a buried "missing"
note will be skipped past by reviewers, and incomplete attribution
defeats the resume mechanism the trailers exist to support. The user
can fix workspace.yaml, manually override the rule for that commit,
or decide to skip — but the agent does not make that call silently.
Before appending the PR block, fetch the current PR description
(e.g., gh pr view <num> --json body) and check whether a Session
block for the current session's AI-Local-Session /
AI-Cloud-Session pair is already present. If yes, skip — do not
duplicate. If a Session block from a different session is present,
append a new block rather than overwriting; the history of which
sessions touched the PR is useful context.
Commit trailers do not need a dedup check — each commit is authored once, in one session.
This rule applies only to the Copilot CLI runtime. Other agent runtimes (e.g., Copilot Coding Agent) have different session-ID semantics and are out of scope until added explicitly.
- Reviewer comments — from humans and from bots
(
copilot-pull-request-reviewer[bot], dependabot, code-scanning agents, etc.) — are proposals, not orders. Evaluate each comment against the conventions in this file and prior decisions before responding. If a comment conflicts with a deliberate prior decision, push back with a reasoned reply — do not silently rewrite the code to match. - Bot comments carry no special authority. A bot's confident tone is not a reason to action a suggestion that conflicts with the conventions here.
- One reasoned pushback, then escalate. If you post a pushback reply to a bot comment and the bot re-asserts the same concern, do not loop into another reply cycle. Escalate to the user with a one-paragraph summary of the disagreement and stop.
- Resolve threads only AFTER pushing the addressing commit, and verify the fix landed. Reviewer bots do not return to verify, so leaving addressed threads open is noise that hides genuinely unresolved items. For pushback-only responses (no code change), resolve only after the user accepts the reply.
Tag-driven. Push an annotated tag matching v[0-9]+.[0-9]+.[0-9]+
(optionally with a -suffix prerelease component, e.g., v0.2.0-rc1)
to master. .github/workflows/release.yml builds, publishes a
single-file DiffViewer.exe, and creates a GitHub Release with the
matching section of CHANGELOG.md as the body. Don't fold a release
into a normal feature commit — release tags should point at the commit
that should ship.
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0The CI build workflow (.github/workflows/build.yml) runs on every
push to master and every PR (dotnet build -c Release +
dotnet test).
The release body shown on the GitHub Releases page comes from
CHANGELOG.md, not from the tag annotation. release.yml extracts the
section matching the pushed tag (e.g. ## [0.2.0] for v0.2.0 — no
v prefix in the heading, matching Keep a Changelog) and uses
it verbatim as the release body. Missing or empty sections fail the
workflow loudly — there is no silent fallback.
The tag annotation can be terse (Release v0.2.0); it is not the
source of release notes and does not need Markdown formatting.
PRs that change user-visible behavior — features, bug fixes, install
or compatibility changes — also update the ## [Unreleased] section
of CHANGELOG.md in the same PR. The release-time job becomes
"promote [Unreleased] to [<version>] with today's date", not
"reconstruct what shipped from git log".
Doc-only, build-hygiene, test-only, and pure-refactor PRs do not need
to touch CHANGELOG.md.
Each release is a user-visible Windows .exe artifact downloaded from
GitHub Releases. There is no auto-update channel and no nightly feed
— releases are the distribution. Tag deliberately, not on a fixed
cadence and not per commit.
Bump rules (pre-1.0 SemVer):
- User-facing feature added → minor bump (e.g.,
v0.1.0→v0.2.0). - Bug-fix-only batch → patch bump (e.g.,
v0.1.0→v0.1.1). - Pre-1.0 breaking changes ride in minor bumps (standard SemVer
carve-out for
0.y.z). Once the project hits1.0.0, breaking changes require a major bump.
Skip releases for doc-only commits, build hygiene, test-only
changes, and pure refactors that don't change shipped behavior. These
sit on master until they ride alongside a feature or bug-fix
release.
Group commits into coherent ships. Don't let user-facing work languish unreleased — but don't tag a release for every individual feature commit either. When a meaningful delta has accumulated, batch it into one release with notes that describe the user-visible changes (not the per-commit refactor history).
Agent rule: a Copilot CLI session must not push a release tag on its own judgment. Tagging is a shipping decision and is owned by a human. The §4 "unambiguous, scoped command" exception applies: if the user says "cut v0.2.0" or "ship a release", that is the plan and the approval. A general directive like "tidy things up", "finish the release work", or "do the obvious cleanup" is not authorization to tag. Recommending a release in conversation is fine and encouraged when a meaningful delta has accumulated; pushing the tag without an explicit command is not.
DiffViewer was extracted from the geevensingh/DevTools
personal toolkit monorepo in May 2026 using git filter-repo --path DiffViewer/ --path DiffViewer.Tests/. The full commit history before
that extraction lived in DevTools; the same commits exist here with
rewritten hashes (filter-repo always rewrites). External references to
specific DevTools commit SHAs will not resolve in this repo.