CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/coding-policy

General-purpose coding policy for Baruch's AI agents

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Medium

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

check-git-sync.shhooks/

#!/usr/bin/env bash
# Sync the local default branch with origin at session start, or report why not.
#
# A SessionStart hook implementing rules/sync-before-work.md as a deterministic
# check: it fetches origin (throttled), and if the local default branch trails
# origin/<default>, injects a short "sync before working" notice via
# additionalContext. The rule says fetch + sync before reading/editing, but
# relied on the agent remembering; we hit stale-checkout ("main is behind")
# repeatedly. This surfaces it the moment a session opens.
#
# Design choices:
#   - It DOES something (fetches + compares refs), it does not re-state a rule.
#   - SessionStart fires once per session, not per turn — no per-turn tax.
#   - Fetches every session by default: an unverified answer would only hand the
#     agent the fetch this hook can run in seconds. SYNC_THROTTLE_HOURS (default 0)
#     can still space fetches out: the fetch then runs at most once per window
#     per repo, so rapid session churn doesn't hammer the network. Without a
#     fresh fetch this session the remote-tracking ref may be stale, so a
#     throttled (or failed) fetch reports "sync not verified" rather than a
#     definitive conclusion (rules/sync-before-work.md).
#   - The fetch is time-bounded (timeout/gtimeout, else git's HTTP low-speed and ssh
#     connect/keepalive limits) so a hung network can't
#     stall session start.
#   - Acts when the answer is mechanical: a default branch strictly behind
#     origin after a fresh fetch is fast-forwarded (git refuses every unsafe
#     case); diverged and unverified sessions are reported only.
#   - A Herdr worker session (HERDR_ENV set, any value, linked worktree) never fetches: the
#     fetch writes remote-tracking refs shared with the foreman's checkout. It
#     reports drift from the refs as last fetched. A failed role probe (HERDR_ENV
#     set or portable mode) or a portable linked worktree may be a worker: no
#     fetch, and a sync-unverified notice naming the cause.
#   - Never blocks (always exits 0), never exits 2.
#
# Contract:
#   stdin : consensus SessionStart JSON — not read (the script needs none of it).
#   stdout: once the repo's default branch is resolved, one JSON object
#           {"additionalContext": "<status>"} whose text begins with the
#           "Session-start status — " marker (rules/hook-action-reporting.md) —
#           reporting in-sync, ahead, fast-forwarded, behind (fast-forward refused), or
#           diverged after a fresh fetch,
#           "sync not verified" when the fetch was throttled or failed, or,
#           for a Herdr worker session, the unfetched drift with a do-not-sync note.
#   exit  : always 0. Every best-effort failure emits an actionable stderr warning
#           and continues/no-ops (rules/error-handling.md Shell Error Handling).
#           Non-repo, no origin, and no local default branch are silent no-ops —
#           the sync check does not apply, so those sessions stay quiet.
#   state : $SYNC_STATE_DIR/sync-<repo-key> (default ${TMPDIR:-/tmp}/coding-policy-sync),
#           a per-repo throttle stamp (keyed by toplevel path). Schema documented
#           in hooks/state-schema.md: one line "<schema_version> <checked_at>".
#   env   : SYNC_THROTTLE_HOURS (default 0 = every session), SYNC_FETCH_TIMEOUT (default 10s),
#           SYNC_STATE_DIR (tests), SYNC_NOW (test-only injected clock; defaults
#           to `date +%s`).
set -euo pipefail

warn() { printf 'check-git-sync: %s\n' "$1" >&2; }

# Does a ref exist? `git show-ref --verify --quiet` exits 0 (exists) or 1 (the
# expected "absent / malformed name" no-result); any other exit is a real git
# failure — surface it and treat the ref as absent so best-effort work continues
# visibly (rules/error-handling.md — distinguish a non-result from a tool error).
ref_exists() { # <fully-qualified-ref>
  local rc=0
  git show-ref --verify --quiet "$1" || rc=$?
  if (( rc == 0 )); then return 0; fi
  if (( rc != 1 )); then
    warn "git show-ref failed (exit ${rc}) checking $1 — treating the ref as absent"
  fi
  return 1
}

