#!/usr/bin/env bash
# ticket
# Dispatcher for the event-sourced ticket system.
# Routes subcommands to their implementation scripts.
#
# Usage: ticket <subcommand> [args...]
#   ticket init                                   — initialize ticket system (orphan branch + worktree)
#   ticket create <type> <title> [--parent <id>]  — create a new ticket
#   ticket show <ticket_id>                       — show compiled ticket state as JSON
#   ticket list                                   — list all tickets as a JSON array
#   ticket transition <ticket_id> <current> <target> — transition status with optimistic concurrency
#                                                     (requires current_status to match actual state;
#                                                      exits non-zero if state has changed since read)
#   ticket comment <ticket_id> <body>             — append a comment to a ticket
#   ticket link <id1> <id2> <relation>            — link two tickets (relation REQUIRED:
#                                                     blocks|depends_on|relates_to|duplicates|supersedes|discovered_from)
#   ticket unlink <source> <target>               — remove ONE link between an ordered pair (no relation arg;
#                                                     removes the most-recent link; call repeatedly for multiple)
#   ticket deps <ticket_id>                       — show dependency graph for a ticket (JSON)
#   ticket bridge-status [--format=json]          — show last bridge run status
#   ticket bridge-fsck [--tickets-tracker=<path>] — audit bridge mappings (orphans, duplicates, stale SYNCs)
#   ticket tag <ticket_id> <tag>                  — add a tag to a ticket
#   ticket untag <ticket_id> <tag>                — remove a tag from a ticket
#   ticket summary <id> [<id> ...]                — one-line ticket summary with blocking status
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Shared cross-environment reconvergence helper (_reconverge_tickets). Self-
# contained; see ticket-sync.sh for the Concurrency Doctrine (§0) rationale.
# shellcheck source=/dev/null
source "$SCRIPT_DIR/ticket-sync.sh"

_print_overview() {
    echo "Usage: rebar <subcommand> [args...]"
    echo ""
    echo "Run 'rebar <subcommand> --help' for usage of a specific subcommand."
    echo ""
    echo "Subcommands:"
    echo "  init        Initialize the ticket system"
    echo "  create      Create a new ticket"
    echo "  show        Show ticket details"
    echo "  list        List all tickets as JSON"
    echo "  transition  Transition ticket status (optimistic concurrency)"
    echo "  claim       Atomically claim an open ticket (-> in_progress + assignee; exit 10 if taken)"
    echo "  comment     Add a comment to a ticket"
    echo "  link        Link two tickets (relation REQUIRED: blocks|depends_on|relates_to|duplicates|supersedes|discovered_from)"
    echo "  unlink      Remove ONE link between an ordered <source> <target> pair (no relation arg; most-recent first)"
    echo "  deps        Show dependency graph for a ticket (<ticket_id>)"
    echo "  edit          Edit ticket fields (--title, --priority, --assignee, --ticket_type, --description, --tags)"
    echo "  tag           Add a tag to a ticket"
    echo "  untag         Remove a tag from a ticket"
    echo "  validate      Repo-wide tracker health check (NO ticket id; scores the whole store 1-5, exit 0-4; --json, --terse, --verbose, --fix)"
    echo "  clarity-check Score one ticket's clarity (<ticket_id>; exit 0=pass, 1=fail; JSON: score/verdict/threshold)"
    echo "  check-ac      Check one ticket has an Acceptance Criteria block (<ticket_id>; exit 0=pass, 1=fail; AC_CHECK: pass|fail)"
    echo "  quality-check Check one ticket's dispatch readiness (<ticket_id>; exit 0=pass, 1=fail; QUALITY: pass|fail)"
    echo "  purge-bridge  Remove non-project inbound bridge tickets (--keep=<KEY> [--dry-run])"
    echo "  summary       One-line ticket summary with blocking status for one or more IDs"
    echo "  bridge-status Show last bridge run status [--format=json]"
    echo "  bridge-fsck   Audit bridge mappings (orphans, duplicates, stale SYNCs)"
    echo "  exists        O(1) presence check (exit 0=exists, 1=not found)"
    echo "  list-epics    List open epics [--all] [--has-tag=TAG] [--min-children=N]"
    echo "  list-descendants  BFS walk from a root ticket, bucketed by type (JSON)"
    echo "  next-batch    Select next parallel agent batch for an epic [--json]"
    echo "  ready         List tickets ready to work (all blockers closed)"
    echo "  search        Full-text search over titles/descriptions/comments/tags (JSON)"
    echo "  archive       Archive an open ticket (excludes from default list; idempotent)"
    echo "  set-file-impact  Record file impact for a ticket (JSON array of {path,reason} objects)"
    echo "  get-file-impact  Get the current file impact array for a ticket (JSON)"
    echo "  set-verify-commands  Record DD-level verify commands (JSON array of {dd_id,dd_text,command})"
    echo "  get-verify-commands  Get the current verify commands array for a ticket (JSON)"
    echo "  scratch          Manage per-ticket scratch values (set/get/clear)"
}

# Misuse (no/unknown subcommand): overview + non-zero exit (preserves prior behavior).
_usage() { _print_overview; exit 1; }
# Explicit help request: overview to stdout, success exit.
_help_overview() { _print_overview; exit 0; }

# Per-subcommand usage, printed on `rebar <sub> --help` / `rebar help <sub>`.
# Mirrors each subcommand's own Usage: text. Printed to stdout; exits 0. The
# command itself is NOT executed (no _ensure_initialized, no side effects).
_print_subcommand_help() {
    local sub="$1"
    case "$sub" in
        init)         echo "Usage: rebar init [--silent]" ;;
        create)       echo "Usage: rebar create <bug|epic|story|task> <title> [--parent <id>] [--priority <0-4>] [--assignee <name>] [--description <text>] [--tags <t1,t2>]" ;;
        show)         echo "Usage: rebar show [--format=llm] <ticket_id> [<ticket_id> ...]" ;;
        list)         echo "Usage: rebar list [--status=<s>] [--type=<t>] [--priority=<n>] [--parent=<id>] [--has-tag=<tag>] [--without-tag=<tag>] [--include-archived] [--exclude-deleted] [--format=llm]" ;;
        transition)
            echo "Usage: rebar transition <ticket_id> <current_status> <target_status> [--reason=<text>] [--force] [--verdict-hash=<hash>] [--force-close=<reason>]"
            echo "       rebar transition <ticket_id> <target_status>   (auto-detects current status)"
            echo "  status: open | in_progress | closed | blocked"
            echo "  bug close requires --reason (prefix 'Fixed:' or 'Escalated to user:'); story/epic close requires --verdict-hash (or --force-close=<reason>)" ;;
        claim)        echo "Usage: rebar claim <ticket_id> [--assignee=<name>]   (atomic open -> in_progress; exit 10 if already claimed)" ;;
        reopen)       echo "Usage: rebar reopen <ticket_id>   (closed -> open; exit 10 if not currently closed)" ;;
        comment)      echo "Usage: rebar comment <ticket_id> <body>" ;;
        link)
            echo "Usage: rebar link <id1> <id2> <relation>   (relation REQUIRED)"
            echo "  relation: blocks | depends_on | relates_to | duplicates | supersedes | discovered_from"
            echo "  blocking deps (blocks/depends_on) are promoted to a comparable hierarchy level" ;;
        unlink)       echo "Usage: rebar unlink <source> <target>   (no relation arg; removes the most-recent link between the pair — call repeatedly for multiple)" ;;
        deps)         echo "Usage: rebar deps <ticket_id>" ;;
        edit)         echo "Usage: rebar edit <ticket_id> [--title=VALUE] [--priority=VALUE] [--assignee=VALUE] [--ticket_type=VALUE] [--description=VALUE] [--tags=VALUE] [--parent=VALUE]" ;;
        tag)          echo "Usage: rebar tag <ticket_id> <tag>" ;;
        untag)        echo "Usage: rebar untag <ticket_id> <tag>" ;;
        validate)     echo "Usage: rebar validate [--quick|--full] [--fix] [--verbose] [--json] [--terse]   (repo-wide health check; takes NO ticket id)" ;;
        clarity-check) echo "Usage: rebar clarity-check <ticket_id>   (exit 0=pass, 1=fail; JSON: score/verdict/threshold)" ;;
        check-ac)     echo "Usage: rebar check-ac <ticket_id>   (exit 0=pass, 1=fail; AC_CHECK: pass|fail)" ;;
        quality-check) echo "Usage: rebar quality-check <ticket_id>   (exit 0=pass, 1=fail; QUALITY: pass|fail)" ;;
        exists)       echo "Usage: rebar exists <ticket_id|alias>   (exit 0=exists, 1=not found)" ;;
        list-epics)   echo "Usage: rebar list-epics [--all] [--has-tag=<tag>] [--min-children=<n>]" ;;
        list-descendants) echo "Usage: rebar list-descendants <root_ticket_id>   (BFS walk bucketed by type, JSON)" ;;
        next-batch)   echo "Usage: rebar next-batch <epic_id> [--json]" ;;
        ready)        echo "Usage: rebar ready   (lists tickets whose blockers are all closed)" ;;
        search)       echo "Usage: rebar search <query> [--status=<s>] [--type=<t>] [--has-tag=<tag>]" ;;
        archive)      echo "Usage: rebar archive <ticket_id>   (excludes from default list; idempotent)" ;;
        delete)       echo "Usage: rebar delete <ticket_id> --user-approved   (destructive; requires explicit approval)" ;;
        set-file-impact)  echo "Usage: rebar set-file-impact <ticket_id> <json_array>   (array of {path,reason} objects)" ;;
        get-file-impact)  echo "Usage: rebar get-file-impact <ticket_id>" ;;
        set-verify-commands) echo "Usage: rebar set-verify-commands <ticket_id> <json_array>   (array of {dd_id,dd_text,command})" ;;
        get-verify-commands) echo "Usage: rebar get-verify-commands <ticket_id>" ;;
        summary)      echo "Usage: rebar summary <ticket_id> [<ticket_id> ...]" ;;
        scratch)      echo "Usage: rebar scratch <set|get|clear> <ticket_id> [<key> [<value>]]" ;;
        compact)      echo "Usage: rebar compact <ticket_id> [--threshold=<n>]" ;;
        compact-all)  echo "Usage: rebar compact-all [--dry-run] [--limit=<n>] [--no-commit]" ;;
        fsck)         echo "Usage: rebar fsck" ;;
        fsck-recover) echo "Usage: rebar fsck-recover [--tracker-dir <path>]" ;;
        bridge-status) echo "Usage: rebar bridge-status [--format=json]" ;;
        bridge-fsck)  echo "Usage: rebar bridge-fsck [--tickets-tracker=<path>]" ;;
        purge-bridge) echo "Usage: rebar purge-bridge --keep=<PROJECT_KEY> [--dry-run]" ;;
        revert)       echo "Usage: rebar revert <ticket_id> <target_uuid> [--reason=<text>]" ;;
        resolve)      echo "Usage: rebar resolve <id_or_alias_or_prefix>" ;;
        format)       echo "Usage: rebar format <ticket_id> [mode]" ;;
        *)
            echo "Error: unknown subcommand '$sub'" >&2
            echo "" >&2
            _print_overview >&2
            return 1
            ;;
    esac
    return 0
}

_ensure_initialized() {
    # When the tracker location is explicitly injected (tests, embedding), the
    # caller manages the tracker — do not auto-init the cwd repo's tracker.
    if [[ -n "${TICKETS_TRACKER_DIR:-}" ]]; then
        return 0
    fi
    local repo_root
    repo_root="${PROJECT_ROOT:-${REBAR_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null)}}" || true
    if [[ -z "$repo_root" ]]; then
        echo "Error: not inside a git repository (set REBAR_ROOT or run inside the repo)" >&2
        exit 1
    fi
    if [[ ! -d "$repo_root/.tickets-tracker" ]]; then
        bash "$SCRIPT_DIR/ticket-init.sh" --silent 1>/dev/null || {
            echo "Error: ticket system initialization failed. Run 'ticket init' manually." >&2
            exit 1
        }
    fi
    # Sync ticket data from origin if not recently synced.
    # Works from both main repo and worktree contexts.
    # _TICKET_TEST_NO_SYNC=1 skips the sync block entirely — used by test
    # harnesses where temp repos have no remote and the fetch is wasted I/O.
    if [[ "${_TICKET_TEST_NO_SYNC:-}" == "1" ]]; then
        return
    fi
    local tracker_dir
    if command -v realpath >/dev/null 2>&1; then
        tracker_dir=$(realpath "$repo_root/.tickets-tracker" 2>/dev/null) || tracker_dir="$repo_root/.tickets-tracker"
    else
        tracker_dir=$(cd "$repo_root/.tickets-tracker" 2>/dev/null && pwd -P) || tracker_dir="$repo_root/.tickets-tracker"
    fi
    if [[ -d "$tracker_dir" ]] && git -C "$tracker_dir" rev-parse --verify tickets &>/dev/null; then
        local _md5_12
        if command -v md5sum >/dev/null 2>&1; then
            _md5_12=$(printf '%s' "$tracker_dir" | md5sum | cut -c1-12)
        else
            _md5_12=$(printf '%s' "$tracker_dir" | md5 | cut -c1-12)
        fi
        local sync_marker="/tmp/.ticket-sync-${_md5_12:-fallback}"
        local now
        now=$(date +%s)
        local marker_age=9999
        if [[ -f "$sync_marker" ]]; then
            local marker_time
            marker_time=$(cat "$sync_marker" 2>/dev/null || echo 0)
            marker_age=$(( now - marker_time ))
        fi
        # Sync at most once per minute to shrink the divergence window during
        # rapid-fire ticket operations (brainstorm sessions). 87f3-3b57.
        #
        # Reconvergence policy lives in _reconverge_tickets (ticket-sync.sh): it
        # detects local-ahead by HEAD (not the lagging branch ref — the WS3
        # data-loss fix), merges-as-union for related divergence, force-resets
        # only the unrelated-history stale-orphan case, refuses to act through a
        # rebase/merge recovery state, and runs under the write lock.
        if [[ "$marker_age" -ge 60 ]]; then
            _reconverge_tickets "$tracker_dir"
            echo "$now" > "$sync_marker"
        fi
    fi
}

