You are installing the deterministic instrumentation layer of the Arkandia Method: everything a machine can verify on its own, in milliseconds, with no ambiguity.
The point is not tidiness. Agents write code faster than a team can review it, so verification has to become mechanical. Each control you install is a sensor the agent hits by itself — before any human reads the diff.
You install eight controls, prove each one fails when it should, and record them in AGENTS.md so
the agent knows they exist.
| # | Control | Artifact | What it prevents |
|---|---|---|---|
| 1 | Reproducible inputs | global.json, Directory.Packages.props, lock files |
Two machines resolving a different SDK or a different dependency tree |
| 2 | Strict build | Directory.Build.props |
A warning reaching main |
| 3 | Style | .editorconfig |
Formatting noise in every diff |
| 4 | Entry point | Makefile |
Nobody knowing how the repo is verified |
| 5 | Shift-left | lefthook.yml |
Errors surfacing at review time |
| 6 | Secrets | gitleaks |
A credential reaching the history |
| 7 | Architecture tests | Arch-test project | The dependency rule silently breaking |
| 8 | CI | Workflow / pipeline | Local gates being skipped |
Philosophy (hold these throughout)
- Never hardcode a version. Read the SDK from the machine, the target framework from the projects, and let NuGet resolve packages. A templated version number is a bug.
- Encode what the repo already does, not what it should do. Every architecture rule you write must pass the moment you write it. A suite that goes red on install is a refactoring proposal, not a sensor.
- Discover before you write. Layout varies: tests under
tests/orsrc/Tests/, one solution or several, VSTest or Microsoft.Testing.Platform. Assume nothing. - Merge, never clobber. Existing config files carry sections you did not write.
- An exception without a comment is invisible debt. Every
NoWarn,severity = none, andNuGetAuditSuppresscarries a reason and an owner. - One definition of "the code is fine." CI invokes the Makefile; it does not restate its steps.
- A gate nobody saw fail is not a gate. Verification means breaking each control on purpose.
- Fail fast, with a clear message. A 30-second pre-commit gets uninstalled within a week.
- Everything you write is in English. Config files, code comments,
fail_textstrings, Make target descriptions, CI step names, commit-free progress output — all English, regardless of the language you are conversing in. The one exception is prose documentation that already exists: editing a SpanishAGENTS.mdin English breaks the document. Match the language of the file you are editing; write every file you create in English. - Say where you are, in words the user can act on. Announce each phase and each control as you
reach it, in English. Name the artifact and what it buys, not the internal label: not
Control 4 — entry point, butControl 4/8 — Makefile: one 'make check' that runs every gate. A run takes fifteen minutes or more; silence is indistinguishable from a hang. - Plain words, no insider vocabulary. Say "move the property to Directory.Build.props", not "hoist it"; "connect the gate", not "wire it up". The user is deciding, not reading your notes.
- Every error code gets a sentence.
CS0219,MSB4181,NU1004mean nothing to most readers. Name it and say what it is: "CS0219 — a variable is assigned but never used; the strict build turns that from a warning into an error." - Never commit. Not at the end, not between phases, not to "checkpoint" your work. The user
reviews the diff and commits when they decide to. The only
gitwrites you make are the break-and-restore of Phase 5, and those are undone before the phase ends.
Phase 1 — Discover (silent)
Do this without talking to the user. Use Glob, Grep, Read, and read-only Bash.
Work through references/inspection.md. It covers, in order:
- Confirm .NET —
*.sln,*.slnx,*.csproj,*.fsproj,global.json,Directory.Build.props. If nothing matches, stop and say so. Write nothing. - Solution and project graph — solution path, project list, layer names, the root namespace
prefix (e.g.
TheOffice.), and the project reference graph. - Target frameworks — the TFM each project uses. Divergence is a question for Phase 3, not a guess.
- Test setup — test project locations, framework and major version (xUnit v2/v3, NUnit, MSTest, TUnit) and runner (VSTest vs Microsoft.Testing.Platform). This decides how the arch-test project is built.
- Package management — whether
Directory.Packages.propsexists (central package management changes how you add references), and whether lock files are in use. Count the lock files against the project count, not merely "are any present": a solution where some projects have one and some do not restores half-pinned under--locked-modeand still goes green. Phase 3 question 4 needs that number, not a boolean. - Existing controls — which of the eight are already present, and what each one contains.
- CI —
.github/workflows/,.azdevops/,azure-pipelines.yml,Jenkinsfile,.gitlab-ci.yml. - Context docs —
AGENTS.md,CLAUDE.md,README.md,docs/. Read the architecture doc and any ADRs: they state rules a compiler cannot enforce, and they are the documents Phase 6 has to update. 8b. Documentation language — what language areAGENTS.md,README.mdanddocs/written in? Phase 6 edits those files and must not switch their language mid-document. Config files and code comments you create are always English, so this only affects the prose edits. 8c. Commit conventions — read the last ~20 subjects (git log --oneline -20). Do they follow Conventional Commits (feat:,fix(scope):)? This is the only evidence for whether thecommit-msghook belongs inlefthook.yml. Imposing a convention the team does not use is a team decision, not an instrumentation fix — if the log says no, drop the block and say why. - Architecture shape — reconstruct the
<ProjectReference>graph and classify what the repo actually is: layered, vertical slices, modular monolith, n-tier, or flat. Followreferences/architecture-discovery.md. Do not assume Clean Architecture. This drives control 7 and is the step that decides whether the arch-tests are useful or noise.
Then report the state to the user as a table — control, status (present / partial / missing),
and what you found — plus a short list of environment facts: SDK version installed, solution path,
test location, test runner, CI platform detected.
Phase 2 — Prerequisites
Check the tooling the controls depend on, and report per OS. Do not install anything yourself.
| Tool | Check | macOS | Windows | Linux |
|---|---|---|---|---|
| .NET SDK | dotnet --version |
— | — | — |
| Lefthook | lefthook version |
brew install lefthook |
winget install evilmartians.lefthook |
go install github.com/evilmartians/lefthook@latest |
make |
make --version |
ships with Xcode CLT | winget install ezwinports.make |
ships with the distro |
| gitleaks (if opted in) | gitleaks version |
brew install gitleaks |
winget install gitleaks |
apt install gitleaks on Debian trixie+ / Ubuntu 25.04+; older LTS needs the release binary |
make does not ship with Windows. If the team is on Windows and make is absent, surface the
winget command as a documented prerequisite and add it to the repo README — do not silently
switch to another task runner. make check is the convention across Arkandia material.
If a prerequisite is missing, print the install command and ask whether to continue installing the files anyway (the config is still valid; only the local hooks won't run until it is installed).
Phase 3 — Agree on scope
Ask only what Phase 1 could not answer. Use AskUserQuestion for the closed questions.
Write every question for someone who does not know the jargon. Never put an acronym in a question without spelling it out and saying what it changes for them. "Migrate to CPM?" is a bad question; "Central Package Management moves every package version out of the 10 project files into one file at the root, so two projects can't drift onto different versions of the same package — migrate?" is a good one. Same for TFM (target framework), lock files, and analyzers. State the consequence and the cost in files touched, then let them choose.
-
Which controls to install — default is all eight. Offer to skip the ones already present.
-
How tightly to pin the .NET SDK —
global.jsonfixes which SDK version builds this repo.Permissive (recommended): the lowest feature band of the installed major.minor withrollForward: latestFeature, so a teammate whose SDK is slightly older still builds.Strict: the exact installed version, for teams whose SDK comes from a devcontainer or a pinned CI image — anyone on a different one is locked out until they update. List whatdotnet --list-sdksfound when you ask. -
Central Package Management — only if
Directory.Packages.propsis absent. Today each project declares its own package versions, which is how two projects end up on different versions of the same package. This moves every version into one file at the repo root; the projects then say only which packages they use, not which version. It edits every.csprojin the solution — state the exact count ("this edits 10 project files") and report the version drift you already measured. OfferMigrate now,Skip — keep versions in each project. Recommend migrating: the drift is usually already there and the change is mechanical. -
Lock files — count them per project first, because the answer is rarely all-or-nothing:
find . -name packages.lock.json -not -path '*/obj/*' | wc -lagainst the project count.- Every project has one — the control is present. Skip the question.
- None do — ask.
- Some do. This is the
partialcase, and it is the one that matters:--locked-moderestores the projects that have a lock and resolves the rest fresh, so CI passes while half the solution is unpinned and nobody can tell from a green build. Do not ask a generic yes/no here. Show which projects have one and which do not, and offerComplete the set (generates N lock files)orRemove the ones that exist — a half-pinned solution reads as pinned. Leaving it as it is, is the one answer to argue against.
Then the question itself. Right now a restore picks whatever version the feed offers today, so a machine that restores next month can get a different dependency tree from the same commit; a lock file records the tree that was actually resolved, and CI restores in
--locked-mode, which fails when the lock and the projects disagree. The cost is real and ongoing: onepackages.lock.jsonper project, all of them committed, and every deliberate version change now needs adotnet restorein the same commit or the build goes red. OfferGenerate lock files,Skip — restore resolves fresh each time.This does not depend on question 3. Lock files work with versions in ten
.csprojfiles exactly as they do under central package management — only the file you edit to change a version differs. Recommending it alongside CPM is fine; requiring CPM first is not, and a repo that declined the migration is precisely the one where a resolved tree drifts unnoticed. -
CI platform — only if Phase 1 found none or found both.
GitHub Actions,Azure DevOps,Skip CI for now. -
Secret scanning — gitleaks blocks a commit that contains something shaped like a credential, and scans history in CI. Off by default, because the first week costs curation: sample connection strings and test GUIDs trip it, and each false positive needs an entry in
.gitleaksignore. OfferYes — pre-commit and CI,CI only,Skip. -
Pipeline scope —
CI only (quality gates)orCI + deploy. Default to CI only; how this repo deploys is a separate conversation. -
Warnings as errors — only if the repo has never built with
TreatWarningsAsErrors. Turning it on means today's warnings stop the build. OfferOn, strictorOn, with a documented list of exceptions to work through over time— the second for any repo with history, and the exception list becomes the visible record of the debt. Say how many warnings the current build produces. -
One-time reformat —
.editorconfigplusEnforceCodeStyleInBuildmakes the build enforce formatting, and existing files that never followed those rules start failing. Fixing that is a singledotnet formatpass, which rewrites source files — a real diff, not config.You cannot measure this before the file exists, so probe for it: write the
.editorconfigyou intend to install, run the check, delete it again, and only then ask. Count only genuine formatting failures, never output lines:# Findings, and the distinct files they land in. Report both; they are different numbers. dotnet format <solution> --verify-no-changes 2>&1 \ | grep -E 'error (WHITESPACE|IMPORTSSORTING|FINALNEWLINE|IDE0055)' > /tmp/fmt.txt || true wc -l < /tmp/fmt.txt # findings grep -oE '^[^(]+\.cs' /tmp/fmt.txt | sort -u | wc -l # files that would be rewrittenTwo traps here.
grep -cexits 1 when the count is zero, so a clean repo looks like a failed command — always append|| trueand read the number. And one file produces many findings, so counting findings and calling them files overstates the change by a wide margin. Tell the user the file count, since that is the size of the diff they will review.Offer
Reformat now,Start style rules at suggestion severity instead, orSkip — just report the failing files. Never reformat the repository as a side effect of installing a config file. -
Target framework — the TFM (
net10.0and friends) says which .NET each project builds for. Three cases, and only when they arise:- Projects disagree. Ask which is authoritative. Do not normalise silently — a project left behind usually has a reason.
- Centralising it. Moving
<TargetFramework>intoDirectory.Build.propsmeans deleting it from every.csproj; otherwise the projects keep overriding it. Same shape as question 3: off by default, offered with the file count. See Where the target framework lives in Phase 4. - It is already in
Directory.Build.propsbut every project overrides it. The property is inert: the file claims the framework is centralised and nothing honours it. This is thepartialcase from Phase 1, and it is worse than absent because the next reader edits the wrong file. Show the evidence — the property and the projects that override it — and offerRemove the inert propertyorFinish the migration (edits N project files). Never leave it as it is.
A control that exists but does nothing follows the same rule. Whenever Phase 1 marked a control
partial, say what is inert and offer both exits — complete it, or remove it. Silence leaves the
team believing they are covered.
Do not ask about anything already visible in the repo.
Phase 4 — Apply
Before writing anything, confirm the working tree is clean (git status). If it is not, stop
and tell the user. This is the phase the check belongs to: from here on the tree is dirty by
design, and Phase 5 can no longer tell your edits from theirs.
Ignore the agent's own footprint. A repo the skill was installed into locally almost always
carries untracked .claude/, .codex/, skills-lock.json or similar. These are tooling, not the
user's work — exclude them from the check rather than stopping on them, and say which ones you
excluded. Everything else counts:
git status --porcelain | grep -vE '^\?\? (\.claude/|\.codex/|skills-lock\.json)' | wc -l
Read the count, never the exit code. grep exits 1 when nothing matches, which is exactly the
clean case — treating that as failure inverts the check. 0 means clean; anything else means stop
and show the user the lines.
Read each template from templates/, adapt it to this repository, and write it. The templates
are skeletons, not literals: solution path, root namespace, layer names, test location and runner
all change. Every template opens with a header of instructions — read it, follow it, and delete it
before writing the file.
| Template | Becomes |
|---|---|
templates/global.json.template |
global.json |
templates/Directory.Packages.props.template |
Directory.Packages.props |
templates/Directory.Build.props.template |
Directory.Build.props |
templates/editorconfig.template |
.editorconfig |
templates/Makefile.template |
Makefile |
templates/lefthook.yml.template |
lefthook.yml |
templates/ci/github-actions.yml.template |
.github/workflows/ci.yml |
templates/ci/azure-pipelines.yml.template |
.azdevops/azure-pipelines.yml |
There is no template for the arch-test project: it is generated from the repo's own test project.
See references/arch-tests.md.
Version resolution (mandatory)
| Value | Source |
|---|---|
global.json SDK version |
See Pinning the SDK below. Not simply dotnet --version |
rollForward |
latestFeature by default. See Pinning the SDK |
TargetFramework |
The TFM the projects already use — and it stays there. See Where the target framework lives |
AnalysisLevel |
latest — it tracks the SDK |
| New packages | dotnet add package <name> without --version, then read back the resolved version and report it |
| Arch-test framework | Whatever the repo's other test projects use |
| GitHub Actions majors | gh api repos/<owner>/<action>/releases/latest --jq .tag_name, truncated to the major. Never the value written in the template |
{{GITLEAKS_VERSION}} (CI) |
gh release view --repo gitleaks/gitleaks --json tagName --jq '.tagName' | tr -d v. Written into the pipeline as a pinned version with a checksum check, not re-resolved on every run — see below |
.editorconfig indentation |
The modal indent of the existing .cs files. See references/inspection.md §4b — never the language default |
Under central package management, <PackageReference> must carry no Version; add a
<PackageVersion> to Directory.Packages.props instead.
"Resolve, never hardcode" means resolve when you write the file — not on every CI run. The two
are opposites for a downloaded binary. A pipeline that fetches releases/latest runs a different
gitleaks tomorrow than it ran today, with nothing recording the change, and a broken or tampered
release lands inside the gate meant to catch problems. So the CI templates resolve the current
gitleaks version here, pin it, and verify the download against the project's published
checksums.txt before extracting. The team bumps it like any other dependency. Say in the report
which version you pinned, so they know what to bump.
Where the target framework lives
Do not move <TargetFramework> into Directory.Build.props by default. MSBuild imports that
file before the body of the .csproj, so any project that declares its own TFM overrides it. In
a repo where every project already sets one — which is every repo created from a template — the
property is inert: the file reads as though the target framework is centralised, it is not, and the
next reader edits the wrong file.
Centralising it for real means deleting <TargetFramework> from every .csproj. That is the same
class of change as central package management, so treat it the same way: off by default, offered
explicitly with the file count, never a side effect of this run. If the user declines — the
default — say so in the report: the TFM stays in the projects.
Two cases where the question does not arise at all:
- Any project multi-targets (
<TargetFrameworks>). Leave every project alone. - Projects disagree on the TFM. That is a Phase 3 question first; centralising a disagreement just hides it.
Pinning the SDK
rollForward only moves forward, never backward. Pinning the version installed on the machine
that runs this skill locks out every teammate on a lower feature band — and the pin gets deleted
within a week, which is worse than not having one.
rollForward |
With version: 10.0.400, accepts |
|---|---|
disable |
10.0.400 only |
latestPatch |
10.0.4xx — fails on 10.0.300 |
latestFeature |
10.0.4xx and higher feature bands within 10.0 — also fails on 10.0.300 |
latestMinor |
any higher 10.x |
Default: pin the lowest feature band of the installed major.minor, with latestFeature. With
10.0.400 installed, that is "version": "10.0.100" — every 10.0.x on the team satisfies it, and the
major.minor is still locked.
Ask for the strict variant only when the team's SDK is controlled centrally (a devcontainer, a
pinned CI image, a managed fleet). Then dotnet --version exactly, with latestPatch.
If the repo already pins a version, do not lower it silently — report the mismatch and ask.
Order and rules
Install in this order — each control builds on the previous one:
-
Reproducible inputs — three artifacts, in this order:
-
global.json— if the file exists, merge: add thesdkblock, preservetest,msbuild-sdks, and anything else. Do not rewrite the file. -
Directory.Packages.props(if the user opted in) — this is a migration, not a file drop:- collect every
<PackageReference Include="X" Version="Y" />across the solution; - where the same package appears at different versions, ask which one wins — do not pick silently. Report the drift you found;
- write one
<PackageVersion Include="X" Version="Y" />per package; - edit every
.csprojto remove theVersionattribute, leaving the rest of the element (PrivateAssets,IncludeAssets) untouched; - delete every
obj/directory first, then rundotnet restoreand confirm it succeeds before continuing. Theproject.assets.jsonleft over from the pre-CPM state makes restore fail with errors that point at packages, not at the migration — the wrong trail entirely. A half-migrated solution does not build.
- collect every
-
Lock files (only if the user accepted question 4 — never as a side effect of the CPM migration, which is a separate decision they may well have answered the other way) — add
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>toDirectory.Build.propsand rundotnet restore. The generatedpackages.lock.jsonfiles belong in version control — leave them in the working tree, list them in the report, and tell the user they must be committed for CI to work. You do not commit them yourself. CI restores in locked mode, which fails when the locks and the projects disagree.If they declined, do not write the property, and say so where it matters:
make ciand both CI templates pass--locked-mode, which fails outright with no lock file present. Drop that flag from the Makefile and the pipeline in the same run, and record the pair in the report — a restore flag that outlives the artifact it depends on is the sort of breakage that surfaces first in someone else's pull request.
-
-
Directory.Build.props— repo root. If one exists, merge property groups; never drop properties you did not add. -
.editorconfig— start style rules atsuggestionseverity when the repo has debt. UnderTreatWarningsAsErrors, anything atwarningbreaks the build. -
Makefile—make checkis the single confidence signal;make ciis what the pipeline calls. Keep both in one file. -
lefthook.yml— pre-commit touches only staged files; pre-push runsmake check. -
Secret scanning (if opted in) —
gitleaks protect --stagedin pre-commit,gitleaks detectin CI. Create an empty.gitleaksignorewith a comment explaining what belongs in it. Rungitleaks detectonce against the working tree and report what it finds before wiring the gate: a repo with existing findings needs those triaged first, or the gate blocks every commit from day one. -
Arch-tests — use the shape detected in Phase 1 step 9. Follow
references/architecture-discovery.mdto derive the candidate rules andreferences/arch-tests.mdto build the project. Evaluate every candidate against the current code before writing it: rules that pass go in, rules that fail broadly become findings, not tests. Add the project to the solution withdotnet sln add. -
CI —
templates/ci/github-actions.yml.templateortemplates/ci/azure-pipelines.yml.template. The pipeline pins the SDK fromglobal.jsonand runsmake ci. It does not restate the steps.The pipeline runs on every push to every branch, not only on
main. A gate that fires after the merge tells the team about breakage once it is already shared; on a feature branch it tells the person who wrote it, while they still have the context. Two things keep that affordable on GitHub Actions, and both are in that template: a second push to a branch cancels the first, and a pull request inside the repository does not trigger the same run twice. Azure Pipelines has neither. It offers no equivalent ofcancel-in-progress, so pushes queue; if the organisation is on a single parallel job, addbatch: trueto the trigger, which waits for the running build and then queues one build covering every change since. That trades away per-commit results — a failure no longer points at a single commit — so only reach for it when the queue is a real problem. Mention the cost when you report: hosted runners bill by the minute, and Azure DevOps free tiers ship one parallel job.Writing the file is not the same as having a pipeline. GitHub Actions picks up anything in
.github/workflows/automatically; Azure DevOps does not — a YAML file at any path is inert until somebody creates a pipeline in the web UI pointing at it (Pipelines → New pipeline → Azure Repos Git → Existing Azure Pipelines YAML file), and it only becomes a gate once it is added to the branch policy. Report control 8 as written but not yet active for Azure DevOps, with those two steps spelled out as the user's to-do. The same applies to GitHub branch protection.
Never touch a .csproj for anything that can live in Directory.Build.props. The only new
project file is the arch-test one.
After each control, run its command once and confirm it passes before moving on. A broken control compounds.
After writing either .props file, confirm MSBuild can load it before doing anything else:
dotnet msbuild <any-project> -getProperty:TargetFramework
It is one second, and it converts the worst error in this whole run — MSB4181, which names no
file and no line — into a message that points at the offending line. See Troubleshooting.
Phase 5 — Verify by breaking
Mandatory. Follow references/verification.md. Installing a gate proves nothing; making it fail
does.
For each installed control: snapshot the file, introduce the violation, confirm the gate catches it, restore.
Restore from the snapshot, not with git checkout. Most of these files are files Phase 4 just
wrote: git checkout -- global.json either fails because the file is untracked, or restores the
version from before the install — silently undoing your own work. git checkout is safe only on
a pre-existing, committed source file you edited to trigger a break (controls 2, 3 and 7).
| # | Control | Violation | Expected |
|---|---|---|---|
| 1 | SDK pin | Set version one feature band above what is installed (10.0.400 → 10.0.500) |
dotnet build fails, naming the requested version, the global.json that asked for it, and the installed SDKs |
| 1b | Lock files (only if accepted in Phase 3) | Bump a version wherever this repo declares it — Directory.Packages.props under central package management, the .csproj otherwise — without restoring |
dotnet restore --locked-mode fails, naming the project whose lock no longer matches |
| 2 | Strict build | Add an unused local variable | dotnet build fails |
| 3 | Style | Reorder the using directives in a file |
make lint fails, naming the file |
| 4 | Entry point | No break needed | make help lists every target; make check chains them |
| 5 | Lefthook | Stage a badly formatted file and commit | The hook blocks the commit |
| 6 | Secrets | Stage a non-allowlisted fake credential — not AKIAIOSFODNN7EXAMPLE, which gitleaks ignores by design |
gitleaks protect --staged blocks the commit |
| 7 | Arch-tests | Add the ORM <PackageReference> to the Application project, then a using in a service |
make test fails, naming the rule |
| 8 | CI | Cannot be broken locally — verify by inspection | Installs the SDK from global.json, calls make ci, triggers on PRs |
Controls 5 and 6 are verified by attempting a real commit. If the hook does not fire, the
commit succeeds and the repo now has a commit the user never asked for. Undo it immediately with
git reset --soft HEAD~1, then fix the hook — and say what happened. Never leave a commit behind.
Restore every change. Then run make check and capture the real output.
Do not report success with a gate in the red. Fix it first.
Phase 6 — Document and report
Installing eight controls makes parts of the repo's documentation wrong. Follow
references/documentation.md: it maps which document goes stale and which section to edit.
Update what exists; do not create the doc pack. If a document is missing, report the gap and
point at /arkandia:agent-context-dotnet.
One fact, one home. Every fact belongs in exactly one document; the others link to it. Copying
the CI steps into three files means two of them are wrong within a month, and an agent reading the
stale copy acts on it. references/documentation.md carries the canonical-home table — follow it,
and when you catch yourself writing something a second time, write a link instead.
Required, in order:
AGENTS.md(orCLAUDE.md) — setup withmake hooks, a Checks to run section, the change-based check matrix, the layering rules now enforced, and the CI paragraph. A gate the agent does not know about does not get used. This step is not optional.README.md— the per-OS prerequisites table (Lefthook, andmake, which does not ship with Windows),make hooksin setup, andmake checkas the verification step.- The rest, if present —
docs/infrastructure.md(prerequisites, CI/CD),docs/dotnet.md(SDK pin, build props, quality-controls table, test organisation),docs/architecture.md(the dependency rule is now enforced),docs/claims-ledger.md(new claims).
Then report:
- tree of files created and modified, with config and documentation listed separately;
- resolved package versions — what NuGet actually picked;
- the real
make checkoutput, green; - every suppressed warning and its reason;
- architecture rules not written, and why (a rule the current code violates is a finding, not a test);
- what was deliberately left out.
Restate the migrations the team declined, if any: central package management, lock files, or
centralising the target framework. Each is pure determinism and each touches every .csproj —
which is exactly why they are offered rather than applied. Restate them with the evidence this run
produced (the version drift you measured, the projects that disagree), not as a generic pitch. If
the team accepted all three in Phase 3, this section is empty — say nothing.
Do not commit. Leave the changes for the user to review.
Troubleshooting
Symptoms whose error message does not name the real cause. Check these before investigating anything else.
| Symptom | Almost always | Confirm with |
|---|---|---|
MSB4181: The "RestoreTask" task returned false but did not log an error — no file, no line |
A -- inside an XML comment in Directory.Build.props or Directory.Packages.props. XML forbids it, so MSBuild cannot load the file |
dotnet msbuild <any-project> -getProperty:TargetFramework — it names the file and line |
A property in Directory.Build.props has no effect and the build reports nothing |
A second Directory.Build.props in a subdirectory shadows it — MSBuild stops at the first one it finds walking up |
dotnet msbuild <project> -getProperty:<TheProperty> shows the value actually in force. See references/inspection.md §6b |
| Restore fails on packages immediately after the CPM migration | Stale project.assets.json from the pre-CPM state |
Delete every obj/, restore again |
dotnet build fails on files nobody touched, right after installing .editorconfig |
The repo was never formatted to these rules; EnforceCodeStyleInBuild now enforces them |
dotnet format --verify-no-changes for the file count. This is a Phase 3 question, not a surprise |
gitleaks protect passes on your test secret |
The string is allowlisted — AKIAIOSFODNN7EXAMPLE and other published examples are ignored by design |
Retry with a fake key that is not a documentation sample |
| The arch-test break does not compile | Application has no reference to the ORM, which is exactly what the rule asserts. A using alone cannot work |
Add the package reference first, then the using |
CS0118: 'Architecture' is a namespace but is used like a type in the arch-test project |
The project's own namespace contains a segment that shadows ArchUnitNET's Architecture type — <Root>.Architecture.Tests is the usual culprit |
Qualify it: ArchUnitNET.Domain.Architecture. The same collision hits any namespace segment that matches a type you import |
| The pre-commit hook does not fire | lefthook install was never run |
ls .git/hooks — all .sample means no hooks are installed. Then git reset --soft HEAD~1 to undo the commit that went through |
References
This list is where the phase numbering lives. The reference files are named for what they contain, not for where they are called from — several are used by more than one phase.
| Reference | Used by | What it covers |
|---|---|---|
inspection.md |
Phase 1 | The discovery checklist, step by step |
architecture-discovery.md |
Phase 1, Phase 4 | Detect the real architecture, then derive rules that pass |
arch-tests.md |
Phase 4 (control 7) | Arch-test project setup and the rule catalogue |
verification.md |
Phase 5 | The break-and-revert procedure, per control |
documentation.md |
Phase 6 | Which documents the install invalidates, and their canonical homes |
templates/ |
Phase 4 | The file skeletons |
Rules
- Do NOT hardcode SDK, framework, or package versions. Resolve them.
- Do NOT overwrite an existing config file without reading and merging it.
- Do NOT modify
.csprojfiles for settings that belong inDirectory.Build.props. - Do NOT write arch-test rules the repository does not actually follow, and do NOT assume Clean Architecture — derive the shape from the reference graph.
- Do NOT report success until
make checkis green and every gate has been proven to fail. - Do NOT commit or push.
- DO leave every exception commented with a reason and an owner.
- DO update the documentation the install invalidated — starting with
AGENTS.md— and keep each fact in exactly one document, linked from the others. - DO tell the user what you skipped and why.