# Emit the sync notice as additionalContext JSON. jq is required only here; its
# absence is an expected environment condition, not a failure.
emit_notice() { # <notice-text>
  # jq first, then python3; printed only once a tool produced it.
  local out
  if command -v jq >/dev/null 2>&1 && out="$(jq -n --arg c "$1" '{additionalContext: $c}')"; then
    printf '%s\n' "$out"
    return 0
  fi
  if command -v python3 >/dev/null 2>&1 && out="$(python3 -c 'import json, sys; print(json.dumps({"additionalContext": sys.argv[1]}))' "$1")"; then
    printf '%s\n' "$out"
    return 0
  fi
  warn "neither jq nor python3 could encode the sync notice — install one of them; it was: ${1}"
  return 0
}

# Is this session a Herdr WORKER rather than the foreman?
#
# The team rules reserve the shared checkout and every worktree operation for
# the foreman: a worker "runs no git command against the shared checkout,
# mutating or otherwise" and "never creates, moves, or removes a worktree"
# (skills/herdr-foreman/references/team-operation.md Writers and Checkouts). A hook that tells a
# worker to fast-forward `main` or remove a worktree is instructing it to
# break that rule -- which is exactly what happened in a live round, where the
# worker reported the contradiction and then obeyed the hook.
#
# Herdr exports no foreman/worker flag, so the role is derived from where the
# session sits: the foreman works in the shared checkout, every worker works in a
# linked worktree. In a linked worktree `--git-dir` and `--git-common-dir`
# resolve differently; in the main checkout they are the same.
#
# HERDR_ENV counts when set at all, empty included (rules/agent-team-operation.md
# Two Modes). Under tessl (SESSION_START_MODE=portable) the environment is
# stripped, so an unset HERDR_ENV proves nothing there and a linked worktree
# may be a worker's.
#
# Tri-state, so an indeterminate session never reads as "the foreman":
#   0 = a Herdr worker (HERDR_ENV set, linked worktree),
#   1 = the foreman or a standalone agent (HERDR_ENV unset outside portable
#       mode, or a proven main checkout) — the only sessions that may fetch,
#   2 = unknown — a failed role probe with HERDR_ENV set or in portable mode,
#       or a portable linked worktree; treated as a possible worker: no fetch,
#       no fast-forward. ROLE_WHY names the cause for the notice.
herdr_role() {
  ROLE_WHY=""
  local portable=0
  if [[ -z "${HERDR_ENV+x}" ]]; then
    [[ "${SESSION_START_MODE:-}" == portable ]] || return 1
    portable=1
  fi

  local git_dir common_dir rc=0
  git_dir="$(git rev-parse --absolute-git-dir 2>&1)" || rc=$?
  if (( rc != 0 )); then
    warn "git rev-parse --absolute-git-dir failed (exit ${rc}: ${git_dir//$'\n'/ }) — cannot tell a Herdr worker from the foreman; not fetching"
    ROLE_WHY="this hook could not tell a Herdr worker from the foreman (see the warning above). Run \`git rev-parse --absolute-git-dir --path-format=absolute --git-common-dir\` to diagnose."
    return 2
  fi
  rc=0
  common_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>&1)" || rc=$?
  if (( rc != 0 )); then
    warn "git rev-parse --git-common-dir failed (exit ${rc}: ${common_dir//$'\n'/ }) — cannot tell a Herdr worker from the foreman; not fetching"
    ROLE_WHY="this hook could not tell a Herdr worker from the foreman (see the warning above). Run \`git rev-parse --absolute-git-dir --path-format=absolute --git-common-dir\` to diagnose."
    return 2
  fi

  [[ "$git_dir" != "$common_dir" ]] || return 1
  if (( portable )); then
    ROLE_WHY="this agent runs SessionStart through tessl, which hides the session's environment, and this linked worktree may be a Herdr worker's."
    return 2
  fi
  return 0
}