if [ $# -lt 1 ]; then
    _usage
fi

# ── Help interception (must run BEFORE the case below, so no subcommand is ever
# executed and no side effects / _ensure_initialized occur) ──────────────────
# Top level: `rebar help [<sub>]`, `rebar --help`, `rebar -h`.
#   - bare  -> full subcommand overview (exit 0)
#   - `help <sub>` -> that subcommand's usage (exit 0)
# Per subcommand: `rebar <sub> --help|-h` is treated as a help request ONLY when
# the flag is the FIRST argument immediately after the subcommand. Free-text
# parameters (title, body, comment, description, tags, search query, ...) always
# sit at position >= 2 (preceded by a type or id), so they can never be mistaken
# for the help flag. The bare word `help` is honored only at top level, so e.g.
# `rebar search help` still searches for the term "help".
case "$1" in
    help)
        if [ -n "${2:-}" ]; then
            _print_subcommand_help "$2"; exit $?
        fi
        _help_overview
        ;;
    --help|-h)
        if [ -n "${2:-}" ]; then
            _print_subcommand_help "$2"; exit $?
        fi
        _help_overview
        ;;
esac

subcommand="$1"
shift

# First positional after the subcommand is `--help`/`-h` -> show that
# subcommand's usage and exit without executing.
case "${1:-}" in
    --help|-h)
        _print_subcommand_help "$subcommand"; exit $?
        ;;
esac

