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.

  1. 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.
  2. Area level. An area takes the level of its weakest step.
  3. Repository level. The highest level whose areas carry at least 50% of the change in the last 90 days.
  4. 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 root area 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, shareBlocked or the findings. A repository whose only changed area is root is read from root.
  • Supporting areas are test trees (role: tests) and documentation sites (role: docs). Their Bound step skips bound.boundary-rules and bound.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 a BUILD file is an area. Undeclared code gets inferred areas: directories with a language manifest, down to depth 3. Files outside every area form the root area.
  • Roles. A path segment test, tests, spec, __tests__ or e2e gives the role tests. A path segment doc, docs, www or website, or a doc-site configuration (Docusaurus, MkDocs, VitePress, mdBook, Antora, DocFX, Sphinx), gives the role docs. Every other area has the role code.
  • 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.yml with 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 reads HEAD; the value counts what 90 days added.
  • Agents. A commit credits an agent when its author address or a Co-Authored-By trailer names an agent tool, such as noreply@anthropic.com or <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. authors90d holds 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. repoFingerprint is 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-payload

It 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; enforcedDays weighs only on bound.cross-area-changes, where L4 needs that CI held for 90 days.
  • recover.ownership counts commit authors only: an agent counts only when it authored a commit, and trailers do not count.
  • bound.cross-area-changes is enforced only at a value ≤ 0.30 with CI that checks dependents.
  • recover.small-changes is enforced when 90% of the window's changes landed through pull requests.
  • verify.reliable-signal exists when the window added a retry, an unguarded skip or a focused test, not when HEAD holds 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.
  • root counts like any other area.
  • Supporting areas answer every Bound marker.
  • bound.cross-area-changes reads 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.

Next