Below is a **complete review** of the `tools/align‑check.sh` script you added for FVC 36, together with:

1. **What the script does today** (layer‑by‑layer walk‑through).  
2. **Potential edge‑cases / bugs** that can bite you in production.  
3. **Suggested hardening / optional enhancements** (minimal‑impact changes that make the gate more reliable).  
4. **How to integrate it into your existing dispatch pipeline** (including CI, pre‑push hooks, and automated reporting).  
5. **A quick test‑suite outline** you can drop into your repo to sanity‑check the script on every change.

Feel free to cherry‑pick the items that make sense for your workflow.

---

## 1️⃣  What the script currently does (layer‑by‑layer)

| Layer | Goal | Implementation | Success criteria |
|------|------|----------------|------------------|
| **[1] Repo clean** | No stray changes except in a whitelist of “scratch” directories (`.mavis/`, `agents/id<N>/`, `.tmp_fvc36/`). | `git status --porcelain` → filter out whitelist patterns → any remaining lines = FAIL. | `git status` reports *nothing* outside the whitelist. |
| **[2] HEAD == origin/main** | The local `HEAD` must point to the same commit as the remote tracking branch `origin/main`. | `git rev-parse HEAD` vs `git rev-parse origin/main`. | Exact SHA match. |
| **[3] No unmerged local branches** | All local branches must already be merged into `main`. | `git branch --no-merged main`. | Empty output. |
| **[4] No local `fix/*` or `feat/*` branches ahead of `main`** | Prevent “forgot‑to‑merge” feature/fix branches from lingering. | For each `fix/*`/`feat/*` branch, count commits ahead of `main` (`git rev-list --count main..branch`). | Zero‑ahead for every branch. |
| **[5] Deploy‑hash verification** | Optional check that the supplied hash (full SHA, prefix, or `Build‑Hash:` trailer) appears in the current commit. | If `$DEPLOY_HASH` is set, compare against `HEAD` SHA, prefix, or grep the commit message. | Any of the three matches → PASS. |

If **all layers PASS** (or N/A for layer 5) the script prints `RESULT: ALIGNED` and exits `0`. Any FAIL turns the result into `MISALIGNED` and exits `1`, which your dispatcher treats as a hard stop.

---

## 2️⃣  Edge‑cases / Gotchas that can cause false‑positives or false‑negatives

| Layer | Issue | Why it matters | Quick fix / mitigation |
|------|-------|----------------|------------------------|
| **[1]** | **Renamed whitelist directories** – e.g. you later add `tmp/` or `build/` but forget to update the regex. | The script will start failing on perfectly clean repos. | Keep the whitelist in a **config variable** (`ALLOWED_UNTRACKED`) and document it. |
|      | **Submodule changes** – `git status` shows `?? submodule/` or `M submodule/`. | Submodule updates are legitimate but will be flagged as “untracked”. | Add `|^(\?\? |\s)submodule/` to the filter, or run `git submodule foreach --quiet 'git status --porcelain'` and treat them separately. |
|      | **Detached HEAD** – CI jobs that checkout a commit directly (`git checkout <sha>`). | `HEAD` still points to a commit, but `origin/main` may be unrelated → layer 2 fails. | Detect detached HEAD (`git rev-parse --abbrev-ref HEAD`) and either skip layer 2 or treat it as a separate “detached‑HEAD” mode. |
| **[2]** | **Remote name not `origin`** – some forks use `upstream` or a custom remote. | The script will compare against a non‑existent ref → FAIL. | Allow the remote name to be overridden via an env var (`REMOTE_NAME=${REMOTE_NAME:-origin}`). |
|      | **Shallow clone** – `git fetch --depth=1` may not have `origin/main` locally. | `git rev-parse origin/main` returns “unknown revision”. | Fall back to `git ls-remote` to fetch the remote SHA when missing. |
| **[3]** | **Branch named `main` vs `master`** – older repos still use `master`. | `git branch --no-merged main` will error out or always report everything as unmerged. | Detect the default branch (`git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'`) and use that variable throughout. |
| **[4]** | **Long‑running feature branches** that are intentionally *ahead* of `main` (e.g. a release branch). | The gate will block a legitimate workflow. | Add a whitelist for branch patterns (`release/*`, `hotfix/*`) via a config array. |
| **[5]** | **Multiple `Build‑Hash:` lines** or a hash that appears in the *body* of the commit message but not as a trailer. | Grep will match any occurrence, which may be a false positive. | Use a stricter regex: `grep -E '^Build-Hash:\s*'"$DEPLOY_HASH"'\b'`. |
|      | **Hash supplied is a short prefix that matches *multiple* commits** (e.g. `a371d`). | The script will treat it as a match even though it’s ambiguous. | Verify uniqueness with `git rev-parse --verify "$DEPLOY_HASH^{commit}"` and fail if ambiguous. |
| General | **Non‑zero exit codes from git commands** (e.g. repo not a git repo, missing remote). | The script currently swallows errors (`|| true`) which can hide real problems. | Use `set -e` *after* the whitelist checks, or explicitly test each command’s exit status and surface a clear error. |
| General | **Locale / encoding differences** – `git status` output may contain non‑ASCII characters. | `grep -vE` patterns may mis‑match. | Force `LC_ALL=C` at the top of the script (`export LC_ALL=C`). |

