A repository for autonomous agents

The harness starts with the repository.

An agent has another thousand iterations ahead. The repository has to stay readable and safe to change.

A good agent loop is not enough for that. You need rules that outlive a change of agent, context and project.

$ harness check --verbose
FAIL  ~/shop
  harness 3.9.0 · repository pins 3.9.0
✅ complexity.csharp       0
❌ docs.policy             1
    violation  NOTES.md: unexpected
    tracked Markdown
❌ dependencies.csharp     1
    violation  module dependency cycle
    Shop.Billing -> Shop.Orders
    -> Shop.Billing
✅ duplication.csharp      0
exit 1
Abridged output: a violation exits with 1. The full finding names the source lines that close the cycle, and harness explain explains the rule and the fix.

I put checks from my own practice into Harness CLI: the non-obvious things I kept explaining again in every repository. Now the same rules run after every change.

The approach has been through many iterations, and I consider the current version ready for release. Take the CLI, or carry the rules over into your own harness.

Strictness is part of the idea

The normal state: every applicable check is on and passes.

I add a check because I think it is needed. If a rule keeps being switched off, I take that as a reason to revisit the rule.

Every check already ships with the CLI. There are no plugins to pick. Rules and settings are written explicitly in .harness.json and change with the code under review.

  • required a violation blocks the run
  • advisory a violation is visible but does not block
  • off the check is switched off as a whole
  • not named in policy — outside the frame: it does not run, and the report shows one line, outside the frame

Any check can be softened or switched off. A single finding cannot be hidden with a per-file exception. A named check carries its settings in full: there are no hidden defaults.

Repository-wide weakening lives in the frame too: generated paths are declared in generated with a reason, and linter or analyzer rules switched off for the whole repository go into settings.<check>.repositoryWide with an id and a reason. An inline //nolint or # noqa blocks even with a reason. ADR-0067 →

adrs.shape keeps ADRs in the shape of a decision record: continuous numbers, a date, a status and sections; the defaults are 1000 words, 10 lines of code and 12 table rows. Why this shape →

Several projects in one Git repository

The root .harness.json registers disjoint directories: "projects": ["apps/api", "apps/web"]. Each holds a tracked .harness.json with its own answers, applicability, policy and settings. Nothing is inherited; the version and commit integration stay in the root only.

The root covers the files outside the projects. A check can be switched off or left advisory as a whole for one project; there are no exceptions for files inside a project. Shared tracked settings in parent directories are readable, but neighbouring code is not measured.

harness check checks the whole workspace; harness check --project apps/api checks the root area and the chosen project after validating every configuration, and reports an explicitly partial run. --verbose shows the scope. An unregistered nested configuration is an error.

Duplication and graphs are measured locally: repetition and dependencies across projects are not checked. Project boundaries change what the metrics cover and need review. ADR-0060.

What is on right after init

init reads the Git index and writes only the languages and stacks that have tracked sources (--languages replaces detection for CI). Every check starts as required, comments included; languages the project does not have are not in the file at all. The application/library question is asked only for C#; for a standalone library the architecture standard does not apply.

harness.coverage guards against a forgotten stack: a language with tracked sources and no applicability entry is a blocking finding with a ready JSON fragment to paste. Declining is an explicit applicable: false with a reason; the harness never edits the file itself.

init leaves the answers about tests, build and other tools empty; every frame.* is required. Filling in the answers completes the setup. Moving a check to advisory or off is something the agent discusses with the owner for that specific check, comments included.

The architecture has eight independent checks, each with an explicit policy. Every finding blocks under required; heuristic recommendations were removed. The selectors architecture and architecture.sliced-dotnet pick the whole group.

44 checks · two parts

Shared by every repository. Specific to its stack.

First, the same questions and rules regardless of language. Then checks of the sources and of how the project is built. The fullest set is for C# and .NET; Go gets comments, duplication, function length, a package DSM and linter suppressions; TypeScript and JavaScript get import cycles, duplication and a file DSM. Ansible gets linter suppressions and role cycles.

01 / COMMON

Order in any repository

