Skip to content

bug(verify-preview): distinguish unreadable diff from trigger no_match #308

Closed

Description

@pengfei-threemoonslab

Summary

When a committed-ref diff cannot be read, agents-shipgate verify --preview still evaluates the Trigger against empty inputs and reports skip_reason: "no_match".

"No rule matched" is not a valid explanation when the verifier never obtained a readable diff.

Reproduction

A shallow/blobless local PR workflow exposed two input failures:

  1. the base and head had no reachable merge base;
  2. after history was deepened, the partial clone still lacked source blobs needed for the textual diff.

In both shapes, Preview reported fields equivalent to:

{
  "changed_files": [],
  "diff_text_available": false,
  "trigger": {
    "should_run": false,
    "skip": true,
    "skip_reason": "no_match",
    "rationale": "No rules matched; nothing in this PR signals a tool-surface change."
  }
}

The underlying Git errors were respectively:

fatal: <base>...<head>: no merge base
fatal: could not fetch <blob-sha> from promisor remote

The latter occurs because verification intentionally sets GIT_NO_LAZY_FETCH=1, which is the right static/no-implicit-network boundary.

Why this matters

The top-level control result can remain fail-closed (merge_verdict: "unknown", can_merge_without_human: false), but the Trigger explanation is misleading. A user or automation may incorrectly interpret no_match as evidence that the PR is unrelated to agent capabilities.

Expected result

When diff acquisition fails or is incomplete:

  • do not emit skip_reason: "no_match";
  • mark trigger evaluation as not evaluated/incomplete;
  • expose a machine-readable diff-input reason, for example:
    • refs_missing
    • merge_base_missing
    • objects_missing
    • git_failed;
  • preserve the relevant Git failure safely in diagnostics;
  • return the precise next action, such as fetching/deepening history or hydrating required partial-clone blobs.

Illustrative shape:

{
  "diff_status": "objects_missing",
  "trigger": {
    "evaluation_status": "not_evaluated",
    "should_run": null,
    "skip_reason": null
  },
  "merge_verdict": "unknown",
  "can_merge_without_human": false
}

Regression coverage

Please add committed-ref Preview tests for:

  1. no merge base in a shallow clone;
  2. promisor/partial-clone blob unavailable with lazy fetching disabled;
  3. a real agent-related diff that must not be classified as no_match merely because its input is unreadable.

Activity

  1. pengfei-threemoonslab commented on Aug 5, 2026

    @pengfei-threemoonslab
    ContributorAuthor

    Re-verified at 527a0349 (v0.16.0b7). Confirmed, and the blast radius is larger than filed in two ways.

    1. On an unconfigured workspace, the diff failure is never reported at all.

    Both diff_unavailable branches in run_preview are gated on manifest_present (cli/verify/orchestrator.py:3149, :3156):

    if diff_unavailable and manifest_present and diff_failure_requires_review:
        ...
    elif diff_unavailable and manifest_present:
        ...
    elif manifest_present:
        ...
    elif trigger.get("should_run") or trigger.get("dry_run_recommended"):
        ...

    With no shipgate.yaml, an unreadable diff falls past both, trigger.should_run is False (because the trigger just evaluated empty inputs), and control reaches the final else:

    "Shipgate is not configured in this workspace." → next_action: initialize

    The Git failure is in base_notes but nothing in the headline, the verdict, or the next action mentions it. merge_verdict: "unknown" and can_merge_without_human: false still hold, so the control result is fail-closed — but the route is wrong, and it is wrong precisely in the cold-start case that this class of failure comes from. Shallow and blobless clones of unadopted repositories are the normal shape for first contact.

    2. Draft PR #305 replicates the defect into two new code paths.

    #305 ("Fail closed when verify cannot resolve a comparison base") adds fetch_required / selection_required states to run_verify, and at that branch does:

    trigger = evaluate(
        paths=[],
        diff_text="",
        manifest_present=True,
        user_requested=True,
    )

    which emits skip_reason: "no_match" with the rationale "nothing in this PR signals a tool-surface change" inside a verifier artifact that has just declared it could not bind a trustworthy comparison base. The PR touches run_verify only; run_preview is unchanged, so the originally filed path survives as well.

    The two are otherwise complementary — #305 covers repro case 1 (no merge base) for verify and usefully distinguishes shallow from unrelated histories, which this issue does not ask for. Suggest sequencing this issue first, or folding the trigger-side fix into #305, so the new fail-closed states do not ship with a contradictory trigger block.

    3. Root cause is in the trigger evaluator, not only its callers.

    triggers.py:452-458 has no representation for "inputs were not obtained" — the else arm is the only landing place, and it hardcodes no_match. Every caller that evaluates against empty inputs will reproduce this. Fixing it at the three call sites will not hold; the evaluator needs the evaluation_status the issue proposes, and adding a state to the catalog's reported output is a schema_version question worth settling as part of the fix.

  2. added
    bugSomething isn't working
    P1Picked up inside the current milestone window; keep about 15 open
    area:verifyverify / preview orchestration and SHIP-VERIFY-* checks
    on Aug 5, 2026
  3. added this to the v0.17.0 milestone on Aug 5, 2026
  4. pengfei-threemoonslab commented on Aug 6, 2026

    @pengfei-threemoonslab
    ContributorAuthor

    Additional reproduction shape from a real supported-framework PR:

    While evaluating google/adk-samples#1745 with Agents Shipgate 0.16.0b7 / contract 19, verify --preview --base base-pr-1745 --head HEAD --json returned:

    {
      "changed_files": [],
      "diff_text_available": false,
      "trigger": {
        "skip": true,
        "skip_reason": "no_match",
        "rationale": "No rules matched; nothing in this PR signals a tool-surface change."
      },
      "base_notes": [
        "Preview diff unavailable: Git diff exceeded static output bounds or could not be read."
      ]
    }

    This PR adds a Google ADK LlmAgent graph and multiple FunctionTool bindings, but also includes large PNG/GIF demo assets. The current implementation collects --name-status and the full diff body separately, then raises one generic error and discards both results if either bounded read returns None. The output therefore cannot distinguish a metadata failure from a body-limit failure, and a body-heavy PR can lose otherwise usable changed-path evidence.

    Suggested extension to this issue's acceptance criteria:

    • Preserve successfully collected --name-status paths when only the diff body exceeds _DIFF_BODY_LIMIT.
    • Collect textual diff bodies only for capability-relevant paths, excluding ordinary image/video assets.
    • Expose a machine-readable diff_status that distinguishes metadata failure, body truncation, missing objects, and other Git errors.
    • Never emit Trigger no_match when diff acquisition is incomplete.
    • Add a binary-heavy Google ADK PR fixture where agent.py is still recognized and routed even though unrelated demo assets make the aggregate PR large.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Picked up inside the current milestone window; keep about 15 openarea:triggerTrigger catalog and relevance routingarea:verifyverify / preview orchestration and SHIP-VERIFY-* checksbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions