fallow decision-surface

Surface only the consequential structural decisions a change embeds. Each one is framed as a judgment question, anchored to a deterministic graph-derived signal_id, paired with the routed expert to ask, and carries the named trade-off it makes. decision-surface is the apex of the review brief, emitted on its own.

fallow decision-surface --base main
fallow decision-surface --base main --format json --quiet
fallow decision-surface --base main --max-decisions 5

fallow decision-surface always exits 0. It is advisory, never a gate: the verdict is carried informationally, unlike fallow audit, which exits non-zero on a fail verdict. It answers "which one or two things here deserve a human judgment call?", not "will CI block this?".

When to reach for it

Three commands look at changed code; they answer different questions. Pick by the question you have.

You want to knowCommandGates?
Will CI block this PR?fallow auditYes (exits non-zero on a fail verdict)
Where do I look, across the whole change?fallow review (the full brief: subtract, focus, structure, direct)No (always exits 0)
Which few decisions deserve a human judgment call?fallow decision-surface (just the structure job)No (always exits 0)

fallow decision-surface runs the same changed-code analysis as fallow review but emits only the decisions and discards the rest of the brief (the subtract list, the weighted focus map, the walkthrough contract). Reach for it when you want just the handful of structural choices the diff bakes in.

What a decision is

A change can touch hundreds of lines and still embed only one or two choices that are expensive to walk back. The decision surface isolates those. Every entry is a framed judgment question with the context an agent or reviewer needs to answer it, not a recommendation. fallow never tells you which way to choose.

The surface has exactly three shippable categories:

CategoryMeaning
coupling-boundaryA new cross-zone dependency edge.
public-api-contractA new exported public-API surface, or a changed contract consumed by modules outside this diff.
dependencyA new third-party dependency, or a declared one moved across a major version (or a 0.x minor). One batched decision per manifest per kind, weighted by the graph's in-repo importers of the affected packages.

Minor and patch bumps and non-numeric ranges (workspace, file, git, tags) are never dependency candidates. The importer count is a union over the batched packages (a module importing two bumped packages counts once). Entries outside dependencies are tagged in the question (dev, optional, peer); a package listed in several sections keeps its dependencies range and anchor line.

The set is ranked by consequence (blast radius x reversibility) and capped to a small, working-memory-sized list: 3 to 5 decisions, default 4, via --max-decisions, with no pagination. The cap is the point: a reviewer can hold a handful of structural questions in their head, not a flat wall of findings.

What each decision carries

FieldTypeWhat it is
questionstringThe judgment framed as a question, the thing a human should weigh in on.
category"coupling-boundary" | "public-api-contract" | "dependency"The kind of structural choice.
routed_expertstringWho to ask. The decision points at the person or role whose judgment the category calls for, so the question reaches the right reviewer.
signal_idstringThe deterministic, graph-derived anchor for this decision. Stable across runs against the same tree, and the join key for re-attaching review comments.
tradeoffstringThe named structural sacrifice, stated as a fact (never a recommendation). For example: "Couples app to infra; 4 in-repo modules already depend on this anchor".
internal_consumer_countintegerThe honest count of in-repo modules outside the diff that already depend on the anchor. This is your reversibility signal: the higher the count, the more expensive it is to walk this back. It is the display number, distinct from the ranking-only blast figure.
previous_signal_idstring | nullPresent when the anchor file was renamed: the signal_id it had before, so a review surface can re-attach a prior comment across a git mv.

The human view shows the question and the trade-off; you read reversibility from the count itself. fallow never labels a decision a one-way or two-way door.

coupling-boundary and public-api-contract decisions are suppressible with an inline // fallow-ignore comment at the decision's anchor, the same way findings are suppressed elsewhere in fallow. A dependency decision anchors on package.json, which cannot carry a comment, so it has no suppress action; ask-expert still applies. Suppression removes a decision from the output; it does not change the verdict.

How an agent consumes it

The decision surface is built for agents.

fallow decision-surface --base main --format json --quiet