# Count the local default branch's drift from its remote-tracking ref, as the
# refs stand now (no fetch). --left-right --count on a three-dot range yields
# "<ahead>\t<behind>" — ahead = local-only commits, behind = origin-only
# commits — so a diverged branch (both > 0) can be distinguished from one that
# is strictly behind (fast-forwardable). Sets DRIFT_AHEAD / DRIFT_BEHIND and
# returns 0; a missing origin ref or a rev-list failure is warned and returns 1,
# never swallowed as "up to date".
drift_counts() { # <default-branch>
  local db="$1" counts
  if ! counts="$(git rev-list --left-right --count "refs/heads/${db}...refs/remotes/origin/${db}" 2>/dev/null)"; then
    warn "could not compare ${db} against origin/${db} — run \`git status\` to inspect; skipping sync check"
    return 1
  fi
  DRIFT_AHEAD="${counts%%[[:space:]]*}"
  DRIFT_BEHIND="${counts##*[[:space:]]}"
  if ! [[ "$DRIFT_AHEAD" =~ ^[0-9]+$ && "$DRIFT_BEHIND" =~ ^[0-9]+$ ]]; then
    warn "unexpected ahead/behind counts '${counts}' for ${db} — run \`git status\` to inspect; skipping sync check"
    return 1
  fi
  return 0
}