---

## 3️⃣  Suggested hardening / optional enhancements

Below are **minimal‑impact patches** you can apply directly to the script. I’ve grouped them by “must‑have”, “nice‑to‑have”, and “future‑proof”.

### 3.1 Must‑have (robustness)

```bash
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C   # deterministic grep / sort behaviour

# Configurable knobs ---------------------------------------------------------
REMOTE_NAME=${REMOTE_NAME:-origin}
DEFAULT_BRANCH=$(git symbolic-ref "refs/remotes/${REMOTE_NAME}/HEAD" 2>/dev/null \
                 | sed 's@^refs/remotes/[^/]*/@@')
DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}   # fallback for very old repos

# Whitelist of paths that are allowed to be untracked/modified
ALLOWED_UNTRACKED=(
    ".mavis/"
    "agents/id[0-9]+/"
    ".tmp_fvc36/"
)
# Convert array to a single extended‑regex for grep
ALLOWED_REGEX=$(printf '|^(\?\? |M )%s' "${ALLOWED_UNTRACKED[@]}" | tr -d '\n')
```

*Why?*  
- `REMOTE_NAME` and `DEFAULT_BRANCH` make the script work on forks or repos that still use `master`.  
- `LC_ALL=C` eliminates locale‑dependent surprises.  
- Centralising the whitelist makes future edits a one‑liner.

### 3.2 Layer‑1 rewrite (clearer output)

```bash
layer1_output=$(git status --porcelain \
    | grep -vE "$ALLOWED_REGEX" \
    || true)

if [[ -z $layer1_output ]]; then
    print_layer 1 PASS
else
    print_layer 1 "FAIL: untracked/modified paths:"
    # Show a tidy, sorted list for readability
    printf '%s\n' "$layer1_output" | sort -u
    fail=1
fi
```

*Why?*  
- Sorting removes duplicate lines (e.g. a file both `??` and `M`).  
- The regex is now built from the whitelist, so you won’t forget to add new patterns.

### 3.3 Layer‑2 – remote‑aware comparison

```bash
# Ensure we have the remote ref locally; fetch if missing
if ! git rev-parse --verify "${REMOTE_NAME}/${DEFAULT_BRANCH}" >/dev/null 2>&1; then
    git fetch "${REMOTE_NAME}" "${DEFAULT_BRANCH}"
fi

head_sha=$(git rev-parse HEAD)
remote_sha=$(git rev-parse "${REMOTE_NAME}/${DEFAULT_BRANCH}")

if [[ $head_sha == $remote_sha ]]; then
    print_layer 2 PASS
else
    print_layer 2 "FAIL: HEAD=$head_sha ${REMOTE_NAME}/${DEFAULT_BRANCH}=$remote_sha"
    fail=1
fi
```

