#!/bin/bash
# housebroken - one command name for the gate scripts.
#
# The skill file, and any future adapter for another agent host, must be able
# to say `housebroken prior-art owner/repo term` without knowing where the
# scripts live. This dispatcher is that stable name: it finds the scripts
# directory, maps a short subcommand to a script, and hands over every
# remaining argument unchanged. Nothing else. No gate logic lives here.
#
# Usage:
#   housebroken <subcommand> [args...]
#   housebroken help
#   housebroken version
#
# Environment:
#   HOUSEBROKEN_SCRIPTS  directory holding the *.sh gate scripts. When
#                        unset, the scripts directory next to this file
#                        (../scripts), then this file's own directory, then
#                        $HOUSEBROKEN_HOME/scripts are tried in that order.
#   HOUSEBROKEN_HOME     workshop root, default $HOME/.housebroken. The gate
#                        scripts read it too; it is passed through untouched.
#
# Assumes bash. The scripts themselves assume gh and jq.
set -u

self_dir=$(cd "$(dirname "$0")" && pwd)
HOUSEBROKEN_HOME="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"

candidates=()
if [ -n "${HOUSEBROKEN_SCRIPTS:-}" ]; then
  candidates+=("$HOUSEBROKEN_SCRIPTS")
fi
candidates+=("$self_dir/../scripts" "$self_dir" "$HOUSEBROKEN_HOME/scripts")

scripts_dir=""
for d in "${candidates[@]}"; do
  if [ -f "$d/prior-art.sh" ]; then
    scripts_dir=$(cd "$d" && pwd)
    break
  fi
done

door() {
  cat <<'EOF'
usage: housebroken <subcommand> [args...]

  1  policy      read the repository's AI-contribution policy where it lives
  1  outside     count outside contributors merged in the last 120 days
  2  prior-art   issues and pull requests on the touched files, rulings quoted
  4  proof       run the test on the base and the head, and stamp red then green
  6  census      added code lines against added comment lines, per branch
  6  defaults    default arguments and wrappers added to keep call sites untouched
  6  comments    refuse a comment added to code nobody asked for
  6  claims      every falsifiable claim in outbound text, listed to re-derive
  6  refs        every issue, link, test, file and commit the text names, looked up
  9  branch      the mechanical facts about the branch: tree, modes, artifacts, claims
  9  declare     what was not tested and what was assumed, for the review to attack
  9  review      a different model attacks the change before it is filed
  9  file        the only way a pull request gets filed
  9  issue       the only way an issue gets filed
 10  verify      the filed pull request is what was meant, and CI settled
 10  sweep       every open pull request where the ball is in your court
 10  inbox       what GitHub has to tell you since you last looked, bodies included
 10  notes       what this repository's maintainers have asked for before
 10  reply       the only way an answer on a thread gets posted
 10  push        the only way a rework reaches a filed pull request's branch
 11  hygiene     delete fork branches after merge or close, list orphan forks

  install-skill  put the skill where Claude Code loads it (pip, pipx and uv installs)
  skill-path     print where the packaged skill is (pip, pipx and uv installs)
  install-hook   a Claude Code hook that refuses GitHub writes outside these gates
  help           this list
  version        the installed housebroken version

Step numbers are the steps of "The rules" in the README. Every argument after
the subcommand is passed to the script unchanged.
EOF
}

version() {
  vpaths=("$self_dir/../VERSION" "$self_dir/VERSION")
  if [ -n "$scripts_dir" ]; then
    vpaths=("$scripts_dir/VERSION" "$scripts_dir/../VERSION" "${vpaths[@]}")
  fi
  for v in "${vpaths[@]}"; do
    if [ -f "$v" ]; then
      head -1 "$v"
      return 0
    fi
  done
  echo "housebroken (unversioned)"
}

cmd="${1:-help}"
case "$cmd" in
  help|--help|-h) door; exit 0 ;;
  version|--version) version; exit 0 ;;
esac
shift

case "$cmd" in
  policy)    script=check-ai-policy.sh ;;
  outside)   script=distinct-outside.sh ;;
  prior-art) script=prior-art.sh ;;
  census)    script=comment-census.sh ;;
  defaults)  script=diff-defaults.sh ;;
  comments)  script=comment-check.sh ;;
  claims)    script=claim-check.sh ;;
  refs)      script=ref-check.sh ;;
  proof)     script=proof.sh ;;
  declare)   script=declare.sh ;;
  branch)    script=branch-check.sh ;;
  review)    script=review.sh ;;
  file)      script=file-pr.sh ;;
  issue)     script=file-issue.sh ;;
  verify)    script=verify-filed-pr.sh ;;
  sweep)     script=pr-sweep.sh ;;
  inbox)     script=inbox.sh ;;
  notes)     script=notes.sh ;;
  hygiene)   script=fork-hygiene.sh ;;
  reply)     script=reply.sh ;;
  push)      script=push.sh ;;
  install-hook) script=gate-hook.sh; set -- --install ;;
  *)
    echo "housebroken: unknown subcommand: $cmd (try: housebroken help)" >&2
    exit 2
    ;;
esac

if [ -z "$scripts_dir" ] || [ ! -f "$scripts_dir/$script" ]; then
  {
    echo "housebroken: cannot find $script. Looked in:"
    for d in "${candidates[@]}"; do echo "  $d"; done
    echo "Set HOUSEBROKEN_SCRIPTS to the directory holding the gate scripts."
  } >&2
  exit 2
fi

# The skill and the hook are copies outside the package, so an upgrade moves the
# gates and leaves what the agent was told, and the hook that holds it, behind.
stale() { # installed-copy packaged-copy command
  if [ -f "$1" ] && [ -f "$2" ] && ! cmp -s "$1" "$2"; then
    echo "note: $1 is not this version's; run $3" >&2
  fi
}
for packaged in "$scripts_dir/../skill/SKILL.md" "$scripts_dir/../skills/housebroken/SKILL.md"; do
  stale "$HOME/.claude/skills/housebroken/SKILL.md" "$packaged" "housebroken install-skill"
done
[ "$cmd" = "install-hook" ] || stale "$HOME/.claude/hooks/housebroken-gate.sh" "$scripts_dir/gate-hook.sh" "housebroken install-hook"

export HOUSEBROKEN_HOME
exec bash "$scripts_dir/$script" "$@"