main() {
  local THROTTLE_HOURS="${SYNC_THROTTLE_HOURS:-0}"
  local FETCH_TIMEOUT="${SYNC_FETCH_TIMEOUT:-10}"
  local STATE_DIR="${SYNC_STATE_DIR:-${TMPDIR:-/tmp}/coding-policy-sync}"
  local rc db inside cand now top repo_key stamp sv ts should_fetch preserve_future fetch_failed ahead behind role ROLE_WHY
  local -a fetch

  # git is required to produce a signal; its absence is an expected environment
  # condition, not a failure — warn and no-op.
  command -v git >/dev/null 2>&1 || { warn "git not found — install git to enable the sync check"; return 0; }

  # Outside a work tree there is nothing to sync. `--is-inside-work-tree` prints
  # true/false and exits 0 inside any repo; its only failure is exit 128 ("not a
  # git repository") — the expected non-repo session. Proceed only on a literal
  # "true"; a bare repo / gitdir ("false") and the non-repo case are silent
  # no-ops, and an unexpected value is surfaced.
  inside="$(git rev-parse --is-inside-work-tree 2>/dev/null)" || inside="__notrepo__"
  case "$inside" in
    true) : ;;
    false|__notrepo__) return 0 ;;
    *) warn "unexpected \`git rev-parse --is-inside-work-tree\` output '${inside}' — skipping sync check"; return 0 ;;
  esac

  # No origin remote => nothing to compare against. `git remote get-url` exits 2
  # for the documented "No such remote" (silent no-op); any other non-zero is a
  # real git failure and is surfaced (rules/error-handling.md).
  rc=0
  git remote get-url origin >/dev/null 2>&1 || rc=$?
  if (( rc != 0 )); then
    if (( rc == 2 )); then return 0; fi
    warn "git remote get-url origin failed (exit ${rc}) — run \`git remote -v\` to inspect; skipping sync check"
    return 0
  fi

  if ! [[ "$THROTTLE_HOURS" =~ ^[0-9]+$ ]]; then
    warn "SYNC_THROTTLE_HOURS='${THROTTLE_HOURS}' is not an integer — using 0 (fetch every session)"
    THROTTLE_HOURS=0
  fi

  # Resolve the remote default branch. Primary path: origin/HEAD's symbolic ref
  # (set at clone time). `git symbolic-ref --quiet` exits 0 when resolved and 1
  # for the expected no-symref case (origin/HEAD absent or not symbolic); any
  # other exit is a real git failure and is surfaced, not swallowed as "no
  # default" (rules/error-handling.md — distinguish an expected non-result from a
  # tool failure).
  db=""
  if db="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)"; then
    db="${db#origin/}"
  else
    rc=$?
    db=""
    if (( rc != 1 )); then
      warn "git symbolic-ref failed (exit ${rc}) resolving origin/HEAD — falling back to name probe"
    fi
  fi
  # Fallback: probe the conventional names among the remote-tracking refs we have.
  if [[ -z "$db" ]]; then
    for cand in main master; do
      if ref_exists "refs/remotes/origin/$cand"; then db="$cand"; break; fi
    done
  fi
  if [[ -z "$db" ]]; then
    warn "cannot determine origin's default branch — set it with \`git remote set-head origin --auto\`; skipping sync check"
    return 0
  fi

  # No local default branch (e.g. only feature branches checked out) => nothing to
  # report as "behind". Silent no-op when absent; ref_exists surfaces a real
  # git failure before returning "absent".
  ref_exists "refs/heads/$db" || return 0

  # A worker never touches the shared checkout, and a fetch writes the
  # remote-tracking refs that checkout shares — so a worker session neither
  # fetches nor stamps the throttle. It reports the drift from the refs as last
  # fetched and names whose job syncing is.
  role=0
  herdr_role || role=$?
  if (( role == 2 )); then
    emit_notice "Session-start status — git: \`${db}\` sync not verified and not fetched or fast-forwarded here — ${ROLE_WHY} If this is a Herdr worker session, do not sync the shared checkout (skills/herdr-foreman/references/team-operation.md Writers and Checkouts); if it is the foreman, run \`git fetch origin\`, then \`git status\` (rules/sync-before-work.md)."
    return 0
  fi
  if (( role == 0 )); then
    drift_counts "$db" || return 0
    emit_notice "Session-start status — git: local \`${db}\` is ${DRIFT_BEHIND} behind / ${DRIFT_AHEAD} ahead of \`origin/${db}\` as last fetched (a Herdr worker session does not fetch). This is a Herdr worker session: the shared checkout is the foreman's (skills/herdr-foreman/references/team-operation.md Writers and Checkouts). Do not sync it — work in this worktree and report the drift."
    return 0
  fi

  # Resolve the clock. A test may inject SYNC_NOW; otherwise read the system clock
  # and handle its failure. Validate as an integer before any arithmetic so a
  # malformed value can't abort the hook under set -e.
  if [[ -n "${SYNC_NOW:-}" ]]; then
    now="$SYNC_NOW"
  elif ! now="$(date +%s)"; then
    warn "cannot read the system clock — skipping sync check"
    return 0
  fi
  if ! [[ "$now" =~ ^[0-9]+$ ]]; then
    warn "clock value '${now}' is not an integer — unset SYNC_NOW; skipping sync check"
    return 0
  fi

  # Per-repo throttle stamp, keyed by the toplevel path so sibling clones throttle
  # independently. cksum gives a stable filename-safe key for an arbitrary path.
  if ! top="$(git rev-parse --show-toplevel 2>/dev/null)"; then
    warn "git rev-parse --show-toplevel failed — keying the throttle stamp on \$PWD instead"
    top="$PWD"
  fi
  # A cksum/cut failure would abort the assignment under set -e and break the
  # always-exit-0 contract, so handle it: fall back to an un-throttled run.
  if ! repo_key="$(printf '%s' "$top" | cksum | cut -d' ' -f1)"; then
    warn "could not derive a throttle key for ${top} — sync check will not throttle this run"
    repo_key=""
  fi
  stamp="${STATE_DIR}/sync-${repo_key}"

  # Throttle stamp schema (see hooks/state-schema.md): one line "<schema_version>
  # <checked_at-epoch>". Per rules/stateful-artifacts.md Migration Policy:
  #   - schema_version 1 within the window => throttle (skip the fetch).
  #   - a future version (sv > 1) => this hook is lagging: no usable prior state
  #     (fetch), and DO NOT downgrade the record — preserve it (preserve_future).
  #   - anything else (old bare-integer, corrupt, absent) => no prior state
  #     (fetch), safe to rewrite as version 1.
  # With no throttle key the fallback fetches unconditionally (never throttles).
  should_fetch=1
  preserve_future=0
  fetch_failed=0
  if [[ -n "$repo_key" && -r "$stamp" ]]; then
    sv=""; ts=""
    read -r sv ts < "$stamp" || { sv=""; ts=""; }
    if [[ "$sv" =~ ^[0-9]+$ ]] && (( sv > 1 )); then
      preserve_future=1
    elif [[ "$sv" == "1" && "$ts" =~ ^[0-9]+$ ]] && (( now - ts < THROTTLE_HOURS * 3600 )); then
      should_fetch=0
    fi
  fi

  if (( should_fetch )); then
    # Record the fetch up front so a slow/failed fetch still throttles the next
    # session rather than retrying the network on every start. Skipped when no
    # throttle key was derived, and when a future-version record must be
    # preserved rather than downgraded.
    if [[ -n "$repo_key" ]] && (( preserve_future == 0 )); then
      if mkdir -p "$STATE_DIR"; then
        printf '1 %s\n' "$now" > "$stamp" ||
          warn "cannot write throttle stamp ${stamp} — check permissions on ${STATE_DIR}; will re-fetch next session"
      else
        warn "cannot create state dir ${STATE_DIR} — check permissions or set SYNC_STATE_DIR; sync check will not throttle"
      fi
    fi

    # Repo hooks are off for the fetch: updating refs runs
    # `reference-transaction`, and session start must never run repo code.
    # Time-bound the fetch so a hung network can't stall session start:
    # timeout/gtimeout when present, else git's own HTTP low-speed limit and ssh
    # connect/keepalive timeouts.
    # Zero or a non-number would switch the bound off; fall back to the default.
    [[ "$FETCH_TIMEOUT" =~ ^[1-9][0-9]*$ ]] || FETCH_TIMEOUT=10
    if command -v timeout >/dev/null 2>&1; then
      fetch=(timeout "$FETCH_TIMEOUT" git -c core.hooksPath=/dev/null fetch --quiet origin)
    elif command -v gtimeout >/dev/null 2>&1; then
      fetch=(gtimeout "$FETCH_TIMEOUT" git -c core.hooksPath=/dev/null fetch --quiet origin)
    else
      fetch=(env "GIT_SSH_COMMAND=${GIT_SSH_COMMAND:-ssh} -o ConnectTimeout=${FETCH_TIMEOUT} -o ServerAliveInterval=5 -o ServerAliveCountMax=2"
        git -c core.hooksPath=/dev/null -c http.lowSpeedLimit=1000 -c "http.lowSpeedTime=${FETCH_TIMEOUT}" fetch --quiet origin)
    fi
    # A fetch failure (offline, auth, timeout) is a no-op, not a broken session —
    # warn and let the unverified-sync branch below report it. Comparing against
    # a remote-tracking ref no fetch refreshed would risk a false "in sync"
    # (rules/sync-before-work.md Staleness Poisons Conclusions).
    "${fetch[@]}" 2>/dev/null || {
      warn "git fetch origin failed or timed out — check connectivity; cannot verify sync against a current origin/${db}"
      fetch_failed=1
    }
  fi

  # A definitive sync conclusion requires a fresh fetch this session. A throttled
  # or failed fetch leaves the remote-tracking ref possibly stale, and stale
  # state poisons conclusions (rules/sync-before-work.md). Report unverified
  # rather than a false "in sync".
  if (( should_fetch == 0 || fetch_failed )); then
    emit_notice "Session-start status — git: \`${db}\` sync not verified against a current \`origin/${db}\` this session — run \`git fetch origin\`, then \`git status\` (rules/sync-before-work.md)."
    return 0
  fi

  drift_counts "$db" || return 0
  ahead="${DRIFT_AHEAD}"
  behind="${DRIFT_BEHIND}"

  # Success path: nothing to pull from origin. Distinguish truly in-sync from
  # ahead-only — a branch ahead of origin (unpushed commits) is not "in sync".
  if (( behind == 0 )); then
    if (( ahead == 0 )); then
      emit_notice "Session-start status — git: local \`${db}\` is in sync with \`origin/${db}\`"
    else
      emit_notice "Session-start status — git: local \`${db}\` is ${ahead} commit(s) ahead of \`origin/${db}\` (unpushed), none behind"
    fi
    return 0
  fi

  # Diverged needs judgment (which side wins), so it stays a report.
  if (( ahead > 0 )); then
    emit_notice "Session-start status — Local \`${db}\` has diverged from \`origin/${db}\` (${behind} behind, ${ahead} ahead) — reconcile before working (rules/sync-before-work.md): \`git fetch origin\`, then rebase \`${db}\` onto \`origin/${db}\` (a fast-forward won't apply)."
    return 0
  fi

  # Under tessl (hooks/session-start.sh portable mode) the environment is
  # stripped and a Herdr worker cannot be ruled out: report, never write.
  if [[ "${SESSION_START_MODE:-}" == portable ]]; then
    emit_notice "Session-start status — Local \`${db}\` is ${behind} commit(s) behind \`origin/${db}\`; not fast-forwarded here (this agent runs SessionStart through tessl, which hides the session's environment) — fast-forward \`${db}\` if this is not a Herdr worker session (rules/sync-before-work.md)."
    return 0
  fi

  # Strictly behind after a fresh fetch: sync it rather than asking someone to.
  if fast_forward "$db"; then
    emit_notice "Session-start status — git: fast-forwarded local \`${db}\` by ${behind} commit(s) to \`origin/${db}\`"
  else
    emit_notice "Session-start status — Local \`${db}\` is ${behind} commit(s) behind \`origin/${db}\` and the automatic fast-forward was refused (see the warning above) — sync before working (rules/sync-before-work.md): fast-forward \`${db}\` to \`origin/${db}\`."
  fi
  return 0
}