*Why?*  
- Guarantees the remote ref exists even in shallow clones.  
- Uses the dynamically discovered default branch.

### 3.4 Layer‑3 – use the discovered default branch

```bash
unmerged=$(git branch --no-merged "$DEFAULT_BRANCH" || true)
```

### 3.5 Layer‑4 – whitelist for “allowed‑ahead” branches

```bash
# Add patterns that you deliberately keep ahead of main (e.g. release/*)
ALLOWED_AHEAD=(
    "release/.*"
    "hotfix/.*"
)

ahead_branches=()
while IFS= read -r b; do
    [[ -z $b ]] && continue
    [[ $b == "$DEFAULT_BRANCH" ]] && continue
    # Skip whitelisted patterns
    for pat in "${ALLOWED_AHEAD[@]}"; do
        if [[ $b =~ $pat ]]; then
            continue 2   # skip to next branch
        fi
    done
    ahead_count=$(git rev-list --count "${DEFAULT_BRANCH}..${b}" || echo 0)
    if (( ahead_count > 0 )); then
        ahead_branches+=("$b ($ahead_count)")
    fi
done < <(git branch --list 'fix/*' 'feat/*' --format='%(refname:short)' || true)
```

### 3.6 Layer‑5 – stricter hash handling

```bash
if [[ -z $DEPLOY_HASH ]]; then
    print_layer 5 'N/A: DEPLOY_HASH not provided'
else
    # Resolve the supplied hash to a full SHA (if possible)
    if full_hash=$(git rev-parse --verify "${DEPLOY_HASH}^{commit}" 2>/dev/null); then
        # full_hash is now a 40‑char SHA
        if [[ $full_hash == $head_sha ]]; then
            print_layer 5 PASS
        else
            print_layer 5 "FAIL: supplied hash resolves to $full_hash, but HEAD is $head_sha"
            fail=1
        fi
    else
        # Not a resolvable SHA → treat as trailer search
        if git show -s --format=%B HEAD | grep -qE "^Build-Hash:\s*$DEPLOY_HASH\b"; then
            print_layer 5 PASS
        else
            print_layer 5 "FAIL: hash $DEPLOY_HASH not found in HEAD or as Build‑Hash trailer"
            fail=1
        fi
    fi
fi
```

*Why?*  
- Guarantees that a short prefix uniquely identifies a commit.  
- Enforces the “trailer” format (`Build-Hash:`) rather than any random occurrence.

### 3.7 Final exit handling (more expressive)

```bash
if (( fail == 0 )); then
    echo 'RESULT: ALIGNED'
    exit 0
else
    echo 'RESULT: MISALIGNED'
    # Optionally dump a machine‑readable JSON blob for CI parsers
    # printf '{"result":"MISALIGNED","failed_layers":[%s]}\n' "$(jq -nR '[inputs]')" >&2
    exit 1
fi
```

---

## 4️⃣  How to wire the gate into your existing dispatch pipeline

Below is a **minimal integration checklist** that works whether you run the dispatch from a CI runner, a local developer machine, or a dedicated “dispatch” VM.