A typical agent flow:

  1. Read decisions[] in order.
  2. For each decision, surface the question and the routed_expert to the human, not the raw diff.
  3. Use internal_consumer_count to decide how loudly to flag it: 0 is a cheap, reversible choice; a high count is a load-bearing anchor that is expensive to undo.
  4. Quote the tradeoff clause verbatim. It is a fact, so an agent can relay it without editorializing.
  5. Anchor any comment to signal_id. If the run reports a previous_signal_id, re-attach a prior comment across the rename instead of posting a duplicate.

The digest the decision surface is built from comes from the graph only; PR prose is never folded into it. An agent can trust that the surfaced decisions reflect the actual structural change, not a description of it.

Picking the comparison point

decision-surface scopes to changed code exactly like fallow audit. Use --base (or its --changed-since alias) to pick what "changed" means.

# Against the merge-base with the default branch (auto-detected)
fallow decision-surface

# Explicit base ref
fallow decision-surface --base main

# Decisions embedded in the last 5 commits
fallow decision-surface --base HEAD~5

Options

FlagDescription
--base <REF>Git ref to compare against (e.g. main, HEAD~5, a commit SHA). Alias for --changed-since. When omitted, the base is the git merge-base against the branch's upstream or the remote default (origin/main); set FALLOW_AUDIT_BASE to pin it without a flag.
--changed-since <REF>Alias for --base. Scope the comparison to changes since this git ref.
--max-decisions <N>Cap on the number of consequential structural decisions surfaced (the working-memory limit). Default 4; clamped to a 3 to 5 band. Values outside the band are clamped, not rejected.
-f, --format <FORMAT>Output format: human (default) or json.
-r, --root <PATH>Project root directory.
-c, --config <PATH>Path to a fallow config file.
-w, --workspace <NAME>Scope to a single workspace package.
-q, --quietSuppress progress and status output on stderr.

See global flags for the full list.

Examples

# Auto-detect base; render the decisions for a human
fallow decision-surface

# Explicit base ref
fallow decision-surface --base main

# Raise the cap
fallow decision-surface --base main --max-decisions 5

Example output

Decision surface: 3 decisions vs main (d4a2f91..HEAD)

 coupling-boundary  (ask: architecture owner)
  src/app/checkout.ts src/infra/queue.ts
  Should the app zone depend directly on infra here?
  trade-off: Couples app to infra; 4 in-repo modules already depend on this anchor.
  internal consumers: 4

 public-api-contract  (ask: API owner)
  src/lib/client.ts:createClient
  This export is now public surface consumed outside the diff. Is the contract right?
  trade-off: Locks the createClient signature for external callers.
  internal consumers: 2

 dependency  (ask: platform owner)
  package.json fast-xml-parser
  A new third-party dependency enters the graph. Is it warranted?
  trade-off: Adds a third-party maintenance and supply-chain surface.
  internal consumers: 0

JSON output

fallow decision-surface --format json carries the decisions[] array; each entry carries the fields listed under What each decision carries. This is the same decision surface fallow review --format json carries under its decisions key.

{
  "command": "decision-surface",
  "base_ref": "main",
  "head_sha": "d4a2f91",
  "decisions": [
    {
      "signal_id": "coupling:app/checkout->infra/queue",
      "category": "coupling-boundary",
      "question": "Should the app zone depend directly on infra here?",
      "routed_expert": "architecture owner",
      "tradeoff": "Couples app to infra; 4 in-repo modules already depend on this anchor",
      "internal_consumer_count": 4
    },
    {
      "signal_id": "public-api:lib/client:createClient",
      "category": "public-api-contract",
      "question": "This export is now public surface consumed outside the diff. Is the contract right?",
      "routed_expert": "API owner",
      "tradeoff": "Locks the createClient signature for external callers",
      "internal_consumer_count": 2,
      "previous_signal_id": "public-api:lib/old-client:createClient"
    }
  ]
}

MCP tool

The decision surface is also exposed as the decision_surface MCP tool, so an agent gets the same decisions through structured tool calling:

{
  "tool": "decision_surface",
  "arguments": {
    "base": "main",
    "max_decisions": 4
  }
}

base and max_decisions map to the CLI flags of the same name. Dependency decisions are included; both routes share the manifest diff. See MCP integration for the full tool list and setup.

See also