# Fast-forward the local default branch to origin's, never anything else.
#
# Both paths let git refuse every unsafe case: `merge --ff-only` on the checked-out
# branch refuses a non-fast-forward and any change that would overwrite local
# work; `fetch . <src>:<dst>` for a branch not checked out here only
# fast-forwards and refuses a branch checked out in any worktree. 0 = moved,
# 1 = refused (warned).
#
# Repo hooks are disabled for both (`core.hooksPath=/dev/null`): a merge runs
# `post-merge`, and session start must not become a path that runs repo code.
fast_forward() { # <default-branch>
  local db="$1" current="" out rc=0
  # `symbolic-ref --quiet` exits 1 for a detached HEAD, the one expected
  # non-result; any other failure is a git error and refuses the fast-forward.
  current="$(git symbolic-ref --quiet --short HEAD)" || rc=$?
  if (( rc > 1 )); then
    warn "git symbolic-ref HEAD failed (exit ${rc}) — cannot tell which branch is checked out, so ${db} was not fast-forwarded"
    return 1
  fi
  rc=0
  if [[ "$current" == "$db" ]]; then
    out="$(git -c core.hooksPath=/dev/null merge --ff-only --quiet "refs/remotes/origin/${db}" 2>&1)" || rc=$?
  else
    out="$(git -c core.hooksPath=/dev/null fetch --quiet . "refs/remotes/origin/${db}:refs/heads/${db}" 2>&1)" || rc=$?
  fi
  if (( rc != 0 )); then
    warn "fast-forward of ${db} refused (exit ${rc}): ${out//$'\n'/ }"
    return 1
  fi
  return 0
}

# Entry-point guard (rules/file-hygiene.md Standalone Scripts): run only when
# executed, so the script can also be sourced to unit-test its functions.
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi

README.md

tile.json