CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/coding-policy

General-purpose coding policy for Baruch's AI agents

73

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

test_check_git_sync.shhooks/tests/

#!/usr/bin/env bash
# Outcome-based tests for check-git-sync.sh.
#
# The hook shells out to real git, so tests build real local repos (a bare
# "origin" plus working clones) and drive git offline — no network, fully
# deterministic. The injected clock (SYNC_NOW) drives the throttle assertions.
#
# Each scenario builds its OWN bare origin + seed (mk_origin) so scenarios share
# no mutable state and run in any order (rules/testing-standards.md Independence).
# The harness drops `set -e` to aggregate results, so every fixture-setup command
# is checked explicitly and aborts with a fatal diagnostic on failure
# (rules/error-handling.md aggregate-reporting carve-out).
#
# Covers:
#   1. Behind        -> fast-forwards the default branch (on it or off it); a
#                       Herdr worker only reports; a refused fast-forward reports.
#   2. Up to date    -> emits a marker "in sync" status, exit 0.
#   3. Throttle      -> with a fixed injected clock: a call inside the window
#                       skips the fetch and reports "not verified" (never a
#                       definitive "in sync"/"behind" against the stale ref); a
#                       call past the window fetches and fires "behind".
#   4. Not a repo    -> silent no-op, exit 0.
#   5. No origin     -> silent no-op, exit 0.
#   6. Fetch failure -> marker "not verified" (never a false "in sync" against a
#                       stale ref), exit 0 (offline/broken remote tolerated).
#   7. Bad clock     -> silent no-op, exit 0 (never aborts SessionStart).
#   8. Diverged      -> marker notice names divergence and recommends rebase, not
#                       a fast-forward (local both ahead and behind origin).
#  10. Worker fetch -> a Herdr worker session leaves refs/remotes/origin/*
#                       unchanged while origin has moved; the foreman fetches.
#  11. Role unknown -> HERDR_ENV set and the role probes fail (git shim): no
#                       fetch (refs unchanged) and a sync-not-verified notice.
#  12. Empty HERDR_ENV -> set-but-empty still marks Herdr: a linked worktree
#                       does not fetch.
#  13. Portable worktree -> SESSION_START_MODE=portable in a linked worktree
#                       (env stripped by tessl): no fetch, sync not verified.
#   9. Future stamp  -> a schema_version > 1 record is not throttled on and is
#                       preserved (not downgraded to version 1).
#
# Run: bash hooks/tests/test_check_git_sync.sh
set -uo pipefail

die() { echo "fatal: $*" >&2; exit 2; }

# 0 when <a> and <b> name the same commit in <repo>, 1 when they differ. Called
# in the harness's own shell, never in $(...): a ref that does not resolve stops
# the harness, so an unresolved ref can never read as "different".
same_commit() { # <repo> <a> <b>
  local x y
  x="$(git -C "$1" rev-parse --verify --quiet "$2^{commit}")" || die "cannot resolve $2 in $1"
  y="$(git -C "$1" rev-parse --verify --quiet "$3^{commit}")" || die "cannot resolve $3 in $1"
  [[ "$x" == "$y" ]]
}

cleanup() { [[ -n "${TMP:-}" ]] && ! rm -rf "$TMP" && echo "warn: could not remove $TMP" >&2; return 0; }

g() { git "$@"; }

commit_push() { # <clone-dir> <message>
  local dir="$1" msg="$2"
  printf '%s\n' "$msg" >> "$dir/f"                || die "commit_push: write to $dir/f failed"
  g -C "$dir" add f                               || die "commit_push: git add failed in $dir"
  g -C "$dir" commit -q -m "$msg"                 || die "commit_push: git commit failed in $dir"
  g -C "$dir" push -q origin main                 || die "commit_push: git push from $dir failed"
}

mk_origin() { # <prefix>: sets globals BARE, SEED to a fresh, independent origin
  local prefix="$1"
  BARE="$TMP/${prefix}.git"; SEED="$TMP/${prefix}-seed"
  g init -q --bare -b main "$BARE"                || die "mk_origin: git init --bare failed for $BARE"
  g clone -q "$BARE" "$SEED" 2>/dev/null          || die "mk_origin: git clone failed for $SEED"
  g -C "$SEED" symbolic-ref HEAD refs/heads/main  || die "mk_origin: git symbolic-ref failed in $SEED"
  commit_push "$SEED" "c1"
}

