Agent readiness method
Method version 0.3. Payload schema version 1.
putnami agent-readiness reads a git repository and reports how far coding
agents can work in it alone. This page lists every fact the command measures,
the threshold each level needs, and a command that reproduces each fact.
The command runs on your machine and sends facts only. The server applies the thresholds on this page. A threshold change bumps the method version.
| Want to… | Do this |
|---|---|
| Run it | putnami agent-readiness |
| See what it sends, and send nothing | putnami agent-readiness --print-payload |
| Read the payload schema | payload.v1.json |
| Read the report schema | report.v1.json |
Levels
A level says what the human stops doing.
| Level | Name | The agent | The human |
|---|---|---|---|
| L1 | Assist | Suggests code and answers questions. | Writes or rewrites every change. |
| L2 | Supervised | Makes a scoped change. | Reads the whole diff and re-runs the checks. |
| L3 | Delegated | Makes and verifies a change inside one area. | Reviews the intent and the outcome, not every line. |
| L4 | Exception-based | Makes changes across areas. | Reviews only what automated checks flag. |
The level rule
Every marker climbs the same ladder.
| Level | Reading |
|---|---|
| L1 | absent: the practice is not in the repository. |
| L2 | exists: the practice is written down or configured. |
| L3 | enforced: the marker's treatment is in place: a tool, a rule or a file enforces the practice on every change. |
| L4 | proven: the treatment has held for 90 days, and 90 days of history meet the marker's threshold. |
A treatment reaches L3 at once. L4 needs 90 days of history. Each marker has one treatment, listed under Treatments. The marker reads L3 in the first run after the treatment lands, and L4 once it has held for a full 90-day window with a value that meets the threshold.
The command dates a treatment by the file that carries it, not by the line.
An old AGENTS.md that names the commands from today reads the file's age.
verify.reliable-signal has no treatment age: once nothing is left at
HEAD, it reads L4 if the last 90 days added no retry, skip or focused
test.
The command sends the first three rungs as the marker's state (absent,
exists, enforced) with a 90-day value. On an enforced marker,
enforcedDays says how long the treatment has been in place. The server
decides L4 from value, enforcedDays and the threshold in the
marker tables.
How levels add up
The report groups markers into four steps: Understand, Bound, Verify and Recover. It builds the verdict in four passes.
- Step level. In each area, a step takes the highest level that at least half of its markers reach. An area marker replaces the repository-wide marker with the same id.
- Area level. An area takes the level of its weakest step.
- Repository level. The highest level whose areas carry at least 50% of the change in the last 90 days.
- Repository step level. For each step, the weakest level among the areas that carry at least 10% of the change.
Ties go up. Markers at L4, L4, L1, L1 read L4. Markers at L4, L3, L2, L1 read L3. Markers at L4, L3, L1 read L3.
shareBlocked is the share of the last 90 days of change that lands in areas
below L3. The terminal prints it.
Two kinds of area follow part of the rule only:
- The
rootarea holds the files no area owns. It keeps its own level in the report, but it does not count in the repository level, the step levels,shareBlockedor the findings. A repository whose only changed area isrootis read fromroot. - Supporting areas are test trees (
role: tests) and documentation sites (role: docs). Their Bound step skipsbound.boundary-rulesandbound.cross-area-changes: they change together with the code they test or document. Every other marker applies.
A commit that touches two areas counts in both. When area shares sum to more than 1, the server divides each share by that sum, so every share-weighted reading stays between 0 and 1.
Markers
Each marker has an id <step>.<name> and a value. Run the evidence command
from the repository root to reproduce the fact. <area> is the area's
directory.
Understand: can an agent find what it needs to know?
| Id | Value | L2 exists | L3 enforced | L4 proven | Evidence |
|---|---|---|---|---|---|
understand.instructions |
Days the file lags behind the code it describes | An agent instruction file exists: AGENTS.md, CLAUDE.md, GEMINI.md, .github/copilot-instructions.md, .cursor/rules/ or .cursorrules |
It names a build and a test command that exist in the repository | It has named them for ≥ 90 days, and lag ≤ 90 days | git log -1 --format=%cs -- AGENTS.md |
understand.commands |
Days CI has run the command | A build or test command is declared: package.json scripts, Makefile, justfile, Taskfile, or a language default such as go test ./... |
CI runs it on pull requests | Enforced ≥ 90 days | git log --diff-filter=A --format=%cs -- .github/workflows/ |
understand.area-docs |
Days the area's README lags behind its code | The area has a README | CI checks the docs: a link check or a docs build | CI has checked the docs for ≥ 90 days, and lag ≤ 90 days | git ls-files -- '<area>/README.md' |
Bound: can it predict what a change will touch?
| Id | Value | L2 exists | L3 enforced | L4 proven | Evidence |
|---|---|---|---|---|---|
bound.declared-areas |
Share of the change inside declared areas (0–1) | A workspace manifest declares the areas: pnpm-workspace.yaml, go.work, a Cargo workspace, Maven <modules>, Nx, Turborepo, Bazel or Putnami |
A build tool orders builds by the declared graph | The build tool has ordered them for ≥ 90 days, and share ≥ 0.90 | git ls-files -- pnpm-workspace.yaml go.work turbo.json nx.json |
bound.boundary-rules |
Days the rule has been enforced | The area declares a public entry point (exports, an index module, Go internal/), or a boundary tool covers it |
CI runs a boundary tool that forbids imports across areas | Enforced ≥ 90 days | git ls-files -- <entry points> or git log --diff-filter=A --format=%cs -- <tool configuration> |
bound.cross-area-changes |
Share of the area's commits that also change another code area (0–1) | Measured: never absent once the area has commits | CI checks the areas that depend on a change (Nx affected, a Turborepo ...[<ref>] filter or --affected, Putnami --impacted), whatever the value |
CI has checked them for ≥ 90 days, and value ≤ 0.30 | git log --since=90.days --no-merges --full-diff --name-only --format=%H -- <area> |
enforcedDays counts the days since the CI job that checks dependents
appeared. Without that job, the marker reads L2 whatever the value.
Boundary tools
A boundary tool counts only when a CI job runs it.
| Tool | Configuration | CI job |
|---|---|---|
| dependency-cruiser, import-linter | .dependency-cruiser.*, .importlinter |
depcruise, dependency-cruiser, lint-imports |
eslint-plugin-boundaries, Nx @nx/enforce-module-boundaries, no-restricted-imports, import/no-internal-modules |
An ESLint configuration that names the rule | eslint or lint |
golangci-lint depguard |
A .golangci.* that enables depguard and names a package of the repository's own Go module |
golangci-lint or lint |
| ArchUnit | A pom.xml or build.gradle(.kts) that depends on ArchUnit |
A Maven, Gradle or test job |
| Putnami architecture gate | putnami.architecture.json |
putnami validate-workspace, putnami architecture or putnami validate |
Go internal/ |
A Go file under the area's own internal/ directory |
go build, go test, go vet, putnami build or putnami test |
A depguard rule that bans only third-party packages guards no boundary between areas, so it does not count.
Verify: can it prove the change works?
| Id | Value | L2 exists | L3 enforced | L4 proven | Evidence |
|---|---|---|---|---|---|
verify.tests |
Share of code commits that also change a test (0–1) | Tests exist in the area or in a test tree that mirrors it | CI runs them on pull requests | Share ≥ 0.50 | git log --since=90.days --no-merges --name-only --format=%H -- <area> |
verify.static-checks |
Days CI has run the check | A type checker, linter or formatter is configured: tsconfig, mypy, Ruff, Pylint, golangci-lint, ESLint, Biome, pre-commit, a Maven lint plugin |
CI runs it on pull requests | Enforced ≥ 90 days | git log --diff-filter=A --format=%cs -- tsconfig.json .golangci.yml |
verify.reliable-signal |
Retries, unguarded skips and focused tests added in 90 days | CI runs tests | No retry flag, unguarded skip or focused test remains at HEAD in tests, test runner configuration or CI configuration |
None added in 90 days: value = 0 | P='<pattern>' && git log --since=90.days -p -U3 --format= -G"$P" -- <counted files> | grep -E -B2 -A3 "^\+.*($P)" |
verify.pinned-toolchain |
Days CI has installed frozen | A lockfile is committed at the root or in a top-level directory, or a Maven or Gradle wrapper pins the build tool | CI installs from the lockfile frozen and pins the toolchain version. For Java: a Gradle lock or a Maven Enforcer dependency rule, and a CI job that runs Maven or Gradle | Enforced ≥ 90 days | git ls-files -- pnpm-lock.yaml go.sum Cargo.lock .nvmrc .tool-versions .mvn/wrapper gradle/wrapper |
Recover: is a wrong change contained?
| Id | Value | L2 exists | L3 enforced | L4 proven | Evidence |
|---|---|---|---|---|---|
recover.ownership |
Contributors of the area in 90 days: people who authored a commit or are credited in a Co-Authored-By trailer, and credited agents |
CODEOWNERS exists |
A rule covers the area | The CODEOWNERS file has held for ≥ 90 days, and ≥ 2 contributors. When no commit credits an agent, the count does not apply |
git ls-files -- CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS |
recover.small-changes |
Median changed lines per change | Some changes land through pull requests | At least 9 of the last 10 changes land through pull requests | That has held for 90 days, and median ≤ 80 lines | git log --since=90.days --first-parent --diff-merges=first-parent --shortstat --format=%s |
A marker reads L1 (absent) when its L2 condition fails.
recover.ownership counts agents because a solo maintainer who works with an
agent has a second contributor on every change. Without a Co-Authored-By
trailer, the command cannot tell a person working alone from a person
working with an agent under their own name. So when no commit of the last 90
days credits an agent, the report says "agent not detected" and the count
does not hold the area back. A person credited in a trailer counts as a
person. The rule reads the whole repository: one commit that credits an
agent makes every area need two contributors for L4.
Treatments
Each marker has one treatment: the change that moves it to L3. A finding's fix names it.
| Marker | Treatment (reaches L3 at once) | L4 after 90 days |
|---|---|---|
understand.instructions |
An AGENTS.md that names the build and test commands exactly as CI runs them, such as make test, npm test, pytest or go test ./... |
The file lags the code by ≤ 90 days |
understand.commands |
Declared build and test targets (Makefile, package.json scripts) that a CI workflow on pull_request runs |
CI has run them for 90 days |
understand.area-docs |
A README.md at the area's root, and a docs check in CI, such as lychee --offline . or markdownlint-cli2 "**/*.md" |
The README lags the code by ≤ 90 days |
bound.declared-areas |
A workspace manifest a build tool orders by: go.work, a Cargo [workspace], Maven <modules>, or pnpm-workspace.yaml with turbo.json or nx.json |
90% or more of the change lands in declared areas |
bound.boundary-rules |
A boundary tool that CI runs on pull requests | The rule has run for 90 days |
bound.cross-area-changes |
CI that tests the changed areas and their dependents: nx affected -t test, turbo run test --filter=...[origin/main] or putnami test --impacted |
30% or fewer of the area's changes reach another area |
verify.tests |
Tests that CI runs on pull requests, such as go test ./..., pytest or vitest run |
Half or more of the code changes change a test |
verify.static-checks |
A linter or type checker that CI runs: golangci-lint run, eslint ., ruff check or tsc --noEmit |
The check has run for 90 days |
verify.reliable-signal |
No retry flag, unguarded skip or focused test left at HEAD |
90 days add none |
verify.pinned-toolchain |
A committed lockfile, a pinned toolchain, and a frozen install in CI: pnpm install --frozen-lockfile, npm ci, uv sync --locked or cargo build --locked |
CI has installed frozen for 90 days |
recover.ownership |
A .github/CODEOWNERS rule that covers the area, such as /src/ @your-team |
Two contributors; an agent credited in a Co-Authored-By trailer counts |
recover.small-changes |
A GitHub ruleset or branch protection that requires pull requests on the default branch | The median change is 80 changed lines or less |
How the command reads a repository
It runs read-only git commands against HEAD and its history. It never runs
the repository's own commands and leaves git status unchanged.
- Areas. A workspace manifest declares them: Putnami, pnpm, npm or Yarn
workspaces, Lerna, Nx,
go.work, Cargo, uv, or Maven<modules>. In Bazel, each top-level directory with aBUILDfile is an area. Undeclared code gets inferred areas: directories with a language manifest, down to depth 3. Files outside every area form therootarea. - Roles. A path segment
test,tests,spec,__tests__ore2egives the roletests. A path segmentdoc,docs,wwworwebsite, or a doc-site configuration (Docusaurus, MkDocs, VitePress, mdBook, Antora, DocFX, Sphinx), gives the roledocs. Every other area has the rolecode. - Scope. Only authored files count. The command skips
node_modules/,vendor/,third_party/,.gen/,dist/, lockfiles, minified bundles and files marked as generated. - Tests. A test is a file named like one (
_test.go,.spec.ts,test_*.py, …) or placed under a test directory. A test tree mirrors the code areas under its parent directory. - CI. It reads GitHub Actions workflows, with the local actions and
reusable workflows they call, and
.gitlab-ci.ymlwith its local includes. - Pull requests. A change counts as a pull request when its subject has the GitHub, GitLab, Bitbucket or Azure merge or squash form, or its body names a GitLab merge request or a Gerrit review.
- Reliable signal. It searches tests, test runner configurations and CI
configurations for
rerun-fails|--retries|retries:|t\.Skip\(|\.skip\(|\.only\(|pytest\.mark\.skip|@Disabled, then counts the matching lines added in 90 days. A skip that fires only when a platform or a dependency is missing is guarded and does not count. A skip that names flakiness, CI, a failure or a to-do still counts. The state readsHEAD; the value counts what 90 days added. - Agents. A commit credits an agent when its author address or a
Co-Authored-Bytrailer names an agent tool, such asnoreply@anthropic.comor<id>+Copilot@users.noreply.github.com. Only the address decides, never the name. A commit counts as reverted when a revert names its id, or names the merge that brought it in. A revert that names no commit git knows falls back to the subject it quotes.
Findings
The report shows up to three findings, one per area. A candidate area is below the goal level and carries at least 5% of the change. The report ranks candidates by share of change times the gap to the goal. Each finding names the marker that holds the area back, the command that reproduces it, and the change that closes the gap: the marker's treatment, or, for an enforced marker, what L4 waits for. Until you set a goal, the goal is L3.
What is sent
The command sends counts, area names and repository-relative paths. It never sends:
- File contents. Evidence samples are paths, with an optional
:line. - Which agent a commit credits. The payload counts agent commits only.
- Author names or emails.
authors90dholds per-run pseudonyms: the first 16 hex digits of SHA-256(salt +:+ lowercase email). The salt is 32 random bytes drawn once per run and never sent, so nobody can test a guessed email or join two runs. - Absolute paths, remote URLs or the repository name.
repoFingerprintis the SHA-256 of the smallest root commit id.
The payload also describes what the repository builds:
| Field | Content |
|---|---|
languages |
Language name, files, lines |
frameworks, packageManagers, monorepoTools, ciProviders, iac, containers, databases, testFrameworks |
Tool name, and version when pinned |
agentTools |
Agent tools configured in the repository |
instructionFiles |
Path, size in bytes, days since last change |
files, lines |
Tracked text files and their lines |
repoAgeDays, commitsTotal, commits90d, activeContributors90d |
History counts |
agentCommits90d, agentCommitsReverted90d |
Commits of the last 90 days that credit an agent, and how many of them a later commit reverted |
Print the exact bytes before you send anything:
putnami agent-readiness --print-payloadIt prints one line of JSON and sends nothing. Author pseudonyms and
collectedAt change on every run. The payload follows the
payload schema v1;
the server refuses any field the schema does not declare.
Versions
Schema v1 only grows: a new field is optional, and the method version that
sends it declares it. Method 0.2 adds areas[].role and
markers[].enforcedDays. Method 0.3 adds inventory.agentCommits90d and
inventory.agentCommitsReverted90d, and sends enforcedDays on every
enforced marker whose treatment it can date.
The server scores each payload with the rules of its own method version. Method 0.2 differs from 0.3 in six ways:
- L4 reads the 90-day value only;
enforcedDaysweighs only onbound.cross-area-changes, where L4 needs that CI held for 90 days. recover.ownershipcounts commit authors only: an agent counts only when it authored a commit, and trailers do not count.bound.cross-area-changesis enforced only at a value ≤ 0.30 with CI that checks dependents.recover.small-changesis enforced when 90% of the window's changes landed through pull requests.verify.reliable-signalexists when the window added a retry, an unguarded skip or a focused test, not whenHEADholds one.- The inventory carries no agent counts.
Method 0.1 differs from 0.2 in five ways:
- A step reads its weakest marker, not the level half its markers reach.
rootcounts like any other area.- Supporting areas answer every Bound marker.
bound.cross-area-changesreads L3 at a value ≤ 0.30 without CI that checks dependents, and L4 once that CI exists.- The collector counts guarded skips, finds no mirrored test trees, and recognizes fewer toolchain pins and boundary tools.