# Verify-Only Gate Worktree Policy

> **Status**: Active  
> **Last Updated**: 2026-03-12  
> **Scope**: Process policy for gate-reviewer, retest, and verification-only worktree handling

---

## Purpose

This document establishes the policy for handling verify-only gate worktree branches, ensuring that:
1. Verification-only branches are never merged into main for plan-state noise
2. Evidence retention is explicit when merge is skipped
3. The same hazard is addressed across all verification-only worktree flows

---

## Definitions

### Verify-Only Gate Branch

A **verify-only gate branch** is a branch created by the orchestrator for:
- Gate-reviewer independent verification steps
- Retest verification steps
- Any step marked with `verify-only` intent

**Key characteristic**: These branches are created to run verification in isolated worktrees and are NOT intended to introduce product code changes.

### Product File

A **product file** is any file that is NOT:
- `plan.yaml` (vectl-managed plan state)
- `.git/vectl/claims.json` (claim metadata — **gitdir-only**, not tracked in repo)
- Evidence files (`.final_evidence.yaml`, `.evidence_*.yaml`)
- Gate worktree markers (`.gate-worktree-*`)

**Important**: Files in `.git/` (including `claims.json`) are not part of the tracked repository content. A diff containing only `.git/` changes will appear empty to `git diff main...HEAD --name-only` because `.git/` is excluded from repository tracking.

### Merge-Safe Diff

A diff is **merge-safe** if it contains product file changes that represent actual implementation work.
A diff is **not merge-safe** if it contains ONLY:
- Plan state changes (`plan.yaml`)
- Claim metadata (`.git/vectl/claims.json` — gitdir-only, appears as empty tracked diff)
- Evidence artifacts

---

## Policy: Merge vs Skip Decision

### Decision Tree

```
When a verify-only gate step completes:

1. CHECK: Does the branch have commits?
   - NO (no commits, empty branch) → SKIP merge, preserve evidence in plan.yaml
   - YES → Continue to step 2

2. CHECK: What is in the diff?
   Run: git diff main...HEAD --name-only
   
   a) Empty diff (no changes)
      → SKIP merge
      → Evidence preserved in plan.yaml completion record
   
   b) Plan-state-only diff (only plan.yaml changed)
      → SKIP merge
      → Evidence preserved in plan.yaml completion record

   c) Claim-commit-only diff (only claim metadata changed)
      → SKIP merge
      → Evidence preserved in plan.yaml completion record
      → Note: Claim metadata is gitdir-only; will appear as empty tracked diff

   d) Product files changed (any non-excluded file)
      → INVESTIGATE before proceeding (verify-only branches should not have product changes)
      → If intentional and valid: proceed with orchestrator's merge workflow
      → If unintentional: report as blocker for fix step
      → Evidence in both commit AND plan.yaml if merged

3. DOCUMENT: In all cases, log the decision:
   - If SKIP: Record reason in plan.yaml evidence field
   - If MERGE: Proceed with normal squash/merge workflow
```

### Exclusion List

Files that do NOT constitute mergeable changes:

```
# vectl-managed files (plan-state and claim metadata)
plan.yaml
.git/vectl/claims.json
.git/vectl/state.json

# Evidence artifacts
.final_evidence.yaml
.evidence_*.yaml

# Gate worktree markers
.gate-worktree-*/

# Generated reports (exclude if verification-only)
coverage.xml
*.pyc
__pycache__/
```

---

## Evidence Retention Without Merge

### When Merge is Skipped

Evidence MUST still be preserved. The retention mechanism varies by context:

| Context | Evidence Location | How to Access |
|---------|-------------------|----------------|
| Gate-reviewer step | `plan.yaml` → `steps[].evidence` field | `vectl show <step-id>` |
| Retest step | `plan.yaml` → `steps[].evidence` field | `vectl show <step-id>` |
| Worktree directory | `.gate-worktree-*/` or step directory | Preserved until cleanup |

### Evidence Field in plan.yaml

Every complete step has an `evidence` field. For verify-only gates:

```yaml
- id: phase.step-name-gate
  name: step-name-gate
  status: done
  evidence: |
    ## Gate Review
    
    ### Findings
    - [finding details]
    
    ### Verification Commands
    ```bash
    <commands run>
    ```
    
    ### Result
    - [PASS/FAIL with details]
    
    Files changed:
    - none (verify-only)
    
    Commit hash(es):
    - none (verify-only)
  ...
```