clone_from() { # <bare> <dest>: a working clone, checked explicitly
  g clone -q "$1" "$2"                            || die "clone_from: git clone $1 -> $2 failed"
}

# run <repo-dir> <state-dir> [extra env...] -> OUT, RC
run() {
  local repo="$1" state="$2"; shift 2
  # Scrub the ambient HERDR_ENV so a suite run from a Herdr pane still sees
  # the non-Herdr cases as non-Herdr; a case opts in by passing it in "$@".
  OUT="$(cd "$repo" && env -u HERDR_ENV SYNC_STATE_DIR="$state" "$@" bash "$SCRIPT" </dev/null 2>/dev/null)"
  RC=$?
}

pass() { PASS=$((PASS+1)); }
fail() { FAIL=$((FAIL+1)); echo "  ✗ FAIL: $1" >&2; }

main() {
  SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/check-git-sync.sh"
  [[ -f "$SCRIPT" && -r "$SCRIPT" ]] || die "hook not found/readable at $SCRIPT"
  command -v jq  >/dev/null 2>&1 || die "jq required for these tests"
  command -v git >/dev/null 2>&1 || die "git required for these tests"

  TMP="$(mktemp -d -t git-sync-test.XXXXXX)" || die "mktemp failed"
  trap cleanup EXIT

  # Isolate git from the operator's global/system config so identity and defaults
  # are deterministic across machines. This isolation is load-bearing — an
  # unchecked mkdir failure would silently defeat it, so guard it explicitly.
  export HOME="$TMP/home"
  mkdir -p "$HOME" || die "could not create isolated HOME at $HOME"
  export GIT_CONFIG_NOSYSTEM=1
  export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t

  # An ambient HERDR_ENV, as when the suite runs from a Herdr pane: every
  # non-Herdr case now depends on run's scrub, and each Herdr case sets its own.
  export HERDR_ENV=1

  FAIL=0; PASS=0

  # 1. Behind, read first from a linked worktree with HERDR_ENV set -- a Herdr
  # worker. The shared checkout is the foreman's (skills/herdr-foreman/references/team-operation.md
  # Writers and Checkouts), so the hook reports the drift and moves nothing.
  mk_origin o1
  clone_from "$BARE" "$TMP/r1"
  commit_push "$SEED" "c2"
  # The foreman's earlier fetch: the worker reads drift from these refs as-is.
  git -C "$TMP/r1" fetch -q origin || die "r1 fetch failed"
  git -C "$TMP/r1" worktree add -q "$TMP/r1-wt" -b feat/worker >/dev/null 2>&1 \
    || die "r1 worktree add failed"
  run "$TMP/r1-wt" "$TMP/s1b" HERDR_ENV=1
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Herdr worker session") and test("1 behind") and test("Do not sync") and (test("fast-forward") | not)' >/dev/null 2>&1 \
    && ! same_commit "$TMP/r1" main origin/main; then
    pass; else fail "worker session: expected a no-sync notice and main unmoved, got RC=$RC OUT=$OUT"; fi

  # 1b. The same drift from the MAIN checkout (on main) is the foreman or a
  # standalone agent: the hook fast-forwards main and says so, without running
  # the repo's own post-merge hook.
  printf '#!/bin/sh\ntouch "%s"\n' "$TMP/post-merge-ran" > "$TMP/r1/.git/hooks/post-merge" \
    || die "could not write the post-merge hook"
  chmod +x "$TMP/r1/.git/hooks/post-merge" || die "could not make the post-merge hook executable"
  run "$TMP/r1" "$TMP/s1c" HERDR_ENV=1
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("fast-forwarded local `main` by 1")' >/dev/null 2>&1 \
    && same_commit "$TMP/r1" main origin/main \
    && [[ ! -e "$TMP/post-merge-ran" ]]; then
    pass; else fail "behind on main: expected a fast-forward with no repo hook run, got RC=$RC OUT=$OUT"; fi

  # 1e. Behind under tessl (session-start portable mode): report, never move.
  mk_origin o1e
  clone_from "$BARE" "$TMP/r1e"
  commit_push "$SEED" "c2"
  run "$TMP/r1e" "$TMP/s1f" SESSION_START_MODE=portable
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("not fast-forwarded here")' >/dev/null 2>&1 \
    && ! same_commit "$TMP/r1e" main origin/main; then
    pass; else fail "portable: expected a report and main unmoved, got RC=$RC OUT=$OUT"; fi

  # 1f. The fetch never runs repo code: a reference-transaction hook, which a
  #     ref-updating fetch triggers, leaves no marker.
  mk_origin o1f
  clone_from "$BARE" "$TMP/r1f"
  commit_push "$SEED" "c2"
  printf '#!/bin/sh\ntouch "%s"\n' "$TMP/sync-ref-hook-ran" > "$TMP/r1f/.git/hooks/reference-transaction" \
    || die "could not write the reference-transaction hook"
  chmod +x "$TMP/r1f/.git/hooks/reference-transaction" || die "could not make the hook executable"
  run "$TMP/r1f" "$TMP/s1g"
  if [[ $RC -eq 0 && ! -e "$TMP/sync-ref-hook-ran" ]]; then
    pass; else fail "repo hooks: the reference-transaction hook ran during the sync fetch or fast-forward (OUT=$OUT)"; fi

  # 1c. Behind while a feature branch is checked out: main moves without a
  # checkout, and the feature branch is untouched.
  mk_origin o1c
  clone_from "$BARE" "$TMP/r1c"
  git -C "$TMP/r1c" checkout -q -b feat/x || die "r1c checkout failed"
  commit_push "$SEED" "c2"
  run "$TMP/r1c" "$TMP/s1d"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1 \
    && same_commit "$TMP/r1c" main origin/main \
    && [[ "$(git -C "$TMP/r1c" symbolic-ref --short HEAD)" == "feat/x" ]]; then
    pass; else fail "behind off main: expected main fast-forwarded in place, got RC=$RC OUT=$OUT"; fi

  # 1d. Behind on main with a local edit the incoming commit would overwrite:
  # git refuses, the hook reports it, and the edit survives.
  mk_origin o1d
  clone_from "$BARE" "$TMP/r1d"
  printf 'local edit\n' >> "$TMP/r1d/f" || die "r1d edit failed"
  commit_push "$SEED" "c2"
  run "$TMP/r1d" "$TMP/s1e"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forward was refused")' >/dev/null 2>&1 \
    && grep -q "local edit" "$TMP/r1d/f" \
    && ! same_commit "$TMP/r1d" main origin/main; then
    pass; else fail "refused fast-forward: expected a report and the edit kept, got RC=$RC OUT=$OUT"; fi

  # 2. up to date -> marker "in sync" status.
  mk_origin o2
  clone_from "$BARE" "$TMP/r2"
  run "$TMP/r2" "$TMP/s2"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("in sync")' >/dev/null 2>&1; then
    pass; else fail "up-to-date: expected in-sync status, got RC=$RC OUT=$OUT"; fi

  # 2b. ahead only -> "ahead ... unpushed", never "in sync" (ahead>0, behind==0).
  #     Clone up to date, add a local commit without pushing; origin unchanged.
  mk_origin o2b
  clone_from "$BARE" "$TMP/r2b"
  g -C "$TMP/r2b" commit -q --allow-empty -m "local ahead"  || die "git commit in r2b failed"
  run "$TMP/r2b" "$TMP/s2b"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("ahead") and (test("in sync") | not)' >/dev/null 2>&1; then
    pass; else fail "ahead-only: expected \"ahead\" (not \"in sync\"), got RC=$RC OUT=$OUT"; fi

  # 3. throttle. Clone up to date. First call fetches, stamps, and reports a
  #    definitive "in sync". Move origin ahead WITHOUT the hook fetching: a call
  #    +60s is throttled, so it does not fetch and reports "not verified" (never
  #    a definitive "in sync"/"behind" against a stale ref). A call past the 1h
  #    window fetches the new commit and fires "behind".
  mk_origin o3
  clone_from "$BARE" "$TMP/r3"
  run "$TMP/r3" "$TMP/s3" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2000000               # fetch, stamp, up to date -> in sync
  # This establishes the throttle stamp the next two assertions depend on, so a
  # failure here must abort, not merely tally (aggregate-reporting carve-out:
  # later checks may not depend on an earlier one merely having incremented FAIL).
  { [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("in sync")' >/dev/null 2>&1; } \
    || die "throttle setup: first call should report in sync, got RC=$RC OUT=$OUT"
  commit_push "$SEED" "c3"                                # origin moves; r3's tracking ref still old
  run "$TMP/r3" "$TMP/s3" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2000060               # +60s: throttled -> no fetch -> not verified
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("not verified") and (test("in sync") | not) and (test("behind") | not)' >/dev/null 2>&1; then
    pass; else fail "throttle active: inside window should report 'not verified' (not 'in sync'/'behind'), got OUT=$OUT"; fi
  run "$TMP/r3" "$TMP/s3" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2003601               # +>1h: fetch -> behind -> fast-forwards
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1; then
    pass; else fail "throttle expired: past window should fetch and fire, got OUT=$OUT"; fi

  # 3b. By default the hook fetches every session: a recent stamp does not stop
  #     it, so origin moving is seen and fast-forwarded, never "not verified".
  mk_origin o3b
  clone_from "$BARE" "$TMP/r3b"
  run "$TMP/r3b" "$TMP/s3b" SYNC_NOW=2000000
  commit_push "$SEED" "c2"
  run "$TMP/r3b" "$TMP/s3b" SYNC_NOW=2000060
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1; then
    pass; else fail "default: expected a fetch every session, got OUT=$OUT"; fi

  # 4. not a repo -> silent no-op.
  mkdir -p "$TMP/notrepo" || die "could not create $TMP/notrepo"
  run "$TMP/notrepo" "$TMP/s4"
  if [[ $RC -eq 0 && -z "$OUT" ]]; then pass; else fail "not a repo: expected silent exit 0, got RC=$RC OUT=$OUT"; fi

  # 5. no origin remote -> silent no-op.
  g init -q -b main "$TMP/noorigin" || die "git init noorigin failed"
  printf 'x\n' > "$TMP/noorigin/f"  || die "write noorigin/f failed"
  g -C "$TMP/noorigin" add f        || die "git add in noorigin failed"
  g -C "$TMP/noorigin" commit -q -m x || die "git commit in noorigin failed"
  run "$TMP/noorigin" "$TMP/s5"
  if [[ $RC -eq 0 && -z "$OUT" ]]; then pass; else fail "no origin: expected silent exit 0, got RC=$RC OUT=$OUT"; fi

  # 6. fetch failure -> "not verified" (broken remote tolerated, no crash, and
  #    never a false "in sync" against a stale ref — sync-before-work). Clone,
  #    then delete the bare origin so the fetch fails; the hook reports
  #    not-verified and exits 0 without crashing.
  mk_origin o6
  clone_from "$BARE" "$TMP/r6"
  rm -rf "$BARE" || die "could not remove $BARE"
  run "$TMP/r6" "$TMP/s6"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("not verified") and (test("in sync") | not)' >/dev/null 2>&1; then
    pass; else fail "fetch failure: expected 'not verified' (not 'in sync') exit 0, got RC=$RC OUT=$OUT"; fi

  # 7. malformed clock -> no-op, exit 0.
  mk_origin o7
  clone_from "$BARE" "$TMP/r7"
  run "$TMP/r7" "$TMP/s7" SYNC_NOW="not-a-number"
  if [[ $RC -eq 0 && -z "$OUT" ]]; then pass; else fail "bad clock: expected silent exit 0, got RC=$RC OUT=$OUT"; fi

  # 8. diverged -> divergence notice (rebase, not fast-forward). Clone, add a
  #    local commit (ahead by 1), and push a different commit to origin (behind
  #    by 1); the hook fetches and reports the diverged state.
  mk_origin o8
  clone_from "$BARE" "$TMP/r8"
  printf 'local\n' >> "$TMP/r8/f"        || die "write r8/f failed"
  g -C "$TMP/r8" add f                    || die "git add in r8 failed"
  g -C "$TMP/r8" commit -q -m local       || die "git commit in r8 failed"   # ahead by 1
  commit_push "$SEED" "c2"                                                    # origin moves -> behind by 1
  run "$TMP/r8" "$TMP/s8"
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Session-start status") and test("diverged") and test("rebase")' >/dev/null 2>&1; then
    pass; else fail "diverged: expected marker divergence notice, got RC=$RC OUT=$OUT"; fi

  # 9. future-version throttle stamp -> not throttled on, and preserved (never
  #    downgraded). Pre-seed a "2 <recent>" record at the stamp path the hook
  #    derives (cksum of the repo toplevel), move origin ahead, and run inside
  #    the window: a v1 stamp would throttle to silence, but the future record
  #    must be ignored (the hook fires) and left untouched.
  mk_origin o9
  clone_from "$BARE" "$TMP/r9"
  commit_push "$SEED" "c2"                                # origin ahead -> a non-throttled run fires
  local top9 key9 stampdir9 sv9
  top9="$(cd "$TMP/r9" && git rev-parse --show-toplevel)" || die "r9 toplevel failed"
  key9="$(printf '%s' "$top9" | cksum | cut -d' ' -f1)"   || die "r9 key derivation failed"
  stampdir9="$TMP/s9"
  mkdir -p "$stampdir9" || die "could not create $stampdir9"
  printf '2 %s\n' 2000000 > "$stampdir9/sync-$key9" || die "could not seed future stamp"
  run "$TMP/r9" "$stampdir9" SYNC_THROTTLE_HOURS=1 SYNC_NOW=2000060            # within window, but future schema
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("fast-forwarded")' >/dev/null 2>&1; then
    pass; else fail "future stamp: expected fire (not throttled), got RC=$RC OUT=$OUT"; fi
  sv9=""; read -r sv9 _ < "$stampdir9/sync-$key9" || sv9=""
  if [[ "$sv9" == "2" ]]; then pass; else fail "future stamp: expected preserved version 2, got '$sv9'"; fi

  # 10. A Herdr worker never fetches: the remote-tracking refs are shared with
  #     the foreman's checkout. Origin moves, the worker session runs, and
  #     refs/remotes/origin/* is byte-for-byte what it was; the notice still
  #     tells the worker not to sync. The foreman in the main checkout then
  #     fetches and sees the new tip.
  mk_origin o10
  clone_from "$BARE" "$TMP/r10"
  git -C "$TMP/r10" worktree add -q "$TMP/r10-wt" -b feat/worker >/dev/null 2>&1 \
    || die "r10 worktree add failed"
  commit_push "$SEED" "c2"
  local refs10_before refs10_after refs10_foreman seed10 tip10
  refs10_before="$(git -C "$TMP/r10" for-each-ref refs/remotes/origin)" || die "r10 for-each-ref (before) failed"
  [[ -n "$refs10_before" ]] || die "r10 has no refs/remotes/origin refs to compare"
  run "$TMP/r10-wt" "$TMP/s10" HERDR_ENV=1
  refs10_after="$(git -C "$TMP/r10" for-each-ref refs/remotes/origin)" || die "r10 for-each-ref (after worker) failed"
  if [[ "$refs10_before" == "$refs10_after" ]]; then
    pass; else fail "worker fetch: refs/remotes/origin changed in a worker session (before=$refs10_before after=$refs10_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Herdr worker session") and test("does not fetch") and test("Do not sync")' >/dev/null 2>&1; then
    pass; else fail "worker fetch: expected the no-fetch worker notice, got RC=$RC OUT=$OUT"; fi
  run "$TMP/r10" "$TMP/s10f" HERDR_ENV=1
  refs10_foreman="$(git -C "$TMP/r10" for-each-ref refs/remotes/origin)" || die "r10 for-each-ref (after foreman) failed"
  seed10="$(git -C "$SEED" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in $SEED"
  tip10="$(git -C "$TMP/r10" rev-parse --verify --quiet 'origin/main^{commit}')" || die "cannot resolve origin/main in r10"
  if [[ $RC -eq 0 && "$refs10_foreman" != "$refs10_before" && "$tip10" == "$seed10" ]]; then
    pass; else fail "foreman fetch: expected the main checkout to fetch the moved origin, got RC=$RC OUT=$OUT"; fi

  # 11. HERDR_ENV set but the role probes fail: the session may be a worker,
  #     so the hook neither fetches nor fast-forwards, and says how to
  #     diagnose. A git shim on PATH fails only the two role probes.
  mk_origin o11
  clone_from "$BARE" "$TMP/r11"
  commit_push "$SEED" "c2"
  local realgit refs11_before refs11_after main11_before main11_after
  realgit="$(command -v git)" || die "cannot locate git"
  mkdir -p "$TMP/shim11" || die "could not create $TMP/shim11"
  # shellcheck disable=SC2016  # the shim's own "$@"/"$a" must stay literal in its source
  printf '#!/usr/bin/env bash\nfor a in "$@"; do case "$a" in --absolute-git-dir|--git-common-dir) echo "shim: role probe refused" >&2; exit 1 ;; esac; done\nexec %q "$@"\n' "$realgit" > "$TMP/shim11/git" \
    || die "could not write the git shim"
  chmod +x "$TMP/shim11/git" || die "could not make the git shim executable"
  refs11_before="$(git -C "$TMP/r11" for-each-ref refs/remotes/origin)" || die "r11 for-each-ref (before) failed"
  [[ -n "$refs11_before" ]] || die "r11 has no refs/remotes/origin refs to compare"
  main11_before="$(git -C "$TMP/r11" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r11"
  run "$TMP/r11" "$TMP/s11" HERDR_ENV=1 PATH="$TMP/shim11:$PATH"
  refs11_after="$(git -C "$TMP/r11" for-each-ref refs/remotes/origin)" || die "r11 for-each-ref (after) failed"
  main11_after="$(git -C "$TMP/r11" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r11 after the run"
  if [[ "$refs11_before" == "$refs11_after" ]]; then
    pass; else fail "role unknown: refs/remotes/origin changed (before=$refs11_before after=$refs11_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("sync not verified") and test("could not tell a Herdr worker") and test("git rev-parse --absolute-git-dir") and (test("fast-forwarded local") | not)' >/dev/null 2>&1 \
    && [[ "$main11_before" == "$main11_after" ]]; then
    pass; else fail "role unknown: expected a no-fetch sync-not-verified notice, got RC=$RC OUT=$OUT"; fi

  # 12. HERDR_ENV set but empty is still a Herdr session (rules/agent-team-operation.md
  #     Two Modes): a linked worktree is a worker and never fetches.
  mk_origin o12
  clone_from "$BARE" "$TMP/r12"
  git -C "$TMP/r12" worktree add -q "$TMP/r12-wt" -b feat/worker >/dev/null 2>&1 \
    || die "r12 worktree add failed"
  commit_push "$SEED" "c2"
  local refs12_before refs12_after
  refs12_before="$(git -C "$TMP/r12" for-each-ref refs/remotes/origin)" || die "r12 for-each-ref (before) failed"
  [[ -n "$refs12_before" ]] || die "r12 has no refs/remotes/origin refs to compare"
  run "$TMP/r12-wt" "$TMP/s12" HERDR_ENV=
  refs12_after="$(git -C "$TMP/r12" for-each-ref refs/remotes/origin)" || die "r12 for-each-ref (after) failed"
  if [[ "$refs12_before" == "$refs12_after" ]]; then
    pass; else fail "empty HERDR_ENV: refs/remotes/origin changed (before=$refs12_before after=$refs12_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("Herdr worker session") and test("Do not sync")' >/dev/null 2>&1; then
    pass; else fail "empty HERDR_ENV: expected the worker notice, got RC=$RC OUT=$OUT"; fi

  # 13. Under tessl the environment is stripped, so a linked worktree may be a
  #     worker's: no fetch, no fast-forward, and a sync-not-verified notice.
  mk_origin o13
  clone_from "$BARE" "$TMP/r13"
  git -C "$TMP/r13" worktree add -q "$TMP/r13-wt" -b feat/maybe-worker >/dev/null 2>&1 \
    || die "r13 worktree add failed"
  commit_push "$SEED" "c2"
  local refs13_before refs13_after main13_before main13_after
  refs13_before="$(git -C "$TMP/r13" for-each-ref refs/remotes/origin)" || die "r13 for-each-ref (before) failed"
  [[ -n "$refs13_before" ]] || die "r13 has no refs/remotes/origin refs to compare"
  main13_before="$(git -C "$TMP/r13" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r13"
  run "$TMP/r13-wt" "$TMP/s13" SESSION_START_MODE=portable
  refs13_after="$(git -C "$TMP/r13" for-each-ref refs/remotes/origin)" || die "r13 for-each-ref (after) failed"
  main13_after="$(git -C "$TMP/r13" rev-parse --verify --quiet 'main^{commit}')" || die "cannot resolve main in r13 after the run"
  if [[ "$refs13_before" == "$refs13_after" && "$main13_before" == "$main13_after" ]]; then
    pass; else fail "portable worktree: refs or main changed (refs before=$refs13_before after=$refs13_after)"; fi
  if [[ $RC -eq 0 ]] && printf '%s' "$OUT" | jq -e '.additionalContext | test("sync not verified") and test("tessl") and test("Herdr worker")' >/dev/null 2>&1; then
    pass; else fail "portable worktree: expected a sync-not-verified notice, got RC=$RC OUT=$OUT"; fi

  echo "─────────────────────────────────────────────" >&2
  if [[ $FAIL -gt 0 ]]; then echo "FAILED: ${FAIL} failed, ${PASS} passed" >&2; exit 1; fi
  echo "PASSED: all ${PASS} checks" >&2
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi

README.md

tile.json