fix(verify): distinguish an unreadable diff from trigger no_match - #340
Conversation
`verify --preview` collapsed every diff-acquisition failure into one message, then evaluated the trigger catalog against the empty inputs that failure left behind — publishing `skip_reason: "no_match"` with the rationale "nothing in this PR signals a tool-surface change" about a PR it had never read. The control result stayed fail-closed, but the explanation invited exactly the wrong conclusion. On a workspace with no manifest the failure was not reported at all: both diff-failure branches were gated on `manifest_present`, so a shallow or blobless clone of an un-adopted repository — the normal shape of first contact — fell through to "Shipgate is not configured in this workspace", with the Git error visible nowhere but `base_notes`. Three changes, at the three layers the defect actually spans. Diff acquisition is classified rather than flattened. `not_attempted`, `refs_missing`, `merge_base_missing`, `objects_missing`, `metadata_limit_exceeded`, `body_limit_exceeded`, `git_timeout`, and `git_failed` are read off Git's own diagnostic (stderr is now piped and drained under a small cap instead of discarded) and travel on a new `verifier.json` `diff_status` block with a bounded, path-redacted excerpt, the precise repair, and whether fetching can perform it. Metadata and body are collected separately, so a body that cannot be read no longer discards the changed paths that were read successfully — a blobless clone answers `--name-status` in full, and those paths are exactly what says a PR touches an agent surface. The trigger evaluator gained the state it was missing. `input_status` and `evaluation_status`, with `should_run`, `run_shipgate`, `skip`, and `skip_reason` all `null` when the inputs were not fully read. The asymmetry is deliberate: rule matching is monotone in the evidence, so a run verdict reached from partial evidence stays sound and is still published, while any skip verdict is withheld. The stop block, which reasons over the very path evidence that is missing, is reported as not evaluable. And an unreadable diff now outranks every adoption route in preview, manifest or not, routing `merge_base_missing`/`objects_missing` to deepening history or hydrating partial-clone objects rather than to review. Trigger catalog schema 0.2 -> 0.3; verifier schema 0.6 -> 0.7 (v0.6 stays a frozen, readable reference). `contract_version` and every other schema counter are unchanged. Closes #308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pengfei-threemoonslab
left a comment
There was a problem hiding this comment.
Engineering review outcome: changes requested before merge.
The fail-closed design is strong, but four contract inconsistencies remain: unrelated histories are routed to an unrecoverable fetch loop; worktree partial evidence is still discarded; preview can contradict its own published trigger verdict; and the agent-facing contract omits the intentional partial-run exception. See the inline findings for concrete reproductions and fixes.
Verification performed: targeted verifier/trigger/schema tests and Ruff passed; GitHub CI, Agents Shipgate, and self-dogfood are green. The full local suite had no assertion failures; eight wheel-packaging setup cases could not install hatchling in the network-restricted sandbox. The exact-head Shipgate receipt validated successfully and reported human_review_required, release decision review_required, 0 blockers, and 18 trust-root review items. Static analysis only.
| DiffCompleteness = Literal["complete", "partial", "unavailable"] | ||
|
|
||
| _FETCHABLE_DIFF_REASONS: frozenset[str] = frozenset( | ||
| {"refs_missing", "merge_base_missing", "objects_missing"} |
There was a problem hiding this comment.
[P2] Distinguish shallow history from genuinely unrelated histories
merge_base_missing is not always fetch-repairable. The new _unrelated_histories fixture creates two orphan roots in a non-shallow repository; no git fetch --deepen or git fetch --unshallow can create a common ancestor, so this routes the agent to fetch_base forever. Please make repairability conditional on the repository's shallow/object state (or split the reason) and route a non-shallow missing merge base to correcting the base or human review.
There was a problem hiding this comment.
Fixed in 09c31d1 — split the reason rather than making one reason conditionally repairable.
You are right that the fixture proves the bug: two orphan roots in a non-shallow repo can never gain a common ancestor, so fetch_base was an infinite loop. git rev-parse --is-shallow-repository discriminates the two causes exactly, and I verified both directions on real repositories before changing anything — a --depth 1 clone with a --depth 1 base fetch reports no merge base and is-shallow-repository: true, and git fetch --deepen genuinely repairs it; the orphan-root repo reports the identical Git error with is-shallow-repository: false.
merge_base_missingnow means shallow checkout truncated a merge base that does exist →fetch_repairable: true, remediation deepens.- New
unrelated_historiesmeans no common ancestor exists →fetch_repairable: false, routed to review with "confirm the base names the right comparison point — a force-push or a rewritten branch produces this". - A failed shallow probe returns
git_failed, not either of the above: it is the one case with no basis to claim the histories are unrelated, so it synthesizes neither repair.
Chose splitting over a conditional flag so the token itself is the machine-readable answer. Regression tests: test_shallow_history_reports_a_repairable_missing_merge_base, test_unrelated_histories_are_never_routed_to_another_fetch, test_unrelated_histories_route_a_verify_run_to_a_human, and test_deepening_a_shallow_clone_actually_repairs_the_diff — the last one actually runs git fetch --deepen and asserts the diff then reads complete, so the remediation is pinned as the one that works rather than the one that reads well.
| # A worktree shortfall is never softened by a committed-ref diff | ||
| # that did read cleanly: the two are unioned into one change set, | ||
| # so the union is only as complete as its weakest half. | ||
| diff_input = _least_complete(diff_input, worktree_failure) |
There was a problem hiding this comment.
[P2] Merge typed worktree evidence before failing
When working_tree_context() raises DiffInputError, worktree_failure already contains the authoritative changed_files, but this block only updates diff_input; it never merges those paths (or any available text) into the verifier accumulators. I reproduced diff_status: partial with changed_files: [] while base_notes said the paths were collected, and a changed tools/new_mcp.json therefore lost its path-rule match. Please merge the partial context before building the artifact.
There was a problem hiding this comment.
Fixed in 09c31d1 — and the reproduction found a second hole next to it.
The handler now merges worktree_failure.changed_files and .diff_text into the accumulators before evaluate() and _build_verifier(), so base_notes and changed_files can no longer disagree.
While writing the regression test I hit the related gap: an untracked tools/new_mcp.json was still missing after the merge, because working_tree_context collected the untracked inventory after the body read. A brand-new file appears in no git diff at all, so a body failure dropped it entirely. ls-files --others is cheap path metadata and independent of the body, so it now runs immediately after --name-status. Same reasoning as splitting metadata from body in the committed-ref collector — every cheap, independent read happens before the expensive one that can fail.
I also attached paths and text to the DiffInputError raised when the binary-hiding guard cannot run inside working_tree_context, which previously propagated bare.
Regression test test_verify_merges_partial_worktree_paths_into_the_change_set drives the real CLI with a shrunk body bound and asserts both the note and changed_files name the path, and that TRIGGER-MCP-EXPORT-CHANGED fires. It deliberately omits --head, since archive_head=head is not None is what selects the worktree collector — my first draft passed --head and silently exercised nothing.
| ) | ||
| headline = ( | ||
| f"Shipgate preview {read} the requested PR diff " | ||
| f"({diff_input.reason}); no relevance verdict was reached." |
There was a problem hiding this comment.
[P2] Keep the headline consistent with the published trigger verdict
An incomplete diff can still yield a sound run verdict: with partial evidence containing tools/new_mcp.json, the evaluator intentionally returns evaluation_status: "evaluated" and should_run: true. This branch nevertheless publishes “no relevance verdict was reached” as both headline and control.reason. Please branch on trigger.evaluation_status; when a run match exists, say relevance was established but the complete diff must still be recovered.
There was a problem hiding this comment.
Fixed in 09c31d1. The branch now reads the published trigger result instead of assuming the diff failure implied no verdict:
run_shipgatetrue → "…the paths it did read already show an agent-capability surface, so relevance is established; recover the full diff before trusting any merge verdict."- otherwise → the previous "no relevance verdict was reached".
next_action stays fetch_base/review: relevance being established does not make the merge verdict knowable, so recovering the diff is still the right first step. merge_verdict stays unknown and can_merge_without_human stays false.
Verified on your exact scenario — a blobless clone whose partial paths include tools/new_mcp.json — where evaluation_status: evaluated, should_run: true, TRIGGER-MCP-EXPORT-CHANGED matched. Covered by test_partial_evidence_that_proves_relevance_keeps_its_run_verdict, which asserts on both headline and control.reason, since they were both wrong and are derived separately.
| - **CLI surface** follows the current 0.x contract line — see https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md. | ||
| - **Installed CLI contract**: when available, run `agents-shipgate contract --json` to verify local schema versions, capability/research surfaces, `release_decision.decision`, and manual-review signal fields. Older installs should use [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md) or upgrade before automating against the local contract command. | ||
| - **Verifier JSON**: `verifier_schema_version: "0.6"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. | ||
| - **Verifier JSON**: `verifier_schema_version: "0.7"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. Check `diff_status.completeness` before you believe any negative result: only `"complete"` means the PR diff was actually read. Anything else (`reason` is one of `not_attempted`, `refs_missing`, `merge_base_missing`, `objects_missing`, `metadata_limit_exceeded`, `body_limit_exceeded`, `git_timeout`, `git_failed`) means the evidence was missing, `trigger.evaluation_status` is `"not_evaluated"`, and `trigger.should_run` is `null` — report the missing input and follow `remediation`; never say the PR is unrelated to agent capabilities. |
There was a problem hiding this comment.
[P2] Document the intentional partial-run exception
This says every incomplete diff implies not_evaluated/null, but the evaluator deliberately preserves evaluation_status: "evaluated" and should_run: true when partial path or manifest evidence already proves Shipgate should run. STABILITY.md later documents that exception. Please align this canonical skill, AGENTS.md, the generated skill mirrors, and llms-full.txt so consumers do not override or misreport a valid run verdict.
There was a problem hiding this comment.
Fixed in 09c31d1 across every teaching surface, not just this one — the same overreaching sentence was in AGENTS.md, all four decide-shipgate-relevance.md copies, all three SKILL.md copies, and llms-full.txt.
They now state the monotonicity rule that STABILITY.md already carried: an input_status other than complete alongside evaluation_status: evaluated is not a contradiction — the paths that were read already proved Shipgate should run — so honor should_run: true rather than overriding it, and still say the diff needs recovering. AGENTS.md adds the explicit instruction to branch on evaluation_status, not on should_run alone. docs/agent-contract-current.md gains the same note, and the new unrelated_histories token is in every reason list.
Byte-identity across the copies is preserved; EXPECTED_CLAUDE_CODE_SKILL_RENDER_SHA256 is updated for both SKILL.md and the prompt. Neither outgoing hash is appended to prior_render_sha256, since both were introduced earlier on this same unmerged branch and never shipped.
… evidence Four contract inconsistencies from the engineering review on #340. **Unrelated histories were routed into an unrecoverable fetch loop.** Git reports two different failures identically as "no merge base": a shallow checkout that truncated a merge base which does exist, and two roots that share no ancestor at all. Only the first is repairable by fetching, and the second was being sent to `fetch_base` forever. `git rev-parse --is-shallow-repository` discriminates them exactly, so the reason splits: `merge_base_missing` (shallow — deepen, `fetch_repairable: true`) and the new `unrelated_histories` (no fetch can create an ancestor — confirm the base ref, routed to a human). A failed shallow probe is neither and stays `git_failed` rather than asserting a cause it cannot establish. A regression test deepens a real shallow clone and proves the diff then reads clean, so the remediation is the one that actually works. **Partial worktree evidence was collected and then dropped.** When `working_tree_context` raised, the handler recorded the classified reason but never merged the paths the failed collector had already read, so `base_notes` said the paths were collected while `changed_files` was empty and a changed `tools/new_mcp.json` lost its path-rule match. The paths and any text now merge into the accumulators before the artifact is built. The untracked-path inventory also moves ahead of the body read: it is cheap metadata independent of the body, and a brand-new capability file appears in no `git diff` at all, so collecting it afterwards meant a body failure dropped it entirely. **Preview could contradict its own published verdict.** Partial evidence can still carry a sound run verdict — a matched path rule needs no diff body — and the evaluator publishes it deliberately. The failure branch nevertheless printed "no relevance verdict was reached" as both `headline` and `control.reason` next to `should_run: true`. It now branches on the trigger result and says relevance is established while the full diff still needs recovering. **The agent-facing surfaces omitted that exception.** SKILL.md (all three copies), the relevance prompt (all four), and AGENTS.md said every incomplete diff implies `not_evaluated`/`null`, which would have consumers override a valid run verdict. They now state the monotonicity rule that STABILITY.md already documented, and carry the new reason token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pengfei-threemoonslab
left a comment
There was a problem hiding this comment.
Engineering re-review of current head 09c31d15. The earlier unrelated-history routing and lost worktree-path findings are fixed, but four P2 contract inconsistencies remain, detailed inline below.
The PR description also needs refreshing: its reason list still omits unrelated_histories; one summary bullet still says every incomplete input nulls all verdict fields; and the before/after table still combines shallow and unrelated histories even though the implementation now deliberately separates them.
Verification performed: all three current GitHub workflows are green; targeted verifier/trigger/contract tests and Ruff passed; the exact-head verification receipt reproduced as valid. Self-dogfood remains correctly human-routed (release_decision: review_required, 0 blockers, 18 review items) because the PR edits trust roots.
| # Always emitted by v0.7. ``None`` means the artifact predates v0.7 and | ||
| # carries no input-health evidence at all — which a consumer must treat as | ||
| # "unknown", never as "complete". | ||
| diff_status: VerifierDiffStatus | None = None |
There was a problem hiding this comment.
[P2] Enforce diff_status on current v0.7 artifacts
The comment says this field is always emitted by v0.7, but both this model and _build_verifier default it to None. Removing diff_status from a current verifier_schema_version: "0.7" payload still passes VerifierArtifact.model_validate, and the generated v0.7 schema does not require it. That leaves the core input-health contract unenforced and makes a malformed current artifact indistinguishable from the legacy-unknown case. Please require a non-null status for current v0.7 artifacts and confine the nullable legacy representation to the compatibility-normalization path.
There was a problem hiding this comment.
Fixed in bfe7c3e — you are right that a documented invariant enforced nowhere is not an invariant.
diff_status is now a required, non-null field on VerifierArtifact, so it is in the generated v0.7 schema's required set and model_validate rejects a current payload without it. _build_verifier also drops its default, so a new call site has to decide rather than inherit complete.
The nullable representation is confined to the compatibility path exactly as you asked, but it needed somewhere honest to land: a pre-v0.7 artifact recorded nothing, and normalizing that to complete would manufacture the one claim the field exists to stop. So DiffCompleteness gains a fourth value, unknown, produced only by legacy normalization (VerifierDiffStatus.unknown()). It is not complete, so it still withholds permission to read a negative trigger verdict — the fail-closed property is preserved, and it matches the BoundaryChangeSet.completeness vocabulary check already uses.
While making it required I also closed a smaller hole beside it: fetch_repairable was free to disagree with reason. A model validator now rejects fetch_repairable: true for any reason outside {refs_missing, merge_base_missing, objects_missing}, so an artifact cannot advertise a fetch as the repair for a failure a fetch cannot touch — the structural half of finding 3738940153.
Cost: 66 tests constructed VerifierArtifact without the field. All updated rather than papered over with a default. Covered by test_a_current_artifact_cannot_omit_its_input_health (asserts the current payload is rejected and that the same payload at 0.6 normalizes to unknown) and test_fetch_repairable_cannot_be_claimed_for_a_deterministic_failure.
| f"Could not collect working-tree diff context. " | ||
| f"{worktree_failure.note}" | ||
| ) | ||
| diff_failure_action = diff_failure_action or _diff_failure_action( |
There was a problem hiding this comment.
[P2] Derive the repair action from the selected failure
When the committed diff first fails with a fetch-repairable partial result (for example objects_missing) and the worktree then fails with a stronger non-fetchable result (unavailable / git_failed), line 515 selects the worktree failure for diff_status, but this or preserves the earlier fetch_base action. I reproduced an emitted artifact with diff_status.reason: git_failed, fetch_repairable: false, and control.next_action.kind: fetch_base. Please consolidate the failures before deriving the action, or give the non-fetchable failure precedence, so the published status and authorized repair cannot disagree or loop.
There was a problem hiding this comment.
Fixed in bfe7c3e. You identified the real defect: the action was derived incrementally while the status was derived by consolidation, so the two could not stay in agreement.
Both now come from one selection. Failures accumulate as (context, expects) pairs, and _worst_diff_failure picks one — ordered first by completeness, then breaking ties toward the failure a fetch cannot repair. Authorizing another fetch against a deterministic failure is precisely the loop the ordering exists to prevent, so when the two halves are equally incomplete the non-fetchable one wins. _diff_failure_action and the headline are then derived from that single selected failure, so diff_status and the authorized repair are structurally incapable of disagreeing.
This also removed the last incremental branch: the ref_missing case previously left the action to _derive_verifier_control, so routing is now uniform across all three collectors.
Belt and braces on top, since this class of contradiction had already occurred twice: VerifierDiffStatus now rejects fetch_repairable: true for a non-fetchable reason at construction, so the disagreement cannot be expressed even by a future call site that bypasses this consolidation.
Reproduced your exact scenario as test_the_worst_failure_decides_both_status_and_repair — committed diff fails refs_missing (fetchable), worktree then fails deterministically — and asserted reason: git_failed, fetch_repairable: false, next_action.kind != "fetch_base".
| # the headline contradict the artifact it summarizes. | ||
| if trigger.get("run_shipgate"): | ||
| outcome = ( | ||
| "the paths it did read already show an agent-capability " |
There was a problem hiding this comment.
[P2] Do not attribute every run verdict to diff paths
run_shipgate can be true without any path evidence: in an adopted repository, TRIGGER-EXISTING-MANIFEST-PRESENT force-runs independently of the unreadable diff. With a missing base I reproduced changed_files: [], force_run: true, followed by this claim that the paths already show a capability surface. The skill mirrors repeat the same explanation. Please distinguish the force-run rule (or say only that available independent evidence established Shipgate must run) instead of asserting paths were read.
There was a problem hiding this comment.
Fixed in bfe7c3e. Correct, and it is a sharper point than it first looks: the previous round's fix made the headline agree with should_run but then over-explained why, attributing a manifest-driven verdict to paths that were never read.
The branch now separates the two kinds of evidence via _matched_diff_evidence(), which asks whether any matched rule has an action other than force_run:
- a rule matched on the change set → "the change it did read already shows an agent-capability surface"
force_runonly → "this workspace is already configured for Shipgate, so verification must run regardless"
Verified on an adopted repo with a missing base: changed_files: [], force_run: true, and the headline now says "already configured for Shipgate" rather than claiming a surface was seen.
The skill mirrors repeated the same explanation, so all of them changed too — the three SKILL.md copies, all four decide-shipgate-relevance.md copies, AGENTS.md, STABILITY.md, and docs/agent-contract-current.md now tell consumers to read matched_rules before attributing a run verdict, and say plainly that a force_run match rests on the manifest rather than on anything the diff showed. Covered by test_a_force_run_verdict_is_not_attributed_to_unread_paths.
| trigger=trigger, | ||
| base_status=base_status, | ||
| base_tree=base_tree, | ||
| diff_status=_diff_status_artifact(diff_input), |
There was a problem hiding this comment.
[P2] Keep the failed-verification headline aligned with the control route
This branch now carries a typed diff failure and repair action, but it does not supply a failure-specific headline, so _verifier_headline() maps every failed/unknown scan to "human review required". For a missing base ref the same artifact has control.state: agent_action_required, human_review.required: false, and next_action.kind: fetch_base, while both headline and control.reason say human review is required. Please derive the failure headline/reason from diff_status and the selected control action so the operational contract does not contradict itself.
There was a problem hiding this comment.
Fixed in bfe7c3e. _verifier_headline() collapses every failed/unknown scan to "human review required", which is right for a failed scan and wrong for a failed input that an agent can repair itself — and control.reason is derived from the same string, so both surfaces contradicted the route.
The diff_unavailable branch now passes a headline_override from the new _diff_failure_headline(), derived from the same selected failure that produced the control action:
fetch_repairable→ "…the history it needs is not available locally yet, so no verdict was reached. Make it available, then rerun verify." (pairs withagent_action_required/fetch_base)- otherwise → "…fetching cannot repair this, so no verdict was reached and a human must resolve the input." (pairs with
human_review_required)
Verified on your missing-base-ref case: control.state: agent_action_required, next_action.kind: fetch_base, human_review.required: false, and neither headline nor control.reason now contains "human review required". test_the_failure_headline_matches_the_control_route asserts exactly that pairing on both surfaces.
The PR description is also refreshed as you asked: unrelated_histories is in the reason list, the summary bullet now states the run/skip asymmetry instead of claiming every incomplete input nulls all verdict fields, and the before/after table has separate rows for shallow versus unrelated histories.
…outing Four contract inconsistencies from the second engineering review on #340. **`diff_status` was documented as always emitted and enforced nowhere.** Both the model and `_build_verifier` defaulted it to `None`, so a current `verifier_schema_version: "0.7"` payload could drop the input-health block entirely and still validate — indistinguishable from one that read its diff cleanly, which is the single claim this field exists to prevent. It is now a required, non-null field on the model and in the generated v0.7 schema, and `_build_verifier` requires its caller to supply one. The nullable representation is confined to the compatibility path: a pre-v0.7 artifact normalizes to `VerifierDiffStatus.unknown()`, a new fourth completeness value that names exactly what such an artifact recorded — nothing. Like every value other than `complete` it withholds permission to read a negative trigger verdict. `fetch_repairable` is now also structurally checked against the reason, so no artifact can advertise a fetch as the repair for a failure a fetch cannot touch. **The repair action was derived incrementally, not from the reported status.** When the committed diff failed fetch-repairably and the worktree then failed deterministically, `_least_complete` selected the worktree failure for `diff_status` while an `or` preserved the earlier `fetch_base` action — an artifact carrying `fetch_repairable: false` beside an authorized fetch, which is the loop the classification exists to prevent. Failures now accumulate with their repair targets and `_worst_diff_failure` selects one, breaking ties toward the failure a fetch cannot repair; the action and the headline are both derived from that single selection. **The failed-verification headline contradicted the control route.** The branch supplied no headline override, so `_verifier_headline` mapped every failed/unknown scan to "human review required" while control said `agent_action_required` with `next_action: fetch_base` and `human_review.required: false`. Both now come from the same classified failure. **Not every run verdict is attributable to diff paths.** In an adopted repository `TRIGGER-EXISTING-MANIFEST-PRESENT` force-runs on the manifest alone, with `changed_files: []`, and the preview headline nevertheless claimed the paths it read showed a capability surface. It now separates rules matched on the change set from the force-run rule, and the skill mirrors, prompts, AGENTS.md, STABILITY.md, and the agent contract all point readers at `matched_rules` before attributing the verdict. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pengfei-threemoonslab
left a comment
There was a problem hiding this comment.
Post-merge engineering re-review of the merged head bfe7c3e3. The four findings from the previous pass are fixed and the PR description is now accurate, but two P2 verifier-contract validation gaps remain, detailed inline below. They are not produced by the normal emitter path, but both the current Python reader and the published v0.7 wire schema still accept malformed or contradictory diff_status states, so follow-up fixes are recommended.
Verification performed: GitHub CI, Agents Shipgate, and self-dogfood are green; targeted verifier/trigger/contract tests and Ruff passed; the full suite reached 100% with no assertion failures (eight packaging setup cases could not download hatchling>=1.31.0 in the network-restricted sandbox). The exact-head verification receipt reproduced as valid. Self-dogfood correctly reported human_review_required, release decision review_required, 0 blockers, and 18 trust-root review items. Static analysis only.
|
|
||
| model_config = ConfigDict(extra="forbid") | ||
|
|
||
| completeness: DiffCompleteness = "complete" |
There was a problem hiding this comment.
[P2] Require an explicit current-v0.7 completeness value
Requiring the top-level diff_status block does not yet require any input-health claim inside it: completeness still defaults to complete, and the public literal includes the compatibility-only unknown value. Starting from an emitted current-v0.7 payload, I replaced the block with {} and both VerifierArtifact.model_validate and docs/verifier-schema.v0.7.json accepted it; Pydantic silently materialized completeness: complete. Replacing it with {"completeness":"unknown"} also passed both validators even though the comment above says unknown is reachable only through legacy normalization. Please require completeness on the current wire model/schema and reject unknown when the original artifact version is v0.7, keeping that state confined to the pre-v0.7 compatibility reader.
| "VerifierDiffStatus.reason must be present exactly when the " | ||
| "diff was partially read or unavailable" | ||
| ) | ||
| if self.completeness != "complete" and self.fetch_repairable and ( |
There was a problem hiding this comment.
[P2] Enforce the diff-status relationships in the public contract
This condition skips repairability validation when completeness == "complete", so VerifierDiffStatus(completeness="complete", fetch_repairable=True) currently passes Pydantic. The generated v0.7 JSON Schema is looser still because model_validator relationships are not emitted: it accepts partial without a reason, complete with reason: refs_missing, and unavailable + unrelated_histories + fetch_repairable: true. Downstream consumers that validate against the published schema can therefore accept states the Python model rejects, including a repair flag on a diff that claims to be complete. Please make any true repairability flag require a fetch-repairable reason and encode the completeness/reason/repair relationships in the generated schema, for example with a tagged union or allOf conditions.
Summary
verify --previewcollapsed every diff-acquisition failure into one message, then evaluated the trigger catalog against the empty inputs that failure left behind — publishingskip_reason: "no_match"with the rationale "nothing in this PR signals a tool-surface change" about a PR it had never read. Fixes bug(verify-preview): distinguish unreadable diff from trigger no_match #308.not_attempted,refs_missing,merge_base_missing,unrelated_histories,objects_missing,metadata_limit_exceeded,body_limit_exceeded,git_timeout,git_failed— read off Git's own diagnostic (stderr wasDEVNULL, which is why every failure looked identical; it is now piped and drained on its own thread under an 8 KiB cap). They travel on a new requiredverifier.jsondiff_statusblock with a bounded, path-redacted excerpt, the precise repair, andfetch_repairable.merge_base_missingis a shallow checkout that truncated a merge base which does exist (deepen —fetch_repairable: true), whileunrelated_historiesis two roots with no common ancestor, which no fetch can ever create (route to a human). Sending the second tofetch_baselooped an agent forever.--name-statusin full, and those paths are exactly what says a PR touches an agent surface. The untracked-path inventory moves ahead of the body read for the same reason — a brand-new capability file appears in nogit diffat all.input_statusandevaluation_status, withshould_run/run_shipgate/skip/skip_reasonnullwhen the withheld verdict would have been a skip — see the asymmetry below.The rule behind the asymmetry
Rule matching is monotone in path and diff evidence: adding evidence can only add matches. So a skip verdict reached from incomplete input is unsound — the missing bytes are exactly what would have flipped it — and is withheld (
evaluation_status: not_evaluated, verdict fieldsnull). A run verdict is sound and is still published (evaluation_status: evaluated,should_run: true). Consumers must branch onevaluation_status, not onshould_runalone, and readmatched_rulesbefore attributing a run to anything the diff showed: in an adopted repositoryTRIGGER-EXISTING-MANIFEST-PRESENTforce-runs on the manifest alone, withchanged_files: [].Before / after, reproduced on real repositories
skip_reason: "no_match"; note said "exceeded static output bounds"merge_base_missing,fetch_repairable: true,next_action: fetch_base, remediation deepens — and a test provesgit fetch --deepenthen reads the diff cleanfetch_baseunrelated_histories,fetch_repairable: false, routed to a human to correct the baseGIT_NO_LAZY_FETCH=1src/agent.pydiscardedpartial/objects_missing,changed_fileskeepssrc/agent.py, remediation namesgit fetch --refetch_DIFF_BODY_LIMITpartial/body_limit_exceeded, paths preservedshipgate.yaml(cold start)initializeGoogle ADK
agent.pyis matched by thediff_contains: "FunctionTool("token, not by any path glob — losing the diff body genuinely loses the match, which is why a body-only failure reportsnot_evaluatedrather thanno_match.Type
Verification
CI is authoritative for
python -m ruff check .,python -m compileall -q src tests, andpython -m pytest.Additional local checks run:
pytest -qsuite green (0 failures) andruff check .clean.verify --config shipgate-self.yaml --base main --head HEAD --ci-mode advisory→merge_verdict: human_review_required(this PR edits trust roots, so a human reviews it; the self workflow fails only onblocked,unknown).scripts/generate_schemas.py;llms-full.txtregenerated withscripts/build-llms-full.py.Release-readiness notes
docs/checks.md— n/a, no check IDs added or changedSTABILITY.mdSchema versions
Trigger catalog
0.2 → 0.3and verifier0.6 → 0.7(v0.6 stays a frozen, readable reference; pre-v0.7 artifacts normalize todiff_status.completeness: "unknown").contract_versiondeliberately stays at19, andreport_schema_versionand every other counter are unchanged:STABILITY.mddecouples schema counters from the contract counter, and contract bumps are tied to CLI-surface breaks. Both bumps have a migration note inSTABILITY.md.Reviewer notes
diff_statusis top-level, not insidetrigger. Burying it in the trigger block would repeat the original mistake — making a diff-acquisition fact look like a trigger opinion. That is what forced the verifier bump. It is required on current artifacts, so a payload that omits it cannot pass as one that read its diff cleanly._worst_diff_failurebreaks ties toward the failure a fetch cannot repair, sofetch_repairable: falsecan never appear beside an authorizedfetch_base, and the headline can never say "human review required" over anagent_action_requiredroute.test_verify_preview_missing_base_without_manifest_recommends_init(now expectsfetch_base, renamed) andtest_scenario_docs_only_no_shipgate_fails_closed(should_runwasFalse, nowNone).nullas falsy stays safe — it routes to "do not claim this PR is irrelevant" — but reporting it as "skip" is not. The only public schema that already typesshould_run(docs/feedback-schema.v0.1.json) declares it nullable, so nothing contradicts.shipgate checkis untouched. It errors loudly on an unreadable diff rather than misleading, so it is outside this issue — but now that the paths survive it could return a partialBoundaryChangeSetinstead. Worth a follow-up.fetch_required/selection_requiredtorun_verifyusing the sameevaluate(paths=[], diff_text="", …)pattern. On this base that call needsinput_status=INPUT_UNAVAILABLE, or its new fail-closed states ship with a contradictory trigger block — which is the sequencing the issue asked for.tests/golden/codex_boundary_result/*.jsonwere regenerated withCLAUDECODEunset (the root conftest scrubs it, so regenerating from an agent shell would writeagent: "claude-code"into the goldens).🤖 Generated with Claude Code