**Key point**: Evidence is captured in plan.yaml regardless of whether a merge occurs.

---

## Failure Path Handling

### Case 1: Empty Gate Branch

**Scenario**: Gate branch has no commits (fresh worktree, nothing committed).

**Action**:
- SKIP merge silently
- Record in evidence: `Files changed: - none (verify-only, empty worktree)`
- Evidence retained in plan.yaml

### Case 2: Plan-State-Only Gate Branch

**Scenario**: Gate branch contains only `plan.yaml` changes from the claim commit.

**Action**:
- SKIP merge
- Record in evidence: `Files changed: - none (verify-only, plan-state drift only)`
- Evidence retained in plan.yaml
- Plan-state drift is intentional (vectl manages it)

### Case 3: Claim-Commit-Only Gate Branch

**Scenario**: Gate branch contains only claim metadata (`.git/vectl/claims.json`) from the claim commit.

**Important**: `.git/vectl/claims.json` exists only in the git directory and is NOT tracked in the repository. A "claim-only" diff will appear as an empty tracked diff because `git diff` excludes `.git/` contents.

**Action**:
- SKIP merge
- Record in evidence: `Files changed: - none (verify-only, claim metadata only)`
- Evidence retained in plan.yaml
- The tracked diff will appear empty; this is expected

### Case 4: Product Files Present

**Scenario**: Gate branch has actual product file changes.

**Action**:
- This indicates the gate step may have made unintended changes OR is a special case
- INVESTIGATE before proceeding
- If intentional and valid: proceed with normal merge
- If unintentional: report as blocker for fix step

---

## Sibling Workflow Hazard Check

### Verification-Only Worktree Flows in This Project

The following steps run in isolated verification worktrees and are subject to this policy:

| Flow Type | Step Pattern | Example Steps |
|-----------|--------------|---------------|
| Gate-reviewer | `*.gate` | `schemas-core-loop.schemas-core-loop-gate` |
| Retest | `*retest-gate*`, `*gate-retest*`, `*.retest-*` | `async-poll-hardening.async-retest-gate-fixes` |
| Verify | `*.verify-*`, `verify-followup-gate-blockers` | `local-provider.verify-followup-gate-blockers` |
| Gate fix | `*.fix-gate-blockers` | `runtime-policy.runtime-policy-fix-gate-blockers` |

### Hazard Check Summary

| Flow | Empty Diff | Plan-State-Only | Claim-Only | Product Files |
|-----|-----------|-----------------|------------|---------------|
| Gate-reviewer | Skip merge | Skip merge | Skip merge | Investigate, then merge if valid |
| Retest | Skip merge | Skip merge | Skip merge | Investigate, then merge if valid |
| Verify | Skip merge | Skip merge | Skip merge | Investigate, then merge if valid |
| Gate fix | N/A (must have fixes) | N/A (must have fixes) | N/A (must have fixes) | Normal merge workflow |

**Note**: Gate fix steps (`*.fix-gate-blockers`) are NOT verify-only — they must contain product fixes and follow the normal merge workflow.

---

## Implementation Checklist

For orchestrator/worktree lifecycle handlers:

- [ ] Detect verify-only intent from step metadata or naming convention
- [ ] Check diff category before merge (empty, plan-state-only, claim-only, product)
- [ ] For non-product diffs: skip merge, preserve evidence in plan.yaml
- [ ] Log merge decision with reason
- [ ] Handle cleanup: remove worktree and branch after evidence is confirmed

---

## Examples

### Example 1: Gate-Reviewer Empty Worktree

```bash
# Gate step completes with no commits
$ git status
nothing to commit, clean working tree

# Decision: SKIP merge
# Evidence: "Files changed: - none (verify-only)"
# Cleanup: worktree removed, branch deleted
```

### Example 2: Gate-Reviewer with Plan-State Drift

```bash
# Gate step completes with only plan.yaml changes
$ git diff main...HEAD --name-only
plan.yaml

# Decision: SKIP merge (plan-state drift is vectl-managed)
# Evidence: "Files changed: - none (verify-only, plan-state drift only)"
```

### Example 3: Gate Fix with Product Changes

```bash
# Fix step completes with actual code changes
$ git diff main...HEAD --name-only
src/anima/shell/agent.py
tests/test_agent.py

# Decision: PROCEED with merge
# Evidence: Includes commit hash and changed files
```

