fallow audit
Get one pass, warn, or fail verdict for the files a PR changes, from dead code, complexity, duplication, and styling checks. CLI reference for fallow audit, the quality gate for PRs and AI-generated code review.
fallow audit tells you if a PR passes your quality gate. It checks the changed files for dead code, complexity, duplication, and styling, and returns a pass, warn, or fail verdict from the severity of the issues it finds.
Audit checks only the changed files, which keeps the signal-to-noise ratio high. By default, only issues that the current PR or commit introduces fail the gate. Inherited findings in touched files show as context.
Dependency findings (unused, misplaced, type-only or test-only dependencies, overrides and catalog entries) are in the audit only when the change touches the package.json or catalog file that declares them. Unlisted dependencies follow their import sites. fallow dead-code reports all of them.
fallow audit is the gate for pull requests. For the first full-repo cleanup of an existing codebase, start with Adopt Fallow in an existing repo. That guide walks through fallow, fallow dead-code, fallow dupes, and fallow health, and hands the actual cleanup to an AI agent before you turn on audit in CI.
fallow audit
Options
Base ref
| Flag | Description |
|---|---|
--base <REF> | Git ref to compare against (for example main, HEAD~5, or a commit SHA). Alias for --changed-since. Default: the git merge-base with the branch's upstream or the remote default (origin/main). To pin the base without a flag, set FALLOW_AUDIT_BASE. |
Diff scoping
| Flag | Description |
|---|---|
--diff-file <PATH> | Path to a pre-computed unified diff (for example git diff --unified=0 main...HEAD > /tmp/pr.diff). Fallow keeps only the source-anchored findings on lines inside an added hunk. Pass - for stdin (gh pr diff | fallow audit --diff-file -) or use the --diff-stdin alias. The bundled GitHub Action and GitLab CI template compute the diff and pass it for you. You need this flag only for non-PR pipelines or local runs. Global flags documents the filtering rules and the FALLOW_DIFF_FILE fallback. |
Gate
| Flag | Description |
|---|---|
--gate new-only | Default. Compare against the base ref and fail only on findings that the current changeset introduces. JSON reports inherited findings in attribution and marks them introduced: false. |
--gate all | Strict mode. Fail on every finding in changed files, including findings inherited from the base ref. Skips the extra base-snapshot attribution pass, so the run is faster. |
You can set the gate in three places. All three accept the same values (new-only, all). Precedence: CLI flag > config > MCP param > default.
fallow audit --gate allDocs-only-diff fast path
Under --gate new-only, audit normally runs every analysis twice: once on the current tree, and once on a temporary worktree at the base ref. The second run tells audit if each finding is introduced or inherited.
audit skips the second run when every changed file is one of these:
- a non-behavioral doc (
.md,.markdown,.txt,.rst,.adoc) - a
.ts/.tsx/.js/.jsx/.mjs/.mts/.cjs/.ctssource file that is token-equivalent at the base ref (a comment-only or whitespace-only edit)
In that case, audit reuses the findings of the current run as the base snapshot and classifies all of them as inherited. Docs-only PRs and formatter sweeps then take about one analysis pass instead of two.
To check if the fast path fired, run fallow audit --base main --gate new-only --performance. The JSON output has base_snapshot_skipped: true|false, and the human output prints the same value on stderr. The fast path turns on automatically when every entry in the diff qualifies. No flag turns it on or off.
New-only duplication demotion
Under --gate new-only, clone-group attribution has one exception. When no instance of an introduced clone group overlaps an added line in the diff of the run, audit demotes the group to inherited. This happens when a change removes duplication elsewhere and so gives a remaining clone group a new structural key. The group is new to the report, but the change wrote no duplicated line, so the group does not fail the gate.
You can see every demotion. The stderr note about findings that the gate excluded gets an extra line with the demoted count and the diff that decided it:
audit gate excluded 3 inherited findings (run with --gate all to enforce)
of which 1 clone group was reclassified as pre-existing: no duplicated line was added by this change (diff source: merge-base worktree diff vs main)
With --explain, the duplication section adds one line per demoted group, with the same cap as the clone listing. Each line shows:
- the group's
dup:<fingerprint>id (matching the listing andfallow dupes --trace) - its instance locations
- the deciding rule
A closing line names the diff source. In JSON, each demoted entry in duplication.clone_groups[] has a demotion_reason field, and the attribution block counts demotions in duplication_demoted. See Key fields.
The decision uses the same diff precedence as diff scoping. A diff from --diff-file, --diff-stdin, or FALLOW_DIFF_FILE wins over the merge-base worktree diff. A supplied diff with a narrower base than the merge base can demote duplication that the branch really introduced. An example is a diff of only the last commit on a branch with several commits. Compute a supplied diff from the same base that the audit compares against. When no diff is available, audit skips this check and every introduced clone group still gates.
Output
| Flag | Description |
|---|---|
-f, --format <FORMAT> | Output format: human (default), json, sarif, compact, markdown, codeclimate, gitlab-codequality, pr-comment-github, pr-comment-gitlab, review-github, review-gitlab, github-annotations, github-summary. badge exits 2. |
-q, --quiet | Suppress progress and status output on stderr |
--explain | Add metric explanations to JSON output (_meta objects with docs links). In human output, also list each clone group that the new-only gate demoted, with the deciding diff source. See New-only duplication demotion. |
Review brief
| Flag | Description |
|---|---|
--brief | Render the deterministic review brief instead of the gating audit report. The brief answers "where do I look?", not "will CI block this?". It runs the same analysis and always exits 0. Works with every --format. fallow review implies this flag. See Review brief and decision surface. |
--max-decisions <N> | Maximum number of structural decisions in the decision surface of the brief. Default 4. Fallow clamps the value to a 3 to 5 band. Used only on the brief path. |
--walkthrough-guide | Print the walkthrough guide for an agent: the current digest (brief + decision surface), the review direction, the JSON schema that the agent must return, and a deterministic graph-snapshot hash pinned into the digest. Implies the brief. Always exits 0. See Agent walkthrough loop. |
--walkthrough-file <PATH> | Read the judgment JSON of an agent and validate it against the live graph. See Agent walkthrough loop for the accepted and rejected outcomes. Implies the brief. Always exits 0. |
--walkthrough | Render the walkthrough guide as a staged tour in the terminal (Stage 1 load-bearing, Stage 2 mechanical). With --format markdown, render text that you can paste into a PR. --format json --walkthrough prints the same agent-contract JSON as --walkthrough-guide. Implies the brief. Always exits 0. |
--mark-viewed <FILE> | Record one or more changed files as viewed in the local walkthrough ledger, then render the tour. Files that are already viewed (and still current) collapse into the Cleared panel. Repeatable. When the tree moves, the render ignores the stale marks but never deletes them. Used only on the --walkthrough path. |
--show-cleared | Expand the Cleared panel in the human and markdown walkthrough tour. The panel then lists each de-prioritized and already-viewed file, instead of a one-line summary. Used only on the --walkthrough path. |
--show-deprioritized | Expand the de-prioritized units in the weighted focus map of the brief. --format json always includes the deprioritized list. This flag only expands the human focus render, which collapses the list by default. Used only on the brief path. |
Scoping
| Flag | Description |
|---|---|
[PATH] | Report only findings in this file or directory. Fallow still builds the full project graph. |
-w, --workspace <NAME> | Scope output to one or more workspace packages. Comma-separated values, globs, and !-prefixed negation are supported. |
--production | Exclude test, story, and dev files (applies to dead-code, health, and dupes) |
--production-dead-code | Production mode for dead-code only. See global flags. |
--production-health | Production mode for health only. |
--production-dupes | Production mode for duplication only. |
--no-css | Turn off audit styling analytics completely. |
--css-deep | Force deep styling analytics on when config turns it off. Deep CSS is on by default in audit. Fallow narrows project-wide styling reachability back to changed anchors, and JSON can include complexity.css_analytics and first-class complexity.styling_findings. |
--no-css-deep | Keep local styling analytics on, but skip project-wide styling reachability. |
Thresholds
| Flag | Description |
|---|---|
--max-crap <N> | Maximum CRAP score before the audit fails (default: 30.0, configurable via health.maxCrap). A function at or above this score adds to the fail verdict, together with dead-code and complexity findings. Pair with --coverage for accurate per-function CRAP. Without coverage data, fallow estimates coverage from the module graph. |
--coverage <PATH> | Path to Istanbul coverage data (coverage-final.json) or raw V8 coverage (a NODE_V8_COVERAGE directory, or one V8 coverage JSON file) for accurate per-function CRAP scores in the health sub-analysis. You can also set it with FALLOW_COVERAGE, then health.coverage (the same chain as fallow health). Format and semantics match fallow health --coverage. Without any of these, fallow auto-detects coverage/coverage-final.json or falls back to the static-estimate model. The head pass and the base attribution pass both use the resolved map. A configured file that does not exist exits 2, as in fallow health. Relative paths resolve against --root. |
--coverage-root <PATH> | Absolute prefix to strip from file paths in coverage data before fallow prepends the project root. You can also set it with FALLOW_COVERAGE_ROOT, then health.coverageRoot. A relative health.coverageRoot exits 2, as in fallow health. Use it when CI or Docker generated the coverage under a different checkout root (for example /home/runner/work/myapp on GitHub Actions). |
Runtime coverage
fallow audit accepts these two runtime coverage flags. For the coverage formats, the capture steps and the hot-path rules, see fallow coverage.
| Flag | Description |
|---|---|
--runtime-coverage <PATH> | Merge runtime coverage into the health sub-analysis, so you do not need a second fallow health run in CI. Accepts a V8 coverage directory, a single V8 JSON file, or an Istanbul coverage map JSON file. The hot-path-touched verdict is informational and does not change the exit code. See fallow health --runtime-coverage. A single local capture is free. Continuous or multi-capture monitoring needs a license. See fallow license. |
--min-invocations-hot <N> | Threshold for hot-path classification when --runtime-coverage is active (default: 100). |
Baselines
| Flag | Description |
|---|---|
--dead-code-baseline <PATH> | Baseline file produced by fallow dead-code --save-baseline. Dead-code issues in the baseline do not count toward the verdict. |
--health-baseline <PATH> | Baseline file produced by fallow health --save-baseline. Complexity findings in the baseline do not count toward the verdict. |
--dupes-baseline <PATH> | Baseline file produced by fallow dupes --save-baseline. Clone groups in the baseline do not count toward the verdict. |
audit rejects the global --baseline / --save-baseline flags (exit 2), because audit runs three analyses with incompatible baseline formats. Use the per-analysis flags above, or set defaults in .fallowrc.json:
{
"audit": {
"deadCodeBaseline": "fallow-baselines/dead-code.json",
"healthBaseline": "fallow-baselines/health.json",
"dupesBaseline": "fallow-baselines/dupes.json"
}
}
Store committed baselines outside .fallow/. fallow init adds that directory to .gitignore for the machine-local cache.
CLI flags override config. An unset baseline has no effect. You can set any subset (for example dead-code only).
Audit cannot judge a baseline
An audit analyzes only the files that changed against its base. A whole-project baseline therefore matches less of the run. The narrow scope causes this, not a stale baseline. An audit loads the baselines that you pass, but it does not judge them.
Each loaded baseline has its own baseline_staleness object in the envelope:
dead_code.baseline_stalenessduplication.baseline_stalenesscomplexity.summary.baseline_staleness
All three report change_scoped: true. Their scope_reasons names what narrowed that sub-pass: changed-since for dead_code, and changed-files for duplication and complexity. All three report gate_trips: false. A section that loaded a baseline it cannot read as its own reports true. Do not build a gate on these values.
To judge a baseline, run the command that owns it over the whole project. The section label is not the command name: duplication belongs to fallow dupes, complexity to fallow health, and dead_code to fallow dead-code.
An audit that loaded a baseline also has gate_outcomes["stale-baseline"] set to {"status": "skipped", "enforced": false}. There is one entry for any number of loaded baselines. The entry puts this fact on the gate outcomes line, which the pull-request comment and the merge-request note render. The entry also stays skipped for a baseline of another kind. See CI for what the shipped integrations report per baseline.
An audit checks the kind of every baseline that it loads, and reports every mismatch. Each mismatch prints a warning on stderr that names the flag with the path: --dead-code-baseline, --dupes-baseline or --health-baseline. That section has baseline_staleness.unrecognised_format: true. Next to it, saved_by names the command that saved the file: dead-code, dupes, or health. The saved_by field is absent for an empty file, for a baseline saved before files named their writer, and for a writer that this version does not know. To fix it, point that flag at a baseline from the command that owns it.
audit accepts --fail-on-stale-baseline, but in audit this gate never runs. A run that sets the flag with a baseline prints one warning on stderr, which names the commands that can run the gate.
--fail-on-baseline-growth does run in audit. It does not judge the findings of the run. It compares each file of --dead-code-baseline, --dupes-baseline and --health-baseline with the same file at the base ref, so a scoped run can judge it. See Baseline growth gate.
Verdict
| Verdict | Exit code | When | What to do |
|---|---|---|---|
| pass | 0 | No introduced issues in changed files (new-only) or no issues at all (all) | Ship it. |
| warn | 0 | Issues found, all warn-severity | CI passes. Consider a fix before the issues become errors. |
| fail | 1 | Error-severity issues found | Fix the reported issues before merging. |
| error | 2 | Runtime error (invalid ref, not a git repo, config error) | Check the error message. In JSON format, audit prints {"error": true, "message": "...", "exit_code": 2}. |
Dead code issues use the severity from your rules configuration (error/warn/off). Complexity findings above the configured thresholds use the complexity rules. They default to error. With warn, a complexity finding gives a warn verdict. The thresholds come from your fallow health config (defaults: cyclomatic 20, cognitive 15). Duplication is a warning unless you configure a --threshold. With the default new-only gate, the report can show inherited error-severity findings while the audit exits 0. The audit ignores --fail-on-issues and keeps its own verdict.
The parse error gate (--fail-on-parse-error or failOnParseError) checks every file that the audit parsed, not only the changed files. The verdict does not include this gate. Read gate_outcomes for the exit reason.
Inline suppression comments (// fallow-ignore-next-line) work in audit. They suppress findings in changed files in the same way as in fallow dead-code.
JSON contract: which fields are severity-aware
Audit reports three counts that look alike but answer different questions. A CI integration or other tool that reads the output must gate on the right one:
| Field | What it counts | Severity-aware? |
|---|---|---|
verdict | Overall outcome (pass / warn / fail). Uses the per-rule severity from .fallowrc.json | Yes |
attribution.dead_code_introduced, attribution.complexity_introduced, attribution.duplication_introduced, attribution.styling_introduced | Findings that the changeset introduced under gate: new-only, at any severity | No |
summary.dead_code_issues, summary.complexity_findings, summary.duplication_clone_groups | All findings in changed files (any gate), at any severity | No |
Per-finding introduced: true | false | Whether the changeset introduced this finding | No (severity is per-rule, not per-finding) |
Rule of thumb for CI gating: branch on verdict == "fail", or check the exit code, which follows the verdict. Do not count introduced findings. A count brings back the bug that command: audit was built to fix: a project with unused-exports: warn would fail CI on every PR that adds a warn-tier finding, although the verdict correctly says warn ("do not fail"). The official GitHub Action and GitLab CI template already gate on the verdict. Third-party wrappers should follow the same contract.
Rule of thumb for AI agents: read verdict first to see if the run passed or failed. Then read attribution for the new and inherited counts, and walk the finding arrays per category for the details. To keep only the findings that the current PR introduced, filter on introduced: true.
Examples
# Auto-detect base branch
fallow audit
# Explicit base ref
fallow audit --base main
# Audit last 3 commits
fallow audit --base HEAD~3
# Strict mode: fail on inherited findings too
fallow audit --gate allExample output
Audit scope: 8 changed files vs main (d4a2f91..HEAD)
✓ No issues in 8 changed files (1.18s)
Audit scope: 8 changed files vs main (d4a2f91..HEAD)
■ dead code 0 · complexity 2 (warn, max cyclomatic: 12) · duplication 0
✓ dead code: 0 issues · complexity: 2 findings (warn) · 8 changed files (1.18s)
The ■ summary line appears only on warn verdicts, before any detail sections. --quiet hides it.
Audit scope: 12 changed files vs main (d4a2f91..HEAD)
── Dead Code ──────────────────────────────────────
● Unused exports (2)
src/components/Button.tsx (2)
:42 OldButton
:67 deprecatedHelper
Exported symbols with no known consumers
https://docs.fallow.tools/explanations/dead-code#unused-exports
── Duplication ────────────────────────────────────
● Duplicates (1 clone group)
23 lines 2 instances
src/utils/helpers.ts:10-32
src/legacy/compat.ts:5-27
── Complexity ─────────────────────────────────────
● High complexity functions (1)
src/parser.ts
:45 parseExpression
28 cyclomatic 32 cognitive 80 lines
Functions exceeding cyclomatic, cognitive, or CRAP thresholds; ! marks the
dimension that breached
https://docs.fallow.tools/explanations/health#complexity-metrics
✗ dead code: 2 issues · complexity: 1 finding · duplication: 1 clone group · 12 changed files (2.14s)
How it works
- Resolve base ref: audit uses
--baseif you set it, thenFALLOW_AUDIT_BASE. Otherwise it auto-detects thegit merge-base(fork point) with the branch's upstream (@{upstream}) or the remote default (origin/HEAD→origin/main→origin/master). Because audit uses the merge-base and not a bare branch name, a feature branch compares against the point where it forked, not against a localmainthat can be stale. Repositories with nooriginremote fall back to the localmain/masterbranch. If audit cannot find a base, it stops with an error. - Find changed files: runs
git diff --name-only <base>...HEAD(three-dot diff, showing changes since the merge base). - Run three analyses scoped to changed files: dead code, complexity, duplication.
- Compute attribution: with
new-only, audit runs the same analyses at the base ref and compares structural issue keys. A dead-code key is the line-free canonical key of the finding: the rule, the root-relative path and the names, the same input as thefinding_id. A line shift above a finding therefore keeps it inherited. The keys compare by count, so a new second finding with the key of an inherited finding is introduced. An unlisted dependency is keyed by the package, so a new import site of a package that the base already reports stays inherited. - Compute verdict:
new-onlygates only introduced findings.allgates every finding in changed files and skips the extra base-snapshot analysis.
JSON output
{
"kind": "audit",
"schema_version": 11,
"version": "3.31.0",
"command": "audit",
"verdict": "fail",
"changed_files_count": 12,
"base_ref": "main",
"head_sha": "d4a2f91",
"elapsed_ms": 2140,
"summary": {
"dead_code_issues": 2,
"dead_code_has_errors": true,
"complexity_findings": 1,
"max_cyclomatic": 28,
"duplication_clone_groups": 0
},
"attribution": {
"gate": "new-only",
"dead_code_introduced": 2,
"dead_code_inherited": 0,
"complexity_introduced": 1,
"complexity_inherited": 0,
"duplication_introduced": 0,
"duplication_inherited": 0,
"styling_introduced": 0,
"styling_inherited": 0,
"duplication_demoted": 0
},
"dead_code": {
"schema_version": 9,
"total_issues": 2,
"unused_exports": [
{ "path": "src/components/Button.tsx", "export_name": "OldButton", "introduced": true, "actions": [...] }
]
},
"complexity": {
"findings": [...]
},
"duplication": {
"clone_groups": [],
"clone_families": [],
"stats": { "duplication_percentage": 0.0 }
}
}
Key fields
| Field | Type | Description |
|---|---|---|
verdict | "pass" | "warn" | "fail" | The audit result. Use this for CI gates. |
changed_files_count | integer | Number of files changed between base and HEAD |
base_ref | string | The git ref used for comparison |
summary.dead_code_issues | integer | Total dead code issues in changed files |
summary.dead_code_has_errors | boolean | Whether any dead code issue has error severity |
summary.complexity_findings | integer | Functions above the complexity thresholds |
summary.max_cyclomatic | integer | null | Highest cyclomatic complexity found (null if none) |
summary.duplication_clone_groups | integer | Clone groups involving changed files |
attribution.*_introduced | integer | Findings whose structural key did not exist at the base ref. With gate=all, these stay 0, because audit skips the base-snapshot attribution pass. |
attribution.*_inherited | integer | Findings already present at the base ref. Also 0 under gate=all. |
attribution.duplication_demoted | integer | Clone groups that the new-only gate demoted from introduced to inherited, because no instance overlaps an added line. Always present in audit-family JSON (audit, review brief, MCP audit). Demoted groups still count in duplication_inherited, so this value never exceeds it. |
dead_code.*[].introduced | boolean | Present in audit JSON sub-results when base attribution is available. true means the finding is new compared with the base ref. |
duplication.clone_groups[].demotion_reason | string | Present only on demoted groups. A kebab-case reason for the demotion, currently no-added-lines. Fallow can add more values, so treat an unknown value as "some demotion reason". fallow dupes --format json never emits this field. |
complexity.styling_findings[] | array | Present when CSS analysis is active. Holds first-class styling issues with rule severity and actions. Each item has an optional introduced attribution when the base comparison is available. |
complexity.css_analytics | object | Descriptive CSS analytics from the CSS analysis: specificity, token usage, context for duplicate declarations, and cleanup candidates. Use it as evidence for styling_findings. Do not gate on it alone. |
The dead_code, complexity, and duplication sub-objects hold full results in the same format as fallow dead-code, fallow health, and fallow dupes. audit omits them when no files changed. By default, the complexity sub-object can also include the css_analytics and styling_findings blocks of the health report. When you pass --runtime-coverage, it can also include runtime_coverage and additive coverage_intelligence blocks.
MCP tool
The audit MCP tool wraps fallow audit --format json --quiet --explain:
{
"tool": "audit",
"arguments": {
"base": "main",
"gate": "new-only",
"css_deep": true
}
}
When you omit base, the tool resolves it as described under How it works. To pin it, set FALLOW_AUDIT_BASE in the server environment. Set gate to all for strict mode. The tool includes styling findings by default. Set css_deep to false to skip project-wide styling reachability, or to true to force it back on when config turns it off. The tool returns the same JSON envelope as the CLI.
See MCP integration for setup instructions.
Audit cache maintenance
Under --gate new-only, audit creates a temporary base snapshot (a git worktree checkout) to compare against the base ref.
- When the current checkout has
node_modules, audit links it into the snapshot. tsconfigextendschains into installed packages and path aliases then resolve as in the working tree. - audit unregisters the snapshot from git right after it creates it. The snapshot never appears in
git worktree list, IDE repo views, or.git/worktrees/, even if the process is force-killed. - The next audit deregisters entries that earlier fallow versions registered, and keeps their warm caches.
Each project root keeps one base-snapshot cache that the root owns. audit rebuilds it in place when the resolved base SHA changes. audit holds the reuse lock for the whole run, so cleanup can never remove files under a running audit.
Every audit runs an automatic age-based garbage collection (GC). The default age is 30 days. To change it, set the FALLOW_AUDIT_CACHE_MAX_AGE_DAYS env var or the audit.cacheMaxAgeDays config field. 0 turns off the age-based sweep. Two subcommands clean the caches on demand.
audit-cache remove
To remove all caches of one project, warm or not, run fallow audit-cache remove --root <PATH>:
fallow audit-cache remove --root ./my-project --dry-run # preview what would be removed
fallow audit-cache remove --root ./my-project --yes # remove (required in non-interactive shells)
--dry-run reports what the command would remove and does not touch the filesystem. Removal in a non-interactive shell requires --yes. The command leaves lock-contended entries (an audit is in the middle of a rebuild) in place, reports them, and exits 2.
audit-cache prune
Use fallow audit-cache prune when audit caches from deleted checkouts or old branches take disk space. It cleans up now, with a visible decision per entry, and you do not wait for the silent sweep of the next audit.
prune applies the same GC policy that every fallow audit run applies silently:
- cleanup of orphaned sidecars
- age-based reclaim under the resolved threshold
- reclaim of abandoned entries whose recorded owner root no longer exists
It reports every cache entry that it considered, with sizes and ages. It never touches entries that other live projects own.
fallow audit-cache prune --dry-run # preview every decision without touching the filesystem
fallow audit-cache prune # apply the GC policy now
fallow audit-cache prune --max-age-days 7 # tighter age threshold for this run
| Flag | Description |
|---|---|
--dry-run | Preview decisions without touching the filesystem. The command removes nothing and creates no lock or age-tracking sidecars. |
--max-age-days <N> | Age threshold in days for this run. Precedence: this flag, then FALLOW_AUDIT_CACHE_MAX_AGE_DAYS, then audit.cacheMaxAgeDays, then the 30-day default. The JSON output reports which source won. 0 turns off age-based reclaim, but the command still reclaims orphaned entries and entries whose recorded owner root is gone. |
--root is optional for prune and defaults to the current directory. The command measures sizes with a full recursive walk of each cache entry, which can take a few seconds on large caches.
Human output prints one row per considered entry, sorted by path, with the decision, reason, size, and age. An entry that stays because another live project owns it gets a hint: run audit-cache remove to clear the cache of that project. The closing line gives the total of reclaimed bytes.
prune exits 0 whenever it ran, unlike remove. This includes runs that skip entries because of lock contention (an audit is in the middle of a rebuild) and runs with per-entry failures. On a shared runner, the command reports entries of other users as kept and does not fail. Exit code 2 is only for validation errors and an unreadable scan root. Scripts gate on the complete field of the JSON envelope, not on the exit code.
With --format json, prune returns its own maintenance envelope. It is separate from the audit envelope, and sits next to the audit-cache-remove envelope:
{
"kind": "audit-cache-prune",
"schema_version": 1,
"command": "audit-cache prune",
"root": "/work/my-project",
"scan_root": "/tmp",
"dry_run": false,
"max_age_days": 30,
"max_age_source": "default",
"entries": [
{
"path": "/tmp/fallow-audit-base-cache-1a2b3c4d",
"pass": "foreign",
"disposition": "removed",
"reason": "aged-out",
"age_days": 41,
"size_bytes": 187563008,
"owner_root": "/work/deleted-checkout"
}
],
"found": 1,
"removed": 1,
"kept": 0,
"skipped": 0,
"failed": 0,
"complete": true,
"reclaimed_bytes": 187563008
}
| Field | Type | Description |
|---|---|---|
dry_run | boolean | true when the run only previewed decisions. |
max_age_days, max_age_source | integer, string | The resolved age threshold and which precedence level supplied it (flag, env, config, or default) |
entries[] | array | One item per considered cache entry: path, pass (owned, legacy, or foreign), disposition (removed, kept, skipped, or failed), a short kebab-case reason (for example aged-out, fresh, owner-live), age_days, size_bytes, and owner_root (each of the last three is null when not measurable). |
found, removed, kept, skipped, failed | integer | Aggregate counts. removed + kept + skipped + failed always equals found |
complete | boolean | true when nothing was skipped or failed. Gate on this field in scripts. The exit code stays 0 either way. |
reclaimed_bytes | integer | Bytes that this run removed from disk. On a dry run, the bytes that the run would remove. |
A normal audit run does its GC silently. To see the per-entry decisions, set RUST_LOG=fallow=debug. fallow audit and audit-cache prune then log one diagnostic line per considered entry, with its decision, reason, age, and threshold. With RUST_LOG unset, the audit output does not change.
Review brief and decision surface
fallow review shows a reviewer where to look in a change. It is an alias for fallow audit --brief. It produces a deterministic review brief from the module graph and always exits 0. The output includes the verdict for information only. fallow audit is the command that gates (exits non-zero on a fail verdict). --format is independent of --brief, so the brief renders in human or json like any other audit output.
The brief answers "where do I look?", not "will CI block this?". It runs the same analysis as the gating audit and organizes the result around four jobs:
| Job | What it does |
|---|---|
| subtract | Fallow reports dead code, complexity, and duplication for the changed files, but keeps them out of the judgment loop. The reviewer does not have to judge mechanical findings. |
| focus | Fallow ranks the changed-file units by a composite attention score and labels each one review-here or not-prioritized. It also gives a full deprioritized list. To expand that list in the human output, use --show-deprioritized. |
| structure | The decision surface: the few structural decisions in the change that have large consequences. |
| direct | The walkthrough guide for an agent (see Agent walkthrough loop). |
Decision surface
The decision surface is the ranked set of coupling-boundary, public-api-contract, and dependency decisions in the change, capped by --max-decisions. Each decision is a framed judgment question anchored to a signal_id. fallow decision-surface prints the decision surface alone. That page documents the categories, the fields of each decision, and suppression.
JSON envelope
fallow review --format json returns decisions, focus, deltas, impact_closure, partition, and graph_facts. When the project has a CODEOWNERS file, it also returns ownership. The brief schema_version is 11 since the ownership section. A consumer can read it to detect the section.
deltas also has the dependency keys that the decision surface reads: dependency_added (<manifest>::<name>) and dependency_major_bumped (<manifest>::<name>@<from>-><to>). The envelope names what changed even when the cap collapses the decision. Both keys are always present, as empty arrays when nothing changed.
impact_closure reports the blast radius as a magnitude, a sample, and a distribution instead of one long list:
affected_countis the exact number of files outside the diff that the change reaches, computed before any cap.affected_not_shownis a path-sorted sample of at most ten of those files. It is a prefix of the sorted set, so it clusters in the directory that sorts first. Use it to open representative files. Do not use it to list the full reach or to infer its shape.affected_by_dirgives the shape as{ dir, count }rows: heaviest directory first, the directory path as tie-breaker, at most twenty-five rows.affected_by_dir_omittedcounts the lighter directories that did not fit. The count in every row is exact.coordination_gapis separate and has no cap. It is not a subset of the sample. It skips, on purpose, the story and test consumers that the affected set counts.
partition has independent_slices only when there are two or more slices. A slice is a connected component of the dependency graph between units, given as a sorted list of module directories. Two or more slices mean that the change splits along a seam that the graph proves, into pieces that you can review and merge separately. A single slice would only repeat order, so fallow leaves the field out. The field helps you orient. It never asks you to split the change.
ownership shows how many CODEOWNERS owner groups the change reaches:
group_countcounts the owner groups of the changed files and of the impact closure.(unowned)counts as one group.transitive_only_countcounts the groups that own no changed file and that the change reaches only through the impact closure.unowned_direct_countcounts the changed files that no CODEOWNERS rule owns.groupsis a capped list of{ owner, direct_count, affected_count }rows, sorted bydirect_count(highest first), thenaffected_count(highest first), thenowner.groups_omittedcounts the groups that did not fit.slicesis present only whenpartition.independent_slicesis present, with the same order. Each entry is{ module_dirs, owners, separable }.separableis true when the slice has one owner.
Each file maps to its primary owner: the last matching rule, and the first owner of that rule. The section reads only the CODEOWNERS file (the configured codeowners path, else the standard locations), so it does not need git history. routing reads the same configured path. Without a readable CODEOWNERS file, the ownership key is absent. A configured path that fallow cannot read prints a warning. The human output lists the owner groups, and names each slice that has one owner. Fallow does not split the change: the reviewer decides.
Agent walkthrough loop
The walkthrough guide turns the brief into a strict contract with a coding agent. The graph checks the agent, not a second model. Fallow validates every judgment that the agent returns against the live graph. Fallow fences the free-text framing of the agent as non-deterministic, and that framing never gates.
1. Fetch the guide.
fallow review --base main --walkthrough-guide --format json
The response has digest (brief + decision surface), direction, graph_snapshot_hash, agent_schema, injection_note, emitted_signal_ids, and change_anchors. Fallow builds the digest from the graph only and never adds PR prose, so the digest resists injection. The snapshot hash is pinned into the digest.
Each direction.units[] entry has test_adjacency:
none: no test file imports the changed unit directly.untouched: a test imports the unit, and none of those tests is in the diff.changed: at least one importing test changed together with the unit.
The field is absent when the graph was not retained or the unit is itself a test file. It is a graph fact about direct importers, not a coverage claim. Mocks, fixtures and snapshots help tests but are not tests, so an importer under __mocks__/, fixtures/, __fixtures__/ or __snapshots__/, or a *.fixture.* file, does not count. An importer inside a test directory, such as test/fixtures/, still counts.
agent_schema publishes two lists:
action_vocabulary(block,address,consider,fyi): the closed set of values for theactionof a judgment.- a recommended
concern_vocabularywith the thirteen trade-off lenses:abstraction,coupling,data-model,error-handling,control-flow,performance,dependencies,api-ergonomics,compatibility,state-ownership,extensibility,testability,trust-boundary.
concern stays free text in the JSON. A value from the list lets a review tool group judgments by lens.
change_anchors has one stable chg: id per changed region (hunk), derived from the same diff source that the run used. The id is content-addressed (file path + normalized added text, without line numbers). It survives an edit above the hunk and a whitespace-only change, and it survives a rename through previous_change_anchor. With a change anchor, an agent can anchor a judgment about a changed REGION that has no graph finding (a trade-off with no signal_id). Fallow can then validate that broader trade-off, so the agent cannot invent it.
2. The agent returns judgment JSON. Each judgment cites a signal_id OR a change_anchor that fallow emitted. The agent echoes the hash.
{
"graph_snapshot_hash": "<echoed>",
"judgments": [
{
"signal_id": "<one fallow emitted>",
"change_anchor": "<or one fallow emitted chg: id, for a changed region with no finding>",
"framing": "<reasoning>",
"concern": "<optional>",
"action": "<optional: block | address | consider | fyi>"
}
]
}
action tells the author of the change what to do with the judgment. block and address are required actions, consider is optional, and fyi needs nothing. It is the instruction of the reviewer. Fallow fences it together with the framing, and it never gates.
3. Post-validate the judgment against the live graph.
fallow review --base main --walkthrough-file judgment.json --format json
Fallow sorts each judgment into one of these outcomes:
| Outcome | Condition | Shape |
|---|---|---|
accepted (signal) | The signal_id is anchored and the snapshot still matches. | Framing fenced deterministic: false; anchor_kind: "signal" (the strong anchor, a graph finding). |
accepted (change) | No signal_id, but the change_anchor is one fallow emitted and the snapshot still matches. | Framing fenced deterministic: false; anchor_kind: "change" (the weaker anchor, a changed region only). |
rejected (unanchored) | The signal_id is not one fallow emitted (a hallucinated id). | reason: "unanchored-signal-id". |
rejected (unknown change anchor) | The change_anchor is not one fallow emitted (a hallucinated region). | reason: "unknown-change-anchor". |
rejected (invalid action) | The anchor resolved, but the action is outside the closed vocabulary. When a judgment has a hallucinated anchor and a bad label, fallow rejects it for the anchor, never as invalid-action. | reason: "invalid-action", invalid_value: "<the offending label>" so the agent can fix it in one round trip. |
rejected (stale) | The echoed graph-snapshot hash no longer matches (the tree moved). | reason: "stale-snapshot", stale: true. Re-fetch the guide. |
An accepted judgment echoes action next to agent_framing when the agent set one.