| Step | Command / Hook | What it does |
|------|----------------|--------------|
| **A. Install the script** | `git add tools/align-check.sh && git commit -m "Add ALIGN gate (FVC‑36)"` | Version‑controlled, so every kit ships the same gate. |
| **B. Make it executable** | `chmod +x tools/align-check.sh` | Guarantees it can be run directly. |
| **C. Run after `git push`** | In your dispatch wrapper (e.g. `dispatch.sh`):<br>`./tools/align-check.sh "$REPO_ROOT" "$DEPLOY_HASH"` | If the script exits non‑zero, abort the rest of the dispatch and surface the failure to the operator. |
| **D. CI enforcement** | Add a job to your CI pipeline (GitHub Actions, GitLab CI, etc.)<br>```yaml<br>align-check:<br>  runs-on: ubuntu-latest<br>  steps:<br>    - uses: actions/checkout@v4<br>      with:<br>        fetch-depth: 0   # need full history for remote refs<br>    - name: Run ALIGN gate<br>      run: ./tools/align-check.sh . ${{ env.DEPLOY_HASH }}<br>``` | Guarantees that *any* PR that lands on `main` passes the gate before it can be merged (you can also make it a required status check). |
| **E. Pre‑push hook (optional)** | Create `.git/hooks/pre-push` with:<br>`#!/usr/bin/env bash`<br>`./tools/align-check.sh "$PWD"`<br>`exit $?` | Gives developers immediate feedback before they even hit the remote. |
| **F. Reporting** | The script already prints a human‑readable block. For automated dashboards you can capture the **stdout** and parse the `RESULT:` line, or add the optional JSON snippet suggested in the exit handling. |
| **G. Documentation** | Add a short “ALIGN gate” section to your `README.md` or `docs/dispatch.md` explaining the five layers, the whitelist variables, and how to override the remote/branch if needed. | Keeps the team aware of the expectations. |

---

## 5️⃣  Quick test‑suite (bash + `bats-core`)

A **few dozen tests** are enough to give you confidence that future refactors don’t break the gate. Below is a skeleton you can drop into `tools/tests/align-check.bats`. It assumes you have `bats-core` installed (`brew install bats-core` or `apt-get install bats`).

```bash
#!/usr/bin/env bats

setup() {
    # Create a fresh temporary repo for each test
    TMPDIR=$(mktemp -d)
    cd "$TMPDIR"
    git init -q
    git config user.email "ci@example.com"
    git config user.name "CI Bot"

    # Create a dummy file and commit it
    echo "hello" > README.md
    git add README.md
    git commit -qm "initial commit"

    # Add a remote named origin (pointing to itself for simplicity)
    git remote add origin "$(pwd)"
    git push -q origin main 2>/dev/null || true
}

teardown() {
    rm -rf "$TMPDIR"
}

@test "Layer 1 passes on clean repo" {
    run ../align-check.sh . 
    [ "$status" -eq 0 ]
    [[ "$output" =~ "\[1\] PASS" ]]
}

@test "Layer 1 fails on stray file" {
    echo "temp" > .tmp_fvc36/foo
    run ../align-check.sh .
    [ "$status" -ne 0 ]
    [[ "$output" =~ "\[1\] FAIL" ]]
}

@test "Layer 2 fails when HEAD diverges from origin/main" {
    # Create a new commit locally, but do NOT push
    echo "change" >> README.md
    git commit -am "local change" -q
    run ../align-check.sh .
    [ "$status" -ne 0 ]
    [[ "$output" =~ "\[2\] FAIL" ]]
}

@test "Layer 2 passes after push" {
    git push -q origin main
    run ../align-check.sh .
    [ "$status" -eq 0 ]
    [[ "$output" =~ "\[2\] PASS" ]]
}

@test "Layer 3 fails with an unmerged branch" {
    git checkout -b fix/bug123 -q
    echo "bugfix" >> README.md
    git commit -am "fix bug" -q
    git checkout main -q
    run ../align-check.sh .
    [ "$status" -ne 0 ]
    [[ "$output" =~ "\[3\] FAIL" ]]
}

@test "Layer 4 fails when a fix/* branch is ahead of main" {
    git checkout -b fix/feature -q
    echo "feature" >> README.md
    git commit -am "feature work" -q
    git checkout main -q
    run ../align-check.sh .
    [ "$status" -ne 