### Example 4: Claim-Commit-Only Gate Branch

```bash
# Gate step completes with only claim metadata (gitdir-only)
$ git diff main...HEAD --name-only
# (empty output - claims.json is not tracked)

# Decision: SKIP merge
# Evidence: "Files changed: - none (verify-only, claim metadata only)"
# Note: The diff appears empty because .git/ is not tracked
```

---

## Enforcement

This policy is enforced by:
1. **Step evidence format**: Must include `Files changed:` section
2. **Orchestrator workflow**: Checks diff category before merge
3. **Incident reviews**: Any merge of plan-state-only diff should be flagged

---

## Related Documents

- [Gate Cleanliness Procedure](./gate-cleanliness.md)
- [vectl Plan Divergence Incident](../archives/incidents/vectl-plan-divergence-2026-03-07.md)
- [AGENTS.md vectl section](../.agents/instructions.md)

---

## Revision History

| Date | Author | Change |
|------|--------|--------|
| 2026-03-12 | software-architect | Initial policy based on runtime-policy-gate blocker findings |

---

## Appendix: Diff Category Detection Script

> **Note**: This script is provided as **reference pseudocode** to illustrate the logic. 
> The glob-like patterns shown are conceptual and would need proper regex conversion
> for production use. For actual implementation, use your orchestrator's existing
> diff-checking logic or adapt the patterns to proper regex syntax.

```bash
#!/bin/bash
# check-verify-only-diff.sh (PSEUDOCODE - ILLUSTRATIVE PURPOSES ONLY)
# Determines if a verify-only gate branch should be merged or skipped
#
# IMPLEMENTATION NOTE: This script shows the INTENT of the check.
# Real implementation must use proper regex syntax for grep -E or
# use alternative tools (awk, grep -F, or file-by-file comparison).
#
# Key insight: .git/vectl/claims.json is gitdir metadata, NOT tracked.
# It will NOT appear in git diff output. Claim-only state appears as
# empty tracked diff.

set -e

# Excluded patterns (conceptual - see implementation note above)
# These represent files that do NOT constitute mergeable changes:
# - plan.yaml: vectl-managed plan state
# - .git/vectl/*: gitdir metadata (not tracked, won't appear in diff)
# - .final_evidence.yaml, .evidence_*.yaml: evidence artifacts
# - coverage.xml, *.pyc, __pycache__/: generated files

EXCLUDED_PATTERNS=(
    "^plan\.yaml$"
    "^\.final_evidence\.yaml$"
    "^\.evidence_.*\.yaml$"
    "^coverage\.xml$"
    ".*\.pyc$"
    ".*/__pycache__/.*"
)

# Get list of changed files (note: .git/ files won't appear here)
CHANGED_FILES=$(git diff main...HEAD --name-only)

# Check for empty diff
if [ -z "$CHANGED_FILES" ]; then
    echo "EMPTY_DIFF"
    exit 0
fi

# Filter out excluded patterns
# Note: In production, use proper regex or file-by-file matching
PRODUCT_FILES=""
while IFS= read -r file; do
    is_excluded=false
    for pattern in "${EXCLUDED_PATTERNS[@]}"; do
        if echo "$file" | grep -qE "$pattern"; then
            is_excluded=true
            break
        fi
    done
    if [ "$is_excluded" = false ]; then
        PRODUCT_FILES="$PRODUCT_FILES"$'\n'"$file"
    fi
done <<< "$CHANGED_FILES"

# Remove leading empty line from PRODUCT_FILES
PRODUCT_FILES=$(echo "$PRODUCT_FILES" | sed '/^$/d')

if [ -z "$PRODUCT_FILES" ]; then
    echo "NON_PRODUCT_DIFF"
    exit 0
fi

echo "PRODUCT_DIFF"
echo "$PRODUCT_FILES"
exit 1  # Indicates merge is needed
```

### Implementation Checklist for Script Adaptation

When adapting this pseudocode to production:

1. **Gitdir awareness**: Files in `.git/` (like `claims.json`) are never in `git diff` output — they're gitdir metadata
2. **Regex syntax**: Convert shell globs (`*.pyc`) to proper regex (`.*\.pyc$`) for use with `grep -E`
3. **Or use `grep -F`**: For fixed-string matching (faster, no regex escaping needed)
4. **Consider your orchestrator**: Existing vectl workflows may have diff-checking utilities — prefer those over custom scripts