case "$subcommand" in
    init)
        exec bash "$SCRIPT_DIR/ticket-init.sh" "$@"
        ;;
    create)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_create "$@"
        exit $?
        ;;
    show)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_show "$@"
        exit $?
        ;;
    transition)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_transition "$@"
        exit $?
        ;;
    claim)
        # Atomic claim (open -> in_progress + assignee in one locked commit).
        # exit 10 surfaces optimistic-concurrency rejection (already claimed).
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        exec bash "$SCRIPT_DIR/ticket-claim.sh" "$@"
        ;;
    reopen)
        # Thin convenience over transition: closed -> open (optimistic concurrency,
        # exit 10 if not currently closed).
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_transition "$1" closed open
        exit $?
        ;;
    list)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_list "$@"
        exit $?
        ;;
    list-epics)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_list_epics "$@"
        exit $?
        ;;
    list-descendants)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_list_descendants "$@"
        exit $?
        ;;
    next-batch)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_next_batch "$@"
        exit $?
        ;;
    comment)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_comment "$@"
        exit $?
        ;;
    compact)
        _ensure_initialized
        # Route through ticket_compact in the library so short IDs / aliases /
        # prefixes are resolved before delegating to ticket-compact.sh
        # (bug ec61-0e1f).
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_compact "$@"
        exit $?
        ;;
    compact-all)
        _ensure_initialized
        exec bash "$SCRIPT_DIR/ticket-compact-all.sh" "$@"
        ;;
    fsck)
        # ticket-fsck.sh: JSON validity + CREATE presence + index.lock cleanup
        _ensure_initialized
        exec bash "$SCRIPT_DIR/ticket-fsck.sh" "$@"
        ;;
    fsck-recover)
        # ticket-fsck-recover.sh: recover the tracker worktree (dangling commits,
        # interrupted rebases). The recover script takes --tracker-dir <path>, not
        # the TICKETS_TRACKER_DIR env var, so translate it when injected (tests).
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
            _ensure_initialized
            exec bash "$SCRIPT_DIR/ticket-fsck-recover.sh" "$@"
        else
            exec bash "$SCRIPT_DIR/ticket-fsck-recover.sh" --tracker-dir "$TICKETS_TRACKER_DIR" "$@"
        fi
        ;;
    bridge-fsck)
        # ticket-bridge-fsck.py: bridge mapping audit (orphans, duplicates, stale SYNCs)
        # Skip _ensure_initialized when TICKETS_TRACKER_DIR is set (test environments
        # provide their own tracker dir and should not trigger ticket-init.sh).
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
            _ensure_initialized
        fi
        exec python3 "$SCRIPT_DIR/ticket-bridge-fsck.py" "$@"
        ;;
    link)
        _ensure_initialized
        # REVIEW-DEFENSE: link routes to ticket-graph.py (not ticket-link.sh) by design.
        # ticket-graph.py's add_dependency() performs link + cycle detection in one atomic
        # operation, which ticket-link.sh does not. unlink intentionally stays on
        # ticket-link.sh because its existing unlink logic correctly computes net-effective
        # state and writes UNLINK events in the same format that ticket-graph.py reads.
        # Both scripts write LINK/UNLINK events to the same append-only event log, so
        # link and unlink remain consistent across the two implementations.
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_link "$@"
        exit $?
        ;;
    unlink)
        _ensure_initialized
        exec bash "$SCRIPT_DIR/ticket-link.sh" unlink "$@"
        ;;
    deps)
        _ensure_initialized
        exec python3 "$SCRIPT_DIR/ticket-graph.py" "$@"
        ;;
    revert)
        # Skip _ensure_initialized when TICKETS_TRACKER_DIR is set (test environments
        # provide their own tracker dir and should not trigger ticket-init.sh).
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
            _ensure_initialized
        fi
        exec bash "$SCRIPT_DIR/ticket-revert.sh" "$@"
        ;;
    bridge-status)
        # Skip _ensure_initialized when TICKETS_TRACKER_DIR is set (test environments
        # provide their own tracker dir and should not trigger ticket-init.sh).
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
            _ensure_initialized
        fi
        exec bash "$SCRIPT_DIR/ticket-bridge-status.sh" "$@"
        ;;
    edit)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_edit "$@"
        exit $?
        ;;
    exists)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_exists "$@"
        exit $?
        ;;
    tag)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_tag "$@"
        exit $?
        ;;
    untag)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_untag "$@"
        exit $?
        ;;
    validate)
        # REVIEW-DEFENSE: _ensure_initialized is intentionally omitted here.
        # validate-issues.sh reads tickets via TICKET_CMD env var (injectable for tests).
        # In production TICKET_CMD defaults to the ticket script itself, which calls
        # _ensure_initialized when it runs ticket list — initialization is transitive.
        # Calling _ensure_initialized here would break test isolation where TICKET_CMD
        # is mocked and no real ticket store exists.
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_validate "$@"
        exit $?
        ;;
    clarity-check)
        # REVIEW-DEFENSE: _ensure_initialized is intentionally omitted here.
        # ticket-clarity-check.sh reads tickets via DSO_CLI (not TICKET_CMD), so test
        # isolation uses --stdin mode (pipe fixture JSON directly) rather than env injection.
        # In production DSO_CLI calls ticket show, which calls _ensure_initialized —
        # initialization is transitive. Calling _ensure_initialized here would break
        # test isolation where no real ticket store exists.
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_clarity_check "$@"
        exit $?
        ;;
    check-ac)
        # REVIEW-DEFENSE: _ensure_initialized is intentionally omitted here.
        # check-acceptance-criteria.sh reads tickets via TICKET_CMD env var (injectable for tests).
        # In production TICKET_CMD defaults to the ticket script itself, which calls
        # _ensure_initialized when it runs ticket show — initialization is transitive.
        # Calling _ensure_initialized here would break test isolation where TICKET_CMD
        # is mocked and no real ticket store exists.
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_check_ac "$@"
        exit $?
        ;;
    quality-check)
        # REVIEW-DEFENSE: _ensure_initialized is intentionally omitted here.
        # issue-quality-check.sh reads tickets via TICKET_CMD env var (injectable for tests).
        # In production TICKET_CMD defaults to the ticket script itself, which calls
        # _ensure_initialized when it runs ticket show — initialization is transitive.
        # Calling _ensure_initialized here would break test isolation where TICKET_CMD
        # is mocked and no real ticket store exists.
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_quality_check "$@"
        exit $?
        ;;
    purge-bridge)
        # Skip _ensure_initialized when TICKETS_TRACKER_DIR is set (test environments
        # provide their own tracker dir and should not trigger ticket-init.sh).
        # purge-non-project-tickets.sh handles tracker initialization internally.
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
            _ensure_initialized
        fi
        exec bash "$SCRIPT_DIR/ticket-purge-bridge.sh" "$@"
        ;;
    ready)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_ready "$@"
        exit $?
        ;;
    search)
        # Full-text search (replay-derived; no committed index). Read-only.
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        exec python3 "$SCRIPT_DIR/ticket-search.py" "$@"
        ;;
    summary)
        # REVIEW-DEFENSE: _ensure_initialized is intentionally omitted here.
        # issue-summary.sh reads tickets via TICKET_CMD env var (injectable for tests).
        # In production TICKET_CMD defaults to the ticket script itself, which calls
        # _ensure_initialized when it runs ticket show — initialization is transitive.
        # Calling _ensure_initialized here would break test isolation where TICKET_CMD
        # is mocked and no real ticket store exists.
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_summary "$@"
        exit $?
        ;;
    archive)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_archive "$@"
        exit $?
        ;;
    delete)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_delete "$@"
        exit $?
        ;;
    set-file-impact)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_set_file_impact "$@"
        exit $?
        ;;
    get-file-impact)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_get_file_impact "$@"
        exit $?
        ;;
    set-verify-commands)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_set_verify_commands "$@"
        exit $?
        ;;
    get-verify-commands)
        _ensure_initialized
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_get_verify_commands "$@"
        exit $?
        ;;
    resolve)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_resolve "$@"
        exit $?
        ;;
    format)
        if [ -z "${TICKETS_TRACKER_DIR:-}" ]; then _ensure_initialized; fi
        # shellcheck source=/dev/null
        source "$SCRIPT_DIR/ticket-lib-api.sh"
        _ticketlib_dispatch ticket_format "$@"
        exit $?
        ;;
    scratch)
        # Route scratch sub-verb to ticket-scratch.sh dispatcher.
        # ticket-scratch.sh handles set/get/clear routing and unknown-verb errors.
        # No _ensure_initialized: scratch uses the filesystem, not the ticket store.
        exec bash "$SCRIPT_DIR/ticket-scratch.sh" "$@"
        ;;
    *)
        echo "Error: unknown subcommand '$subcommand'" >&2
        _usage
        ;;
esac