Where the agent reads the rules, how it records changes and what it verifies the result with.

Documentation, ADRs, commits, configuration Short navigation, decision records, one history and explicit rules · 5 checks
    Project tests and tools Unit, integration, architecture, format, lint, build, typecheck, verify · 8 checks

    The repository says where its tools are or why they are absent. The harness checks that the answer is complete; your verify script runs the tests, build and linters in CI.

      02 / LANGUAGES & STACK

      Code structure and boundaries

      Language checks use the same frame. If a language is absent, its group is simply not in the file; if a language appears, harness.coverage says what to add.

      C# Cycles, coupling, duplication, function length, one type per file, comments · 6 checks
        .NET Build, packages, .slnx, style, warning suppressions · 5 checks
          .NET application architecture Layers and slices of sliced-dotnet/1 · 8 checks

            See the project layout ↓

            Go Comments, duplication, function length, package DSM, linter suppressions · 5 checks

            The DSM unit is the package: files of one package see each other without imports, and the compiler forbids import cycles. go vet and gofmt are not duplicated: the frame asks where they run.

              Ansible Linter suppressions and role cycles · 2 checks

              The axis is detected by ansible.cfg, roles or playbooks. YAML comments stay a separate check; the project's verify script runs ansible-lint, syntax-check and molecule.

                YAML, TypeScript and JavaScript Comments, imports, duplication and DSM · 5 checks

                The TS/JS axis reads tracked imports without tsc, finds directory cycles and builds a file DSM with transparent barrels. Unresolved imports are shown in the details; the starting ceiling for average reachable files is 8.

                  Details of any check: harness explain <check-id>. The identifiers and the version on this site are checked against the binary by a test.

                  Three ideas worth unpacking

                  Where code lives. What depends on what. How far dependencies reach.

                  01. Slices: every task has its place

                  Orders go in Orders, payments in Billing. The layer is the role of the code, the slice is the product task. The agent finds the part it needs and changes it within set boundaries.

                  This adapts ideas from Feature-Sliced Design to .NET: split by meaning, isolated slices and an explicit public API. The layers here are Clean Architecture ones, with the rule “a layer is an assembly”.

                  Part of src/Shop/: read a row as a layer, a column as a slice
                  Layer / roleOrders / orderingBilling / payment
                  Api · HTTP entryApi/Orders/Endpoints/Api/Billing/Endpoints/
                  Application · use casesApplication/Orders/Placement/Application/Billing/Invoicing/
                  Domain · business rulesDomain/Orders/Domain/Billing/
                  Infrastructure · adaptersInfrastructure/Orders/Postgres/Infrastructure/Billing/Payments/
                  Each row has its own .csproj. Host composes the application; Consumers is the entry for queues. Slices sit directly in the layer, without an intermediate Features/.

                  The usual entry into a slice

                  Api/Orders→Application/Orders/Contracts

                  Outside code uses the declared API of the use case. The internals of Placement/ can change without touching every consumer.

                  A link to a neighbouring slice

                  Application/Orders→Application/Billing/Contracts/X/Orders

                  Billing opens an API for Orders explicitly. A plain Contracts/ does not allow importing a neighbour on the same layer; it takes X/Orders, the counterpart of @x in FSD.

                  Layer directions and a few more rules
                  Allowed dependencies between layers
                  FromMay use
                  HostAll the others — it composes the application
                  Api, ConsumersApplication, Domain
                  InfrastructureApplication, Domain
                  ApplicationDomain
                  DomainNo other layer
                  • Every Application slice needs an entry in Api or Consumers. Domain slices are open to the upper layers regardless of the consuming slice's name.
                  • Directories inside a slice are named by purpose: Placement, Pricing. Helpers and Utils do not explain a purpose.
                  • The shared reserved places are Domain/Shared and Infrastructure/Persistence. Application wiring lives in Host.
                  • A layer with code has one assembly. Pulling other layers in through Compile Include is not allowed; ProjectReference follows the same table.
                  • A standalone library marks the architecture as not applicable, with a reason.

                  The sliced-dotnet/1 standard and its rationale →

                  02. A cycle: a dependency came back to where it started

                  Orders uses Billing. If Billing also uses Orders, the modules depend on each other. They become harder to change and move independently: a fix on one side can require edits on the other.

                  A cycle of two modules

                  Orders→Billing→Orders

                  Back in Orders — the ring is closed. Through three or ten modules it is still a cycle.

                  An arrow always means “uses”. The check shows the shortest ring and the source lines that close it.

                  One way to break the ring

                  Orders→Billing→Domain/Orders

                  If Billing needs only the order type, move the business concept itself into Domain. The back reference to the Orders use cases disappears: Domain does not depend on the upper layers.

                  If the modules always change together, check whether they are one task. Adding Contracts does not break a cycle by itself.

                  In dependencies.csharp a module is a namespace. Only dependencies proven by the source reader block; a guess by name never becomes a violation. How a dependency is proven →

                  03. Average reachable files: how many files a dependency chain reaches

                  There may be no cycles and still too many links. Average reachable files shows how many files, on average, you can reach from one file by following its dependencies. The starting file counts too.

                  The whole example is three files

                  A.cs→B.cs→C.cs
                  Follow the arrows from each file
                  StartReachableCount
                  AA, B, C3
                  BB, C2
                  CC1

                  Average reachable files = (3 + 2 + 1) / 3 = 2 files

                  Without links every file sees only itself: average reachable files = 1. Add C → A and each file sees all three: average reachable files = 3, largest cyclic group = 3.

                  How to read the number

                  The higher it is, the further a file's dependencies reach on average. It signals coupling; it does not promise that the next task will change exactly that many files.

                  Largest cyclic group is the size of the largest group of files where each one can reach all the others. It is the size of the whole group, not the length of one simple cycle. The chain on the left has no such group: largest cyclic group = 0.

                  By default complexity.csharp requires average reachable files ≤ 8 and largest cyclic group = 0. When a limit is exceeded, the report names the files with the most reachable dependencies or the members of the cyclic group.

                  What the calculation covers and where the limit of 8 comes from

                  It measures product files and the proven links between them. For sliced-dotnet these are the files inside the architecture zones; without zones, the files outside test projects. Generated code is excluded only with a declared path and a marker; exclusions are shown in scope:. Host counts in the number but is never a candidate for cutting links.

                  The limit of 8 comes from practical measurements and from how sliced-dotnet is built. It is the author's chosen constraint and can be revisited with more experience. Both limits are written explicitly in settings."complexity.csharp".

                  The same keys and starting limits 8 / 0 apply to complexity.typescript (files, barrels are transparent) and complexity.go (packages resolved through tracked go.mod; the compiler forbids import cycles).

                  Formula and scope · Why these limits

                  Use it yourself

                  Take the rules. Or the ready CLI.

                  The core of the approach is agreements kept in the repository and checked after every change. The implementation is open; the reasons behind decisions are recorded in ADRs.

                  ADRs, commit messages and the Russian version of this page are written in Russian; the CLI output, harness explain and the README are in English.

                  Install Harness CLI and put it to work

                  A standalone binary; it needs Git. No .NET runtime is required.

                  1. Install the CLI:
                    curl -fsSL https://raw.githubusercontent.com/gently-whitesnow/harness-cli/master/install.sh | sh
                  2. Create the configuration in the repository root:
                    harness init --kind application

                    For a standalone library: --kind library.

                  3. Fill in the answers in .harness.json, mark languages that do not apply and review the policy. Add the configuration and the project files to the Git index.
                  4. Prepare the clone and check the repository:
                    harness setup
                    harness check

                  After every change

                  Put harness check into your verify script next to tests, build and linters. Run that script in CI and hand the report back to the agent to fix.

                  Harness sees only files in the Git index. It does not run tests or builds itself.

                  0 — blocking checks passed; advisory findings and readiness gaps are still possible.
                  1 — a violation is proven.
                  2 — the check could not complete reliably.

                  A rule explained: harness explain <check-id>.
                  Raise the repository contract: harness upgrade.
                  The agent's guide in one command: harness guide.