#!/usr/bin/env bash
# Briefkin hook: keeps one card per agent session on your wall, without the agent's help.
#
#   prompt submitted          -> what the agent is now doing          (in progress)
#   permission / input needed -> "Waiting for permission / input"     (needs you; nothing when Codex's
#                                                                       Auto Review answers it for you)
#   permission answered       -> back to the task                     (in progress)
#   task list updated         -> the agent's own todo list or plan as a checklist, with progress from it
#                                (Claude Code todos and tasks, a Projects thread's status checklist,
#                                Codex's plan, Cursor's todos)
#   turn finished             -> where the task stands: what happened (done), or, only when the agent
#                                stopped part-way and cannot go on without you, what it needs (needs you);
#                                still in progress while agents it started in the background are at work
#   background report arrives -> back to the task                     (in progress)
#   session closed            -> done; nothing it asked is waiting any more
#   ask settled out of sight  -> off Needs you, when this computer shows it no longer waits: a permission prompt
#                                answered or refused with no hook after it, a Projects thread the person wrote in
#                                since, in a newer session (checked by the other sessions' hooks, sweep_asks)
#
# Answers from Briefkin (Claude Code on your own computer, terminal or desktop app): when a turn ends on a question,
# the card takes an answer, from the dashboard or Telegram. It is kept on the card, and the next time you write in
# that session the prompt hook hands it to Claude along with your message. Nothing wakes the session by itself.
# Permission prompts and Projects threads are still answered in the session. BRIEFKIN_REPLIES=0 turns this off.
#
# Status lines are written by a model you already use, for every agent: Claude Code's Haiku
# (`claude -p`), else Codex (`codex exec`, low reasoning), else OpenCode (`opencode run`, its default
# model), on your own login, in the background so the agent never waits. A turn that changed nothing
# sends nothing. With none of them installed, or if they fail, the hook falls back to trimming the prompt
# and the last message.
#
# What gets sent is the person's choice (Settings, "What gets sent" on the dashboard), and it is decided here, so what
# they keep off the board never leaves this computer. Projects can all be sent, all but some, or only some. A chat
# (a session outside any project: no repository, no Projects thread) can always be sent, never, or only when the
# person's rule allows it: the model that writes the status line reads the rule with each new task and holds back
# the ones it keeps here; nothing about a held task is sent. Without a model to ask, a chat under a rule is held.
#
# The installer sets this up for every harness it finds:
#   curl -fsSL https://briefkin.dev/install | sh
#
# Usage: briefkin-hook.sh [claude|codex|cursor|gemini|copilot|windsurf|opencode|amp] [event]   (default: claude)
#   The hook payload arrives as JSON on stdin, except Codex's legacy `notify`, which passes it as the
#   last argument. Copilot CLI and Windsurf hooks name the event as the second argument. The OpenCode
#   and Amp plugins pipe {"event": prompt|permission|stop|fail, "session", "cwd", "text"}.
#
# Credentials: BRIEFKIN_TOKEN and BRIEFKIN_URL from the environment, else ~/.config/briefkin/env.
# The rules come from Briefkin and are kept in ~/.config/briefkin/rules.json (BRIEFKIN_RULES), fetched again when a
# minute old. Optional: BRIEFKIN_PROJECT (default: the repository, else the Claude Code Projects project when
# the session runs in one, else the folder name), BRIEFKIN_SOURCE (default "<Harness> · <folder>"),
# BRIEFKIN_LINK (a URL to open the session, e.g. a Projects thread), BRIEFKIN_PRIVATE=1 (never send prompt
# or message text, only states), BRIEFKIN_DETAILS=1 (also send the agent's whole final message, shown when
# the card is opened; off by default, since a card says where things stand), BRIEFKIN_SUMMARIZER (which
# CLIs write status lines, in order; default claude,codex,opencode, or off), BRIEFKIN_CLAUDE_MODEL (default
# haiku), BRIEFKIN_CODEX_MODEL, BRIEFKIN_OPENCODE_MODEL (default: each CLI's own).
#
# Needs curl and jq (or python3). Never blocks the agent: it exits 0 whatever happens.
set -u
# Sent with every update, so the dashboard can say when this script is out of date.
BRIEFKIN_HOOK_VERSION=26
# The model call below runs Claude Code, whose own hooks would call this script again.
[ -z "${BRIEFKIN_HOOK_INNER:-}" ] || exit 0
harness=${1:-claude}
[ $# -gt 0 ] && shift
arg=${1:-}

# Cursor's beforeSubmitPrompt hook must answer before the prompt is sent; its other hooks need no output.
cursor_event=
[ "$harness" = cursor ] && trap 'case "$cursor_event" in ""|beforeSubmitPrompt) printf "{\"continue\":true}\n" 2>/dev/null ;; esac' EXIT

if [ -z "${BRIEFKIN_TOKEN:-}" ] && [ -r "${BRIEFKIN_CONFIG:-$HOME/.config/briefkin/env}" ]; then
  # shellcheck disable=SC1090
  . "${BRIEFKIN_CONFIG:-$HOME/.config/briefkin/env}"
fi
[ -n "${BRIEFKIN_TOKEN:-}" ] || exit 0
command -v curl >/dev/null 2>&1 || exit 0

if [ "$harness" = codex ] && [ "${arg#\{}" != "$arg" ]; then
  input=$arg
else
  input=$(cat 2>/dev/null) || exit 0
fi
[ -n "$input" ] || exit 0

# Claude Code, Codex and Cursor run PostToolUse after every tool call. It only matters when the call updated the
# agent's task list, or right after a permission prompt (a tool ran, so the prompt was answered), so otherwise the
# hook stops here, before parsing anything.
plan_tools='TodoWrite|TaskCreate|TaskUpdate|mcp__hearthbot__update_status|update_plan'
if [ "$harness" = claude ] || [ "$harness" = codex ]; then
  case "$input" in *'"PostToolUse"'*)
    case "$input" in *'"TodoWrite"'*|*'"TaskCreate"'*|*'"TaskUpdate"'*|*'"mcp__hearthbot__update_status"'*|*'"update_plan"'*) ;; *)
      set -- "${TMPDIR:-/tmp}"/briefkin-*.ask
      [ -e "$1" ] || exit 0 ;;
    esac ;;
  esac
elif [ "$harness" = cursor ]; then
  case "$input" in *'"postToolUse"'*) cursor_event=postToolUse; case "$input" in *'"todos"'*) ;; *) exit 0 ;; esac ;; esac
fi

if command -v jq >/dev/null 2>&1; then
  get() { printf '%s' "$input" | jq -r --arg k "$1" 'getpath($k | split(".") | map(tonumber? // .)) // empty | if type == "string" then . else tojson end' 2>/dev/null; }
elif command -v python3 >/dev/null 2>&1; then
  get() {
    printf '%s' "$input" | python3 -c 'import json,sys
v=json.load(sys.stdin)
for k in sys.argv[1].split("."):
    v = v[int(k)] if isinstance(v, list) and k.isdigit() and int(k) < len(v) else v.get(k) if isinstance(v, dict) else None
print("" if v is None else v if isinstance(v,str) else json.dumps(v))' "$1" 2>/dev/null
  }
else
  exit 0
fi

# True when Codex's own reviewer answers this session's approval requests (approvals_reviewer "auto_review", or the
# older "guardian_subagent"): the desktop app's Auto Review, or `codex --approve-for-me`. The PermissionRequest
# payload does not say, so this reads the turn's settings from the session's rollout file (the desktop app sets
# them per turn), and falls back to the top-level setting in config.toml.
codex_auto_review() {
  t=$(get transcript_path) turn=$(get turn_id) reviewer=
  if [ -n "$t" ] && [ -r "$t" ]; then
    settings=$(grep -E '"(turn_context|thread_settings_applied)"' "$t" 2>/dev/null | grep '"approvals_reviewer"')
    this_turn=
    [ -n "$turn" ] && this_turn=$(printf '%s\n' "$settings" | grep -F "\"$turn\"" | tail -n 1)
    reviewer=$(printf '%s\n' "${this_turn:-$settings}" | tail -n 1 | sed -n 's/.*"approvals_reviewer" *: *"\([a-z_]*\)".*/\1/p')
  fi
  [ -n "$reviewer" ] || reviewer=$(awk '/^[[:space:]]*\[/ { exit }
    /^[[:space:]]*approvals_reviewer[[:space:]]*=/ { v = $0; sub(/^[^"]*"/, "", v); sub(/".*/, "", v); print v; exit }' \
    "${CODEX_HOME:-$HOME/.codex}/config.toml" 2>/dev/null)
  case "$reviewer" in auto_review|guardian_subagent) return 0 ;; esac
  return 1
}

# Normalise each harness's payload to: what happened, session id, folder, and the text to show.
what= session= cwd= text= ntype=
case "$harness" in
  claude)
    label="Claude Code"
    session=$(get session_id) cwd=$(get cwd)
    case "$(get hook_event_name)" in
      UserPromptSubmit) what=prompt text=$(get prompt) ;;
      Notification) what=notify ntype=$(get notification_type) text=$(get message) ;;
      PostToolUse) what=resume; [[ "$(get tool_name)" =~ ^($plan_tools)$ ]] && what=plan ;;
      Stop) if [ "$arg" = wait ]; then what=wait; else what=stop text=$(get last_assistant_message); fi ;;
      SessionEnd) what=end ;;
    esac
    ;;
  codex)
    label="Codex"
    if [ "$input" = "$arg" ]; then
      # notify: {"type": "agent-turn-complete", "thread-id", "cwd", "last-assistant-message", ...}
      session=$(get thread-id) cwd=$(get cwd)
      case "$(get type)" in
        agent-turn-complete) what=stop text=$(get last-assistant-message) ;;
      esac
    else
      # hooks.json: Claude-style payload on stdin. Codex has no idle event; Stop ends each turn.
      session=$(get session_id) cwd=$(get cwd)
      case "$(get hook_event_name)" in
        UserPromptSubmit) what=prompt text=$(get prompt) ;;
        PermissionRequest)
          # Codex runs this before it decides who answers. With Auto Review its own reviewer does, and nobody is asked.
          codex_auto_review && exit 0
          what=notify ntype=permission_prompt text=$(get tool_input.description)
          [ -n "$text" ] || { tool=$(get tool_name); text=${tool:+Approve $tool}; }
          ;;
        PostToolUse) what=resume; [ "$(get tool_name)" = update_plan ] && what=plan ;;
        Stop) what=stop text=$(get last_assistant_message) ;;
        SessionEnd) what=end ;;
        # Not Codex's: sent by this script's own turn watcher (watch_codex_turn) when a turn was stopped.
        TurnAborted) what=halt ;;
      esac
    fi
    ;;
  copilot)
    label="Copilot CLI"
    session=$(get sessionId) cwd=$(get cwd)
    case "$arg" in
      prompt) what=prompt text=$(get prompt) ;;
      permission) what=notify ntype=permission_prompt text=$(get toolName) ;;
      stop) what=stop text= ;;
    esac
    ;;
  cursor)
    label="Cursor"
    session=$(get conversation_id) cwd=$(get workspace_roots.0) cursor_event=$(get hook_event_name)
    case "$cursor_event" in
      beforeSubmitPrompt) what=prompt text=$(get prompt) ;;
      # Cursor's todo tool; its name is not documented, so any tool whose input carries a todos list counts.
      postToolUse) [ "$(get tool_input.todos.0.status)" != "" ] && what=plan ;;
      # The IDE sends the final text here; the CLI only sends stop, which then ends the turn itself.
      afterAgentResponse) what=stop text=$(get text) generation=$(get generation_id) ;;
      stop)
        generation=$(get generation_id)
        case "$(get status)" in
          error) what=fail text="Cursor stopped with an error." ;;
          *) what=stop text= ;;
        esac
        ;;
    esac
    ;;
  opencode|amp)
    # The OpenCode and Amp plugins hand over a normalised payload: {event, session, cwd, text}.
    [ "$harness" = amp ] && label="Amp" || label="OpenCode"
    session=$(get session) cwd=$(get cwd) text=$(get text)
    case "$(get event)" in
      prompt) what=prompt ;;
      permission) what=notify ntype=permission_prompt ;;
      resume) what=resume ;;
      stop) what=stop ;;
      fail) what=fail ;;
    esac
    ;;
  windsurf)
    label="Windsurf"
    session=$(get trajectory_id) cwd=$PWD
    case "$arg" in
      pre_user_prompt) what=prompt text=$(get tool_info.user_prompt) ;;
      post_cascade_response) what=stop text=$(get tool_info.response) ;;
    esac
    ;;
  gemini)
    label="Gemini CLI"
    session=$(get session_id) cwd=$(get cwd)
    case "$(get hook_event_name)" in
      BeforeAgent) what=prompt text=$(get prompt) ;;
      Notification) what=notify ntype=$(get notification_type) text=$(get message) ;;
      AfterAgent) what=stop text=$(get prompt_response) ;;
    esac
    ;;
  *) exit 0 ;;
esac
[ -n "$what" ] && [ -n "$session" ] || exit 0

folder=$(basename "${cwd:-$PWD}")
# A session run from / (or with no folder) has no folder to name.
case "$folder" in /|.) folder= ;; esac
# A Projects session names its project and thread in the transcript's opening context. Best effort:
# anything else falls back to the folder and no link.
transcript=$(get transcript_path)
projects_name= projects_link= projects_session=
if [ -n "$transcript" ] && [ -r "$transcript" ]; then
  head_=$(head -c 300000 "$transcript" 2>/dev/null)
  projects_name=$(printf '%s' "$head_" | grep -o -m1 'Project: \\"[^"\\]\{1,100\}\\" (id: `chan_' | sed 's/^Project: \\"//; s/\\" (id: `chan_$//')
  projects_link=$(printf '%s' "$head_" | grep -o -m1 'https://claude\.ai/code/project/chan_[A-Za-z0-9]*?thread=cmsg_[A-Za-z0-9]*')
  # A thread's session on this computer (Remote Control) has no thread link in its context: each message it gets
  # comes in a Projects wrapper (projectsUserTurn) naming the project and the thread, so the link is built from that.
  if [ -z "$projects_link" ]; then
    turn_=$(grep -m1 '"projectsUserTurn": \{0,1\}true' "$transcript" 2>/dev/null)
    chan_=$(printf '%s' "$turn_" | grep -o -m1 '<project id=\\"chan_[A-Za-z0-9]*' | grep -o 'chan_[A-Za-z0-9]*')
    cmsg_=$(printf '%s' "$turn_" | grep -o -m1 '<thread ts=\\"cmsg_[A-Za-z0-9]*' | grep -o 'cmsg_[A-Za-z0-9]*')
    # A session started on a message in the project chat gets that message with no <thread>: it is the thread's root.
    [ -n "$cmsg_" ] || cmsg_=$(printf '%s' "$turn_" | grep -o -m1 '<message [^>]*id=\\"cmsg_[A-Za-z0-9]*' | grep -o 'cmsg_[A-Za-z0-9]*$')
    [ -n "$chan_" ] && [ -n "$cmsg_" ] && projects_link="https://claude.ai/code/project/$chan_?thread=$cmsg_"
    [ -n "$turn_" ] && projects_session=1
    # Started by a project (its first message is the project's brief) though no person has written to it yet.
    [ -z "$turn_" ] && printf '%s' "$head_" | grep -m1 '"type": \{0,1\}"user"' | grep -q '<relay from=\\"coordinator\\"' && projects_session=1
  fi
fi
[ -n "$projects_link" ] && projects_session=1
# The repository names the project: its remote's name, else the main checkout's folder (so worktrees and
# agent scratch checkouts still say which project they belong to). It wins over the Projects project's name,
# so a thread's session and a plain session in the same checkout land under one project. Outside any repository, a folder named after
# a chat ("can-you-give-me-ideas-for") says nothing, so the card says "No project".
repo=
if [ -z "${BRIEFKIN_PROJECT:-}" ] && [ -n "$cwd" ] && command -v git >/dev/null 2>&1; then
  repo=$(git -C "$cwd" config --get remote.origin.url 2>/dev/null) repo=${repo%.git} repo=${repo%/} repo=${repo##*[/:]}
  if [ -z "$repo" ]; then
    common=$(git -C "$cwd" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
    [ -n "$common" ] && { common=${common%/.git}; repo=${common##*/}; }
  fi
fi
# The Codex app runs each chat that has no project in its own ~/Documents/Codex/<date>/<first words of the
# prompt> folder ("i-ne", "su"), so that folder names a chat, not a project.
[ -n "$repo$projects_name" ] || case "$cwd" in */Codex/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*) repo="No project" ;; esac
[ -n "$repo$projects_name" ] || case "$folder" in ''|*-*-*-*) repo="No project" ;; esac
project=${BRIEFKIN_PROJECT:-${repo:-${projects_name:-$folder}}}
source=${BRIEFKIN_SOURCE:-"$label · $folder"}
[ -n "$folder" ] || [ -n "${BRIEFKIN_SOURCE:-}" ] || source=$label
[ "$repo" = "No project" ] && [ -z "${BRIEFKIN_SOURCE:-}" ] && source=$label
private=${BRIEFKIN_PRIVATE:-}
# What this session is on: the task name from its last real prompt (so turn ends and short replies keep
# it), that prompt, and the card's last status line, for the model's context.
state_file="${TMPDIR:-/tmp}/briefkin-$(printf '%s' "$label-$session" | tr -c 'A-Za-z0-9_-' '_')"
task=$(cat "$state_file" 2>/dev/null || true)
asked=$(cat "$state_file.asked" 2>/dev/null || true)
last=$(cat "$state_file.last" 2>/dev/null || true)
# Cursor's IDE ends a turn twice (afterAgentResponse with the text, then stop): report it once.
if [ -n "${generation:-}" ]; then
  [ "$(cat "$state_file.turn" 2>/dev/null)" = "$generation" ] && [ "$what" != fail ] && exit 0
  printf '%s' "$generation" > "$state_file.turn" 2>/dev/null || true
fi

# What the person lets onto their board. The rules are fetched at most once a minute; when Briefkin cannot be
# reached the last copy stands, and with no copy at all everything is sent, as before rules existed.
#   {"projects": "all|except|only", "project_list": "<one name per line>", "chats": "all|rule|none", "rule": "..."}
rules_file=${BRIEFKIN_RULES:-$HOME/.config/briefkin/rules.json}
if [ -z "$(find "$rules_file" -mmin -1 2>/dev/null)" ]; then
  mkdir -p "${rules_file%/*}" 2>/dev/null
  fresh=$(mktemp "$rules_file.XXXXXX" 2>/dev/null) && {
    if curl -sSf -m 3 "${BRIEFKIN_URL:-https://briefkin.dev}/v1/rules" -H "authorization: Bearer $BRIEFKIN_TOKEN" -o "$fresh" 2>/dev/null; then
      mv -f "$fresh" "$rules_file" 2>/dev/null
    else
      rm -f "$fresh" 2>/dev/null
      if [ -e "$rules_file" ]; then touch "$rules_file" 2>/dev/null; else printf '{}' > "$rules_file" 2>/dev/null; fi
    fi
  }
fi
rules=$(cat "$rules_file" 2>/dev/null)
rule() { local input=$rules; get "$1"; }
# A chat: no repository and no Projects thread, so nothing says it is work.
chat=
[ -z "${BRIEFKIN_PROJECT:-}$projects_name" ] && { [ -z "$repo" ] || [ "$repo" = "No project" ]; } && chat=1
listed() { rule project_list | grep -qxF -- "$project"; }
case "$(rule projects)" in
  except) listed && exit 0 ;;
  only) [ -n "$chat" ] || listed || exit 0 ;;
esac
# check: this chat's tasks are sent only when the rule lets them through. A task it holds back leaves .off, and
# nothing more is sent until a new task passes; while a task is being checked (.checking), reports wait for it.
check=
if [ -n "$chat" ]; then
  case "$(rule chats)" in
    none) exit 0 ;;
    rule) [ -n "$(rule rule)" ] && check=1 ;;
  esac
fi
held() { [ -n "$check" ] && [ -e "$state_file.off" ] && [ -z "$(find "$state_file.checking" -mmin -2 2>/dev/null)" ]; }
[ -n "$check" ] || rm -f "$state_file.off" 2>/dev/null
[ "$what" != prompt ] && held && exit 0
checking=

# Codex runs no hook when a turn is stopped (the stop button, Esc, or the message sent again in a new chat), so the
# card would stay in progress. Each prompt starts a watcher that reads the session's log until this turn ends; when
# the turn was stopped, it runs this script again with a TurnAborted payload. A newer prompt in the session takes its
# place (.watch), and it gives up after 12 hours.
watch_codex_turn() {
  command -v jq >/dev/null 2>&1 || return 0
  local log turn me from
  log=$(get transcript_path) turn=$(get turn_id)
  [ -n "$log" ] && [ -r "$log" ] ||
    log=$(find "${CODEX_HOME:-$HOME/.codex}/sessions" -name "rollout-*$session.jsonl" -mtime -2 2>/dev/null | head -n 1)
  [ -n "$log" ] && [ -r "$log" ] || return 0
  me="$$-$RANDOM"
  printf '%s' "$me" > "$state_file.watch" 2>/dev/null || return 0
  # The prompt is logged before the turn starts, so everything this turn writes comes after this point.
  from=$(( $(wc -c < "$log" | tr -d ' ') + 1 ))
  (
    end=$(( $(date +%s) + 43200 )) line=
    while [ "$(date +%s)" -lt "$end" ]; do
      sleep 3
      [ "$(cat "$state_file.watch" 2>/dev/null)" = "$me" ] || exit 0
      line=$(tail -c +"$from" "$log" 2>/dev/null | grep -E '"type": ?"(turn_aborted|task_complete)"' | grep -F -- "${turn:+\"$turn\"}" | head -n 1)
      [ -n "$line" ] && break
    done
    [ -n "$line" ] && [ "$(cat "$state_file.watch" 2>/dev/null)" = "$me" ] || exit 0
    rm -f "$state_file.watch" 2>/dev/null
    # When it stopped, in milliseconds: the line's timestamp, else the turn's end in seconds.
    at=$(printf '%s' "$line" | jq -r 'select(.payload.type == "turn_aborted")
      | ((.timestamp // "" | capture("^(?<s>[^.]+)\\.(?<ms>[0-9]{3})") | (.s + "Z" | fromdateiso8601) * 1000 + (.ms | tonumber))?
         // ((.payload.completed_at // 0) * 1000)) | floor' 2>/dev/null)
    [ -n "$at" ] || exit 0
    jq -cn --arg s "$session" --arg cwd "$cwd" --arg t "$log" --argjson at "$at" \
      '{hook_event_name: "TurnAborted", session_id: $s, cwd: $cwd, transcript_path: $t, stopped_ms: $at}' |
      "${BASH:-bash}" "$0" codex
  ) </dev/null >/dev/null 2>&1 &
}
[ "$harness" = codex ] && [ "$what" = prompt ] && [ "$input" != "$arg" ] && watch_codex_turn

# First line, trimmed to n characters.
line() { local s=${1%%$'\n'*}; s=${s#"${s%%[![:space:]]*}"}; printf '%s' "${s:0:$2}"; }

# The person's words from a prompt. Projects and other harnesses wrap prompts in markup (wake,
# relay, system-reminder, session-context blocks); this keeps a human message when there is one, else
# the coordinator's note, and drops the wrappers and boilerplate. Prompts the harness itself sends
# (background task notifications, reminders) come out empty. Plain prompts pass through unchanged.
human_text() {
  # Codex lists attached files (a pasted screenshot is "Codex-clipboard-<id>.png") before the prompt:
  # "# Files mentioned by the user:\n\n## <name>: <path>\n\n## My request for Codex:\n<prompt>".
  case "$1" in *"## My request for Codex:"*) set -- "${1##*"## My request for Codex:"}"; set -- "${1#"${1%%[![:space:]]*}"}" ;; esac
  case "$1" in *"<"*">"*) ;; *) printf '%s' "$1"; return ;; esac
  if command -v perl >/dev/null 2>&1; then
    printf '%s' "$1" | perl -0777 -ne '
      # Codex hands the answers to its own in-app questions back as a prompt: the person'"'"'s words are the answers.
      if (/<send_user_message_question_reply>(.*?)<\/send_user_message_question_reply>/s) {
        $_ = join "\n", map { (my $a = $_) =~ s/\\n/ /g; $a =~ s/\\(.)/$1/g; $a } $1 =~ /"answer"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
      }
      elsif (/<cited\b[^>]*author="user"[^>]*>(.*?)<\/cited>/s) { $_ = $1 }
      elsif (/<message\b[^>]*from="human"[^>]*>(.*?)<\/message>/s) { $_ = $1 }
      else {
        if (/<note>(.*?)<\/note>/s) { $_ = $1 }
        # Anything else in tags is the harness talking (reminders, task notifications): drop whole blocks.
        else { 1 while s/<([A-Za-z][\w-]*)\b[^>]*>.*?<\/\1\b[^>]*>//gs }
      }
      s/<[^>]*>//g; s/&lt;/</g; s/&gt;/>/g; s/&quot;/"/g; s/&#(x[0-9a-fA-F]{1,6}|[0-9]{1,7});/my $v = $1; my $c = chr(lc substr($v, 0, 1) eq "x" ? hex substr $v, 1 : $v); utf8::encode($c); $c/ge; s/&amp;/&/g;
      print join "\n", grep { /\S/ && !/^\s*(The (note|messages) below|A message from this project|You are a Thread Session|Codebase and user instructions)/ } split /\n/;'
  else
    printf '%s' "$1" | sed 's/<[^>]*>//g' | grep -v '^[[:space:]]*$' | grep -v '^[[:space:]]*\(The note below\|The messages below\|A message from this project\)'
  fi
}

# A short name for the task a prompt asks for: the first sentence that asks for something, without
# preambles ("You are running on…", "Context: …"), politeness ("Please", "Can you"), paths or URLs.
# "You are on Petar's Mac in ~/x. Your job: render the reels in /Users/p/x." -> "Render the reels".
task_name() {
  if ! command -v perl >/dev/null 2>&1; then line "$1" 80; return; fi
  printf '%s' "$1" | perl -0777 -ne '
    s/```.*?(```|\z)//gs;
    # A Markdown link keeps its text; a pasted link, whose text is the address itself, keeps the site name.
    s{\[([^\]\n]*)\]\((https?://[^)\s]+)\)}{ my ($t, $u) = ($1, $2); $t =~ m{^\s*(?:https?://|www\.)}i ? ($u =~ m{^https?://(?:www\.)?([^/?#:]+)}i)[0] : $t }ge;
    s{\bhttps?://\S+}{}g;
    # A file path becomes its file name; a folder path goes, with the "in"/"at" that introduces it.
    s{(\b(?:in|at|under|from|to|into|on)\s+)?(?:~|\.{1,2})?(?:/[\w.@+-]+){2,}/?}{ my ($p, $b) = ($1 // "", $&); $b =~ m{/([\w@+-]+\.\w+)$} ? "$p$1" : "" }ge;
    my @s;
    for my $l (split /\n/) {
      $l =~ s/^\s*(?:[#>]+|[-*+]|\d+[.)])\s+//; $l =~ s/\*\*|__|`//g;
      push @s, grep { /\S/ } split /(?<=[.!?])\s+(?=\S)/, $l;
    }
    my $t = "";
    for (@s) {
      s/^\s+|\s+$//g;
      s/^(?:your (?:job|task|goal)(?: here)?(?: is)?|task|goal|todo|ask|request)\s*[:\-]\s*//i;
      # Nothing but punctuation, or a bare site name, says nothing about the task.
      next if !/\S/ || /:$/ || /^[\s[:punct:]]*$/ || /^[\w-]+(?:\.[\w-]+)+$/;
      next if /^(?:you are|you.re|you have|you.ve|you will|this is|context|background|note|fyi|hi|hey|hello|thanks|thank you|repo|repository|branch|cwd|folder|project)\b/i;
      next if /^(?:i.m|i am|we are|we.re)\b/i && !/\b(?:need|want)\b/i;
      $t = $_; last;
    }
    $t = $s[0] // "" if $t eq "";
    1 while $t =~ s/^(?:please|pls|ok(?:ay)?|so|now|then|also|and|hey|hi)[,\s]+//i;
    # A request phrased as a question is still a request: "Can you fix X?" -> "Fix X".
    $t =~ s/\?$// if $t =~ s/^(?:(?:can|could|would|will) you(?: please)?|i (?:want|need|would like) you to|i.d like you to|let.s|go ahead and|help me)\s+//i;
    $t =~ s/(?:\s+(?:please|thanks))?[\s.!]*$//i;
    $t =~ s/\s{2,}/ /g; $t =~ s/\s+([,.;:])/$1/g; $t =~ s/^\s+|[\s,;:]+$//g;
    # Too long: end at the last clause break that leaves enough to read, else at a word.
    if (length($t) > 80) {
      $t = substr($t, 0, 81);
      if ($t =~ /^(.{30,}?)(?:,|;|\s+-|\s+(?:and|then|but|so|because|which|while|when)\b)\s(?!.*(?:,|;|\s(?:and|then|but|so|because|which|while|when)\s))/) { $t = $1 }
      else { $t =~ s/[\s,;:]+\S*$//; $t .= "…" }
    }
    print ucfirst $t;'
}

# Splits a final message into title, summary and details, separated by \037. The title is the first
# sentence that says something: "Done.", "All set." or "Reported to Petar." are skipped, and so are
# Markdown markers. The summary is the next few lines, Markdown kept; details is the whole message
# when the summary does not already cover it.
outcome() {
  printf '%s' "$1" | awk '
    function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
    function filler(s) {
      s = tolower(trim(s)); gsub(/[.!:]+$/, "", s)
      return s == "" || s ~ /^(all )?(done|set|finished|complete|completed)$/ || s ~ /^(ok|okay|great|perfect|sure|got it|all set|summary|recap|tl;?dr|update|result|results)$/ ||
        s ~ /^here(.s| is) (the |a |my )?(summary|recap|update|what (i did|changed|happened))/ ||
        s ~ /^(i( have|.ve) )?(reported|posted|replied|sent|told)( it| this| that| the [a-z]+)? (back )?(to|in) /
    }
    function clip(s, n,   c) {
      if (length(s) <= n) return s
      c = substr(s, 1, n); sub(/[ \t][^ \t]*$/, "", c); return c "…"
    }
    { lines[NR] = $0 }
    END {
      code = 0; at = 0; title = ""
      for (i = 1; i <= NR && title == ""; i++) {
        l = lines[i]
        if (l ~ /^[ \t]*(```|~~~)/) { code = !code; continue }
        if (code) continue
        sub(/^[ \t]*([#>]+|[-*+]|[0-9]+[.)])[ \t]+/, "", l); gsub(/\*\*|__|`/, "", l); l = trim(l)
        # Drop leading filler sentences, keep the rest of the line.
        while (l != "" && match(l, /^[^.!?:]*[.!?:]+([ \t]+|$)/) && filler(substr(l, 1, RLENGTH))) l = trim(substr(l, RLENGTH + 1))
        if (l == "" || filler(l)) continue
        # The title is the first sentence; the rest of its line opens the summary.
        tail = ""
        if (match(l, /^([^.!?]|[.!?][^ \t.!?])*[.!?]+[ \t]+/) && RLENGTH >= 12) { tail = trim(substr(l, RLENGTH + 1)); l = trim(substr(l, 1, RLENGTH)) }
        sub(/[.]$/, "", l); title = clip(l, 110); cut = (title != l); at = i
      }
      summary = tail; shown = (tail != "")
      for (i = at + 1; i <= NR && shown < 2; i++) {
        if (lines[i] ~ /^[ \t]*$/) { if (summary != "") summary = summary (summary ~ /\n$/ ? "\n" : "\n\n"); continue }
        if (lines[i] ~ /^[ \t]*(```|~~~)/) break
        summary = summary (summary != "" && summary !~ /\n$/ ? "\n" : "") lines[i] "\n"; shown++
      }
      gsub(/\n+$/, "", summary); full = summary; summary = clip(summary, 240)
      rest = 0
      for (; i <= NR; i++) if (lines[i] !~ /^[ \t]*$/) rest = 1
      body = ""
      for (i = 1; i <= NR; i++) body = body lines[i] (i < NR ? "\n" : "")
      printf "%s\037%s\037%s", title, summary, (rest || cut || summary != full ? substr(body, 1, 7000) : "")
    }'
}

# The agent's own task list, saved as [{id, label, active, status}] in $state_file.plan whenever a tool call changes
# it: Claude Code's TodoWrite (the whole list) or TaskCreate/TaskUpdate (one task; the list is in its tasks folder),
# a Projects thread's status checklist (lines marked ✓ done, ✱ in progress, ○ not started), Codex's update_plan
# and Cursor's todos (with merge, only the changed items). A Projects thread's checklist is what its person
# reads, so once a session has one, its other lists are ignored.
save_plan() {
  command -v jq >/dev/null 2>&1 || return 1
  local tool items
  tool=$(get tool_name)
  if [ -e "$state_file.hearth" ] && [ "$tool" != mcp__hearthbot__update_status ]; then return 1; fi
  case "$tool" in
    TaskCreate|TaskUpdate)
      local dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/tasks/${CLAUDE_CODE_TASK_LIST_ID:-$session}"
      set -- "$dir"/*.json
      [ -e "$1" ] || return 1
      items=$(jq -cs 'map(select(type == "object" and .status != "deleted"))
        | sort_by(.id | tonumber? // 0) | map({id: (.id | tostring), label: .subject, active: .activeForm, status})' "$@" 2>/dev/null) ;;
    mcp__hearthbot__update_status)
      items=$(printf '%s' "$input" | jq -c '.tool_input.text // "" | split("\n")
        | map(gsub("\\*\\*"; "") | capture("^\\s*(?<m>[✓✱○])\\s*(?<l>\\S.*)$")?
          | {label: .l, status: ({"✓": "completed", "✱": "in_progress", "○": "pending"}[.m])})' 2>/dev/null)
      [ -n "$items" ] && [ "$items" != "[]" ] && : > "$state_file.hearth" 2>/dev/null ;;
    update_plan)
      items=$(printf '%s' "$input" | jq -c '.tool_input | (if type == "string" then (fromjson? // {}) else . end)
        | .plan // [] | map({label: .step, status})' 2>/dev/null) ;;
    *)
      # TodoWrite, and Cursor's todo tool (merge: true sends only the changed items, matched by id).
      items=$(printf '%s' "$input" | jq -c --slurpfile old <(cat "$state_file.plan" 2>/dev/null || printf '[]') '
        (.tool_input.todos // []) | map({id: (.id // null | if . == null then null else tostring end), label: (.content // .subject), active: .activeForm, status}) as $new
        | if $input_merge and ($old[0] | type) == "array" then
            ($old[0] | map(. as $o | ($new | map(select(.id != null and .id == $o.id)) | first) as $n
              | if $n then $o + ($n | with_entries(select(.value != null))) else $o end))
            + ($new | map(select(.id == null or ([.id] | inside($old[0] | map(.id)) | not))))
          else $new end' --argjson input_merge "$( [ "$(get tool_input.merge)" = true ] && echo true || echo false )" 2>/dev/null) ;;
  esac
  [ -n "$items" ] || return 1
  items=$(printf '%s' "$items" | jq -c 'map(select((.label // "") | length > 0))' 2>/dev/null)
  [ -n "$items" ] || return 1
  printf '%s' "$items" > "$state_file.plan" 2>/dev/null
}

# The saved list as the card's checklist: a Steps widget of at most 12 rows (from just before the first unfinished
# one), and progress as the share finished. Prints {widget, progress}, or nothing without a list.
plan_card() {
  [ -s "$state_file.plan" ] && command -v jq >/dev/null 2>&1 || return 0
  jq -c 'def st: {completed: "done", done: "done", in_progress: "active", active: "active", cancelled: "skipped",
      skipped: "skipped", failed: "failed"}[.status // ""] // "pending";
    map(. + {state: st}) as $all | ($all | length) as $n
    | select($n > 0)
    | ([$all | to_entries[] | select(.value.state != "done" and .value.state != "skipped") | .key] | first // $n) as $at
    | ([0, ([$at - 2, $n - 12] | min)] | max) as $from
    | {widget: {type: "Steps", props: {items: ($all[$from:$from + 12]
        | map({label: ((if .state == "active" and (.active // "") != "" then .active else .label end) | .[0:120]), state}))}},
       progress: (([$all[] | select(.state == "done" or .state == "skipped")] | length) * 100 / $n | floor)}'     "$state_file.plan" 2>/dev/null
}

kind=update state=in_progress title= summary= details= request= why= waiting= offset=0 noop= resumed= short= reply=false stopped_ms= ask_type= ask_tool=
# Only a local Claude Code session can take an answer from the card: its next prompt hands it to Claude. A Projects
# thread is answered in the thread, and so is a thread's session on this computer: nobody writes in it directly.
can_reply=
[ "$harness" = claude ] && [ -z "$projects_session" ] && [ "${BRIEFKIN_REPLIES:-1}" != 0 ] && can_reply=1
# A permission prompt leaves a flag until something shows it was answered (resume) or the turn moved on.
had_ask=; [ -e "$state_file.ask" ] && had_ask=1
[ "$what" = notify ] || [ "$what" = resume ] || rm -f "$state_file.ask" 2>/dev/null
# The desktop app stops an idle session's process and starts it again every couple of minutes (to keep Remote Control
# attached), and each stop is a SessionEnd. Only the first one after the session did something is news.
if [ "$what" = end ]; then
  [ -e "$state_file.ended" ] && exit 0
else
  rm -f "$state_file.ended" 2>/dev/null
fi
# Where this turn starts in the transcript, so its end can tell what the turn did (asked_in_thread).
[ "$what" = prompt ] && [ -n "$transcript" ] && [ -r "$transcript" ] && wc -c < "$transcript" | tr -d ' ' > "$state_file.from" 2>/dev/null
# True when this turn put a question to the person in the thread. In a Projects session the person never reads the
# session's own last message: a thread's session posts to the thread (reply, a decision card), and a session the
# thread started on this computer reports to the thread's assistant, which decides what the person hears.
asked_in_thread() {
  local from; from=$(cat "$state_file.from" 2>/dev/null)
  [[ "$from" =~ ^[0-9]+$ ]] || from=0
  [ -n "$transcript" ] && tail -c +"$((from + 1))" "$transcript" 2>/dev/null | grep -q '"name": \{0,1\}"mcp__hearthbot__\(reply\|ask_decision\)"'
}
# An answer to this session's last question, given on the card or in Telegram, goes to Claude with the next message
# typed in the session (the prompt hook's additionalContext). Only asked while the last turn ended on a question, and
# each answer once (.answered holds the time of the one handed over).
hand_over_answer() {
  [ "$what" = prompt ] && [ -n "$can_reply" ] && command -v jq >/dev/null 2>&1 || return 0
  [ "$(printf '%s' "$last" | jq -r '.kind // ""' 2>/dev/null)" = action ] || return 0
  local key r at
  key=$(printf '%s/%s' "$(printf '%s' "$label" | tr 'A-Z ' 'a-z-')" "$session" | jq -sRr @uri)
  r=$(curl -sS -m 4 "${BRIEFKIN_URL:-https://briefkin.dev}/v1/cards/$key" -H "authorization: Bearer $BRIEFKIN_TOKEN" 2>/dev/null) || return 0
  [ "$(printf '%s' "$r" | jq -r '.answered // false' 2>/dev/null)" = true ] || return 0
  at=$(printf '%s' "$r" | jq -r '.response.at // 0')
  [ "$(cat "$state_file.answered" 2>/dev/null)" = "$at" ] && return 0
  printf '%s' "$at" > "$state_file.answered" 2>/dev/null
  printf '%s' "$r" | jq -c '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext:
    ("The user answered your last question on Briefkin (their dashboard or phone) before writing this message. Their answer: " + (.response.text // ""))}}' 2>/dev/null
}
hand_over_answer

send_event() {
  curl -sS -m 8 -o /dev/null -X POST "${BRIEFKIN_URL:-https://briefkin.dev}/v1/events" \
    -H "authorization: Bearer $BRIEFKIN_TOKEN" -H "content-type: application/json" -H "x-briefkin-hook: $BRIEFKIN_HOOK_VERSION" \
    --data-binary "$1" >/dev/null 2>&1 || true
}
# An ask on the card leaves .open: what was posted, and what can later show it is no longer waiting (the permission
# prompt's call in the transcript, the Projects thread). Any newer report from the session takes it away.
track_ask() {
  command -v jq >/dev/null 2>&1 || return 0
  local f="$state_file.open" was
  was=$(jq -r '.rev // 0' "$f" 2>/dev/null) || was=0
  [ "${was:-0}" -gt "$rev" ] 2>/dev/null && return 0
  if [ "$kind" = action ]; then
    jq -cn --argjson body "$body" --argjson rev "$rev" --arg type "${ask_type:-question}" --arg transcript "$transcript" \
      --arg tool "$ask_tool" --arg link "${projects_link:+$link}" --arg session "$session" --arg task "$task" \
      '{rev: $rev, type: $type, transcript: $transcript, tool: $tool, link: $link, session: $session, task: $task, body: $body}' \
      > "$f" 2>/dev/null || rm -f "$f" 2>/dev/null
  else
    rm -f "$f" 2>/dev/null
  fi
}
# A session can leave an ask on its card that no hook of its own will ever take back: a permission prompt answered "no"
# (the turn stops, so neither PostToolUse nor Stop runs), a session left with its prompt open, a Projects thread that
# went on in a newer session on this computer. So each hook run (at most once a minute) looks at the other sessions'
# open asks and settles the ones this computer can show are no longer waiting. Nothing is cleared on a guess: an ask
# with no such sign stays.
sweep_asks() {
  command -v jq >/dev/null 2>&1 || return 0
  local dir=${TMPDIR:-/tmp} stamp f meta type t tool link sid st sum title r res
  stamp="$dir/briefkin-sweep"
  [ -n "$(find "$stamp" -mmin -1 2>/dev/null)" ] && return 0
  : > "$stamp" 2>/dev/null || return 0
  for f in "$dir"/briefkin-*.open; do
    [ -e "$f" ] && [ "$f" != "$state_file.open" ] || continue
    # The session's own hooks get the first word.
    [ -n "$(find "$f" -mmin -2 2>/dev/null)" ] && continue
    meta=$(cat "$f" 2>/dev/null) || continue
    type=$(printf '%s' "$meta" | jq -r '.type // ""' 2>/dev/null) t=$(printf '%s' "$meta" | jq -r '.transcript // ""' 2>/dev/null)
    tool=$(printf '%s' "$meta" | jq -r '.tool // ""' 2>/dev/null) link=$(printf '%s' "$meta" | jq -r '.link // ""' 2>/dev/null)
    sid=$(printf '%s' "$meta" | jq -r '.session // ""' 2>/dev/null)
    st= sum= title=
    if [ "$type" = permission ] && [[ "$tool" =~ ^[A-Za-z0-9_-]{8,}$ ]] && [ -r "$t" ]; then
      # The prompt was answered: the waiting call has its result. A "no" is a result too.
      res=$(grep -E "\"tool_use_id\": ?\"$tool\"" "$t" 2>/dev/null | tail -n 1)
      if [ -n "$res" ]; then
        title=$(printf '%s' "$meta" | jq -r '.task // ""' 2>/dev/null)
        case "$res" in
          *"doesn't want to proceed"*|*"tool use was rejected"*) sum="You said no to the permission prompt." ;;
          *) sum="You answered the permission prompt." ;;
        esac
        # Still busy (its own hooks will report the rest), or quiet since: then nothing is going on and nothing waits.
        if [ -n "$(find "$t" -mmin -2 2>/dev/null)" ]; then st=in_progress sum=; else st=done; fi
      fi
    elif [ "$type" = question ] && [ -n "$link" ]; then
      # The person wrote in the same Projects thread since, and it reached a newer session on this computer: the
      # question is answered or behind them.
      for g in "$dir"/briefkin-*.link; do
        [ -e "$g" ] && [ "${g%.link}" != "${f%.open}" ] || continue
        [ "$(cat "$g" 2>/dev/null)" = "$link" ] && [ "$g" -nt "$f" ] && { st=done sum="You wrote in the thread since; it went on in a newer session."; break; }
      done
      title=$(printf '%s' "$meta" | jq -r '.task // ""' 2>/dev/null)
    fi
    [ -n "$st" ] || continue
    r=$(( $(perl -MTime::HiRes=time -e 'printf "%d", time * 1000' 2>/dev/null || echo $(( $(date +%s) * 1000 ))) + 5 ))
    body=$(printf '%s' "$meta" | jq -c --arg st "$st" --arg sum "$sum" --arg title "$title" --argjson rev "$r" --arg ev "$sid:sweep:$r" \
      '.body | del(.action) | .kind = "update" | .work_state = $st | .revision = $rev | .event_id = $ev
       | (if $title != "" then .title = $title else . end) | (if $sum != "" then .summary = $sum else del(.summary) end)' 2>/dev/null)
    [ -n "$body" ] || continue
    rm -f "$f" "${f%.open}.ask" 2>/dev/null
    send_event "$body"
  done
  # Permission flags of sessions long gone (each one makes every tool call in every session read its payload).
  find "$dir" -maxdepth 1 -name 'briefkin-*.ask' -mmin +720 -exec rm -f {} + 2>/dev/null
  return 0
}
# A Projects session notes when the person last wrote to its thread (their message, not a relay or a report), for
# sweep_asks.
if [ "$what" = prompt ] && [ -n "$projects_link" ]; then
  case "$(get prompt)" in *'from="human"'*|*'author="user"'*) printf '%s' "$projects_link" > "$state_file.link" 2>/dev/null ;; esac
fi
( sweep_asks ) </dev/null >/dev/null 2>&1 &

case "$what" in
  prompt)
    raw=$text text=$(human_text "$text")
    # Nothing a person wrote (a background agent's report, a PR event, a reminder): the session is working
    # again on the task it is on, so the card goes back to it, named as it was. Without a task, nothing.
    if [ -z "${text//[[:space:]]/}" ]; then
      [ -n "$task" ] || exit 0
      held && exit 0
      title=$task resumed=1
    elif [ -n "$private" ]; then
      # Private sends no text to the model either, so a chat under a rule cannot be checked: it is held.
      [ -n "$check" ] && { : > "$state_file.off"; exit 0; }
      title="Working on a prompt"
    else
      # A short reply ("merge", "yes, go ahead") continues the task the session is on; anything longer
      # names a new one.
      words=$(printf '%s' "$text" | wc -w | tr -d ' ')
      # An answer to the agent's own question goes on with the task, however long.
      case "$raw" in *"<send_user_message_question_reply>"*) words=0 ;; esac
      if [ -n "$task" ] && [ "$words" -le 6 ]; then
        held && exit 0
        # The status line's model has little to go on in "merge" or "yes" and makes up a step: the card keeps the task.
        title=$task short=1
      else
        # A new task in a chat under a rule: the status line's model says whether the rule lets it through.
        checking=$check
        title=$(task_name "$text")
        [ -n "$title" ] || title=$(line "$text" 80)
        task=$title
        printf '%s' "$task" > "$state_file" 2>/dev/null || true
        # A new task starts without the last one's list (a Projects thread keeps its checklist; it is the thread's).
        [ -e "$state_file.hearth" ] || rm -f "$state_file.plan" 2>/dev/null
      fi
      asked=${text:0:3000}
      printf '%s' "$asked" > "$state_file.asked" 2>/dev/null || true
    fi
    ;;
  notify)
    case "$ntype" in
      permission_prompt|ToolPermission)
        kind=action state=blocked offset=1
        title="Waiting for permission"
        request=${private:+Approve or deny a tool call in the session.}
        # Claude Code only says which tool ("Claude needs your permission to use Read"): the call waiting in the transcript
        # says on what, so the person can tell which prompt it is.
        if [ -z "$private" ] && [ "$harness" = claude ] && [ -n "$transcript" ] && command -v jq >/dev/null 2>&1; then
          on=$(tail -c 400000 "$transcript" 2>/dev/null | grep '"tool_use"' | tail -n 1 | jq -r '[.message.content[]? | select(.type == "tool_use")] | last
            | (.input.file_path // .input.notebook_path // .input.command // .input.url // .input.pattern
               // ([.input | .. | strings | select(test("^https?://"))] | first) // "") | tostring | split("\n")[0] | .[0:160]' 2>/dev/null)
          on=${on/#$HOME/\~}
          [ -n "$on" ] && [ -n "$text" ] && text="$text: $on"
        fi
        ask_type=permission
        # The call waiting on the prompt, so a later look at the transcript can tell it was answered (sweep_asks) even
        # when no hook runs after it: a "no" that stops the turn, or a session left open. A call that already has its
        # result is not the one waiting (a background agent's prompt is in that agent's own transcript): none is kept.
        if [ "$harness" = claude ] && [ -n "$transcript" ] && command -v jq >/dev/null 2>&1; then
          ask_tool=$(tail -c 400000 "$transcript" 2>/dev/null | grep '"tool_use"' | tail -n 1 |
            jq -r '[.message.content[]? | select(.type == "tool_use")] | last | .id // ""' 2>/dev/null)
          [[ "$ask_tool" =~ ^[A-Za-z0-9_-]{8,}$ ]] || ask_tool=
          [ -n "$ask_tool" ] && grep -qE "\"tool_use_id\": ?\"$ask_tool\"" "$transcript" 2>/dev/null && ask_tool=
        fi
        request=${request:-${text:-$label needs your permission to continue.}}
        why="The session is paused until you answer in it."
        # A thread's session on this computer asks in the Claude app here, and the thread shows the prompt too.
        [ -n "$projects_session" ] && why="The session is paused until you answer it in the Claude app on this computer or in the thread."
        # A scheduled task runs in the background in the Claude app: nobody has its session open, so say which task it is.
        sched=
        [ -n "$transcript" ] && sched=$(head -c 300000 "$transcript" 2>/dev/null | grep -o -m1 '<scheduled-task name=\\"[^"\\]\{1,80\}' | sed 's/.*name=\\"//')
        [ -n "$sched" ] && why="The scheduled task \"$sched\" is paused until you answer it in the Claude app on this computer (open the task's session there)."
        : > "$state_file.ask" 2>/dev/null || true
        ;;
      # Claude Code's idle_prompt comes a minute after every turn and says nothing the turn's own report did not.
      elicitation_dialog)
        kind=action state=blocked offset=1
        title="Waiting for your input" ask_type=input
        request=${private:+$label is waiting for you in the session.}
        request=${request:-${text:-$label is waiting for your input.}}
        why="Nothing happens until you reply in the session."
        ;;
      *) exit 0 ;;
    esac
    ;;
  plan)
    # A list changed right after a permission prompt: a tool ran, so the prompt was answered, and the card leaves
    # Needs you now even when the list itself is not used.
    if [ -e "$state_file.ask" ]; then
      save_plan
    else
      save_plan || exit 0
      # The status line written for the prompt is still on its way: it carries the list when it lands.
      [ -n "$(find "$state_file.pending" -mmin -2 2>/dev/null)" ] && exit 0
    fi
    rm -f "$state_file.ask" 2>/dev/null
    # The card keeps what it says while the work goes on; after a finished turn or an ask, it is back to the task.
    offset=2 title=${task:-Working}
    cur=$(cat "$state_file.cur" 2>/dev/null)
    if [ "$(printf '%s' "$cur" | jq -r '.kind + ":" + .state' 2>/dev/null)" = update:in_progress ]; then
      title=$(printf '%s' "$cur" | jq -r '.title // ""') summary=$(printf '%s' "$cur" | jq -r '.summary // ""')
      title=${title:-${task:-Working}}
    fi
    ;;
  resume)
    # A tool ran after a permission prompt, so the person answered it: the card is back to the task.
    [ -e "$state_file.ask" ] || exit 0
    rm -f "$state_file.ask" 2>/dev/null
    offset=2 title=${task:-Working}
    ;;
  wait)
    # Hooks 12 to 16 installed a background waiter on Stop; answers now go with the next prompt instead.
    exit 0
    ;;
  end)
    # The session closed: whatever it last said stands, and nothing it asked is waiting on anyone. A Projects thread's
    # session is closed and opened again by the app (idle, memory) while its thread goes on: only its turns say where
    # the work stands.
    # A permission prompt still open is the exception: it goes with the process, so nobody is waiting on it any more.
    if [ -n "$projects_session" ]; then
      [ -n "$had_ask" ] || exit 0
      : > "$state_file.ended" 2>/dev/null
      state=done offset=4 title=${task:-Session ended} summary="The session closed before the permission prompt was answered."
    else
      [ -n "$last$task" ] || exit 0
      : > "$state_file.ended" 2>/dev/null
      state=done offset=4 ending=1
      title=$(printf '%s' "$last" | jq -r '.title // ""' 2>/dev/null) summary=$(printf '%s' "$last" | jq -r '.summary // ""' 2>/dev/null)
      title=${title:-${task:-Session ended}}
    fi
    ;;
  fail)
    state=failed offset=3 title=${task:-Turn failed} summary=$text
    ;;
  halt)
    # The turn was stopped before it finished; nobody is waiting on it. Dated when it stopped, so a prompt sent
    # right after still wins.
    [ -n "$task" ] || exit 0
    state=done offset=3 title=$task summary="Stopped before it finished."
    at=$(get stopped_ms)
    [[ "$at" =~ ^[1-9][0-9]{11,}$ ]] && stopped_ms=$at
    ;;
  stop)
    state=done offset=3 title=${task:-Turn finished}
    if [ -z "$private" ] && [ -n "$text" ]; then
      # A turn that only says there was nothing to do changes nothing on the card.
      if [ ${#text} -lt 600 ] && printf '%s' "$text" | grep -qiE "nothing (to do|for me|needed|to change)|no action\b|no changes? (needed|required)|not (meant )?for (me|this (session|machine))|not work (for|on) this|predates my|(is|was|are|were) for the project assistant"; then
        noop=1
      fi
      parts=$(outcome "$text")
      t=${parts%%$'\037'*} rest=${parts#*$'\037'}
      summary=${rest%%$'\037'*} details=${rest#*$'\037'}
      [ "${BRIEFKIN_DETAILS:-}" = 1 ] || details=
      # The card stays named after the task; what came of it is the summary.
      if [ -n "$task" ]; then
        if [ -n "$t" ]; then
          case "$t" in *[.!?…]) ;; *) t="$t." ;; esac
          # The rest of the first sentence's paragraph joins it; a list or table starts below it.
          case "$summary" in '') summary=$t ;; [-*+\#\>\|0-9\`]*) summary="$t"$'\n\n'"$summary" ;; *) summary="$t $summary" ;; esac
        fi
      else
        title=${t:-$title}
      fi
    fi
    ;;
esac
[ -n "${title//[[:space:]]/}" ] || title="Turn finished"

# Milliseconds, plus a per-event offset (prompt, notify, resume, stop, end) so the order holds even when two
# events share a millisecond. Without perl, seconds * 1000 plus the offset.
ms=$(perl -MTime::HiRes=time -e 'printf "%d", time * 1000' 2>/dev/null) || ms=
[ -n "$ms" ] || ms=$(( $(date +%s) * 1000 ))
ms=${stopped_ms:-$ms}
rev=$(( ms + offset ))
slug=$(printf '%s' "$label" | tr 'A-Z ' 'a-z-')
item_key="$slug/$session"
event_id="$session:$what:$rev"
link=${BRIEFKIN_LINK:-$projects_link}
link_label="Open session"; [ -z "${BRIEFKIN_LINK:-}" ] && [ -n "$projects_link" ] && link_label="Open thread"
# A session of the Claude desktop app (a chat, a scheduled task's run, a thread's session on this computer) opens
# there with claude://code/continue?session=local_<id>. The app keeps one JSON file per session naming the CLI's
# session id; the id found is kept, so the folder is searched until then (recent files only).
app_link= app_label="Open in Claude app"
if [ "$harness" = claude ] && [ "${BRIEFKIN_APP_LINK:-1}" != 0 ]; then
  app_id=$(cat "$state_file.app" 2>/dev/null)
  if [ -z "$app_id" ]; then
    app_dir="$HOME/Library/Application Support/Claude/claude-code-sessions"
    if [ -d "$app_dir" ]; then
      app_id=$(find "$app_dir" -maxdepth 3 -name 'local_*.json' -mtime -3 -exec grep -l -E "\"cliSessionId\": ?\"$session\"" {} + 2>/dev/null | head -n 1)
      app_id=${app_id##*/} app_id=${app_id%.json}
      [[ "$app_id" =~ ^local_[A-Za-z0-9-]{1,64}$ ]] && printf '%s' "$app_id" > "$state_file.app" 2>/dev/null || app_id=
    fi
  fi
  [ -n "$app_id" ] && app_link="claude://code/continue?session=$app_id"
fi
# Other desktop apps: the Codex app opens a thread by the hook's own id (codex://threads/<uuid>); Cursor and
# OpenCode can only open the folder. Only when the app is installed, so the link never leads nowhere.
has_app() { [ -d "/Applications/$1.app" ] || [ -d "$HOME/Applications/$1.app" ]; }
uri_path() { local p=${1//%/%25}; p=${p// /%20}; p=${p//#/%23}; p=${p//\?/%3F}; p=${p//&/%26}; p=${p//+/%2B}; printf '%s' "$p"; }
if [ -z "$app_link" ] && [ "${BRIEFKIN_APP_LINK:-1}" != 0 ]; then
  case "$harness" in
    codex)
      [[ "$session" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]] &&
        { has_app Codex || has_app ChatGPT; } && app_link="codex://threads/$session" app_label="Open in Codex"
      ;;
    cursor)
      case "$cwd" in /?*) has_app Cursor && app_link="cursor://file$(uri_path "$cwd")" app_label="Open in Cursor" ;; esac
      ;;
    opencode)
      case "$cwd" in /?*) has_app OpenCode && app_link="opencode://open-project?directory=$(uri_path "$cwd")" app_label="Open in OpenCode" ;; esac
      ;;
  esac
fi


# Claude Code ends a turn while agents it started in the background are still at work, and a Projects thread
# does this all the time: it hands the work to agents and its turn ends until their reports come in. Each one
# launched ("async_launched" in the transcript) reports back with a task notification naming its id; any id with
# no notification yet is still running. Background shell commands are left out, since many are servers that
# never end. Prints the count.
running_agents() {
  local n=0 id
  if [ "$harness" = claude ] && [ -n "$transcript" ] && [ -r "$transcript" ]; then
    for id in $(grep -F '"async_launched"' "$transcript" 2>/dev/null | grep -o '"\(agentId\|taskId\)":"[A-Za-z0-9_-]*"' | sed 's/^.*:"//; s/"$//' | sort -u); do
      grep -qF "<task-id>$id</task-id>" "$transcript" 2>/dev/null || n=$((n + 1))
    done
  fi
  printf '%s' "$n"
}
# A finished turn with agents still out is not done: the card stays in progress, waiting on them.
# The steps of the saved list not yet finished: a step in progress, and in a Projects thread any step left, since the
# thread's checklist is what its person reads and the thread resolves every line before it is done.
open_steps() {
  [ -s "$state_file.plan" ] && command -v jq >/dev/null 2>&1 || { printf 0; return; }
  jq -r --arg all "${projects_session:+1}" '[.[] | (.status // "") | select(. == "in_progress" or . == "active" or ($all == "1" and (. == "pending" or . == "")))] | length' \
    "$state_file.plan" 2>/dev/null || printf 0
}
hold() {
  [ "$what" = stop ] && [ "$state" = done ] || return 0
  local n; n=$(running_agents)
  if [ "$n" -gt 0 ] 2>/dev/null; then
    state=in_progress waiting="$n background agent$([ "$n" -gt 1 ] && printf s)"
    return 0
  fi
  # A turn that ends with its own list unfinished (a build left running, a step still to do) is not done. Projects
  # threads end turns all the time while they wait on a build, CI or an agent.
  n=$(open_steps)
  [ "$n" -gt 0 ] 2>/dev/null && state=in_progress
  return 0
}

# False for a chat task the rule held back: nothing about it is sent or remembered. A report that comes in while its
# task is still being checked waits for the answer.
cleared() {
  [ -n "$check" ] || return 0
  for _ in $(seq 1 45); do [ -n "$(find "$state_file.checking" -mmin -2 2>/dev/null)" ] || break; sleep 2; done
  [ ! -e "$state_file.off" ]
}

post() {
  cleared || return 0
  hold
  if command -v jq >/dev/null 2>&1; then
    local plan; plan=$(plan_card); [ -n "$plan" ] || plan=null
    jq -cn --arg kind "$kind" --arg state "$state" --arg title "$title" --arg summary "$summary" \
      '{kind: $kind, state: $state, title: $title, summary: $summary}' > "$state_file.cur" 2>/dev/null || true
    body=$(jq -cn --argjson plan "$plan" --arg event_id "$event_id" --arg item_key "$item_key" --arg project "$project" --arg source "$source" \
      --arg kind "$kind" --arg state "$state" --arg title "$title" --arg summary "$summary" --arg details "$details" --arg request "$request" \
      --arg why "$why" --arg waiting "$waiting" --arg link "$link" --arg link_label "$link_label" --arg app_link "$app_link" --arg app_label "$app_label" --argjson rev "$rev" --argjson reply "$reply" '
      {v: 1, event_id: $event_id, item_key: $item_key, project: $project, source: $source, kind: $kind,
       title: $title, work_state: $state, revision: $rev}
      + (if $summary != "" then {summary: $summary} else {} end)
      + (if $details != "" then {details: $details} else {} end)
      + (if $waiting != "" then {waiting_on: $waiting} else {} end)
      + (if $kind == "action" then {action: {request: $request, why: $why, reply: $reply}} else {} end)
      + ([(if $link != "" then {label: $link_label, url: $link} else empty end),
          (if $app_link != "" then {label: $app_label, url: $app_link} else empty end)] | if length > 0 then {links: .} else {} end)
      + (if $plan != null then {widget: $plan.widget, progress: $plan.progress} else {} end)')
  else
    body=$(python3 - "$event_id" "$item_key" "$project" "$source" "$kind" "$state" "$title" "$summary" "$request" "$why" "$waiting" "$link" "$rev" "$details" "$link_label" "$reply" "$app_link" "$app_label" <<'PY'
import json, sys
a = sys.argv[1:]
e = {"v": 1, "event_id": a[0], "item_key": a[1], "project": a[2], "source": a[3], "kind": a[4],
     "title": a[6], "work_state": a[5], "revision": int(a[12])}
if a[7]: e["summary"] = a[7]
if a[13]: e["details"] = a[13]
if a[10]: e["waiting_on"] = a[10]
if a[4] == "action": e["action"] = {"request": a[8], "why": a[9], "reply": a[15] == "true"}
links = ([{"label": a[14], "url": a[11]}] if a[11] else []) + ([{"label": a[17], "url": a[16]}] if a[16] else [])
if links: e["links"] = links
print(json.dumps(e))
PY
  )
  fi
  [ -n "$body" ] || return 0
  track_ask
  send_event "$body"
}

# Asks a small model for the card's status line. Prints nothing (and the hook keeps its own guess) when
# the CLI is missing, slow or answers something unusable.
summarize() {
  local input prompt out r tool rule_text=
  [ -n "$checking" ] && rule_text=$(rule rule)
  input=$(jq -cn --arg event "$what" --arg task "$task" --arg status "$(printf '%s' "$last" | jq -r '.title // ""' 2>/dev/null)" \
    --arg asked "$asked" --arg message "${text:0:6000}" --arg rule "$rule_text" \
    '{event: $event, task: $task, status_before: $status, latest_request: $asked} + (if $event == "stop" then {final_message: $message} else {} end)
     + (if $rule != "" then {rule: $rule} else {} end)')
  prompt=$(printf '%s\n\n%s' "$STATUS_PROMPT" "$input")
  [ -n "$rule_text" ] && prompt=$(printf '%s\n\n%s\n\n%s' "$STATUS_PROMPT" "$RULE_PROMPT" "$input")
  for tool in $(summarizers); do
    out=$(cd "${TMPDIR:-/tmp}" && printf '%s' "$prompt" | BRIEFKIN_HOOK_INNER=1 ask_"$tool" 2>/dev/null) || continue
    r=$(printf '%s' "$out" | perl -0777 -ne 'print $1 if /(\{.*\})/s' | jq -c --arg rule "$rule_text" \
      'select(type == "object" and ((.title // "" | length > 0) or .skip == true) and ($rule == "" or (.send | type) == "boolean"))' 2>/dev/null)
    [ -n "$r" ] && { printf '%s' "$r"; return 0; }
  done
}

# The agents on this machine that can write the status line, cheapest first, from BRIEFKIN_SUMMARIZER
# (default claude,codex,opencode; "off" for none). Each reads the prompt on stdin and prints the answer;
# none of them runs its hooks or plugins (BRIEFKIN_HOOK_INNER stops this script if one does), saves a
# session where it can avoid it, or gets more than a minute.
summarizers() {
  local list=${BRIEFKIN_SUMMARIZER:-claude,codex,opencode} t
  [ "$list" = on ] && list=claude,codex,opencode
  for t in ${list//,/ }; do
    case "$t" in claude|codex|opencode) command -v "$t" >/dev/null 2>&1 && printf '%s\n' "$t" ;; esac
  done
}
timed() { perl -e 'alarm shift; exec @ARGV' 60 "$@"; }
ask_claude() {
  timed claude -p --model "${BRIEFKIN_CLAUDE_MODEL:-haiku}" --no-session-persistence --tools "" \
    --strict-mcp-config --disable-slash-commands --settings '{"disableAllHooks":true}'
}
ask_codex() {
  local f; f=$(mktemp "${TMPDIR:-/tmp}/briefkin-codex.XXXXXX") || return 1
  timed codex exec --ephemeral --skip-git-repo-check --sandbox read-only --color never \
    -c model_reasoning_effort='"low"' ${BRIEFKIN_CODEX_MODEL:+-m "$BRIEFKIN_CODEX_MODEL"} -o "$f" - >/dev/null &&
    cat "$f"
  local rc=$?; rm -f "$f"; return $rc
}
ask_opencode() {
  timed opencode run --pure --title "Briefkin status" ${BRIEFKIN_OPENCODE_MODEL:+-m "$BRIEFKIN_OPENCODE_MODEL"} "$(cat)"
}

STATUS_PROMPT='You write the status line of a card on a dashboard that shows a person where each of their
coding-agent sessions stands. Below is JSON: "event" is "prompt" (the person just sent a request) or "stop"
(the agent just finished its turn), "task" is the card'"'"'s current name, "status_before" is the status line
the card showed after the previous turn, "latest_request" is what the person asked, and "final_message" is
the agent'"'"'s last message.

Answer with one JSON object and nothing else:
{"skip": false, "state": "in_progress|done|needs_you|failed", "title": "...", "summary": "...", "ask": "..."}

- title: at most 70 characters. The current status of the work, as a headline: what happened ("6 UGC reels
  rendered, 4 have garbled subtitles"), what is happening ("Rendering 6 UGC reels"), or what is needed
  ("Pick a fix for the garbled subtitles"). Name the actual thing. Never "I", never who said what, never quote
  or restate the request, never explain or talk about messages, threads, briefs, reports or requests handled
  elsewhere. When the final message reports to another assistant, state what it found or did.
  Only state what the final message says; when part of the work fell short, say so.
- summary: Markdown, at most 2 short sentences or 3 bullets, only facts that change what the person does:
  results, numbers, names, links, the decision needed and its options. Empty when the title says it all.
- state: for "prompt" always "in_progress". For "stop":
  - "needs_you" ONLY when the agent stopped part-way and cannot go on until the person answers a question
    it asked, makes a choice between options it laid out, approves a step it is about to take, or supplies
    something it is missing, and the answer has to come back to this agent. The work is waiting on the person.
    This puts the card at the top of their board and can ping their phone, so be strict.
  - "done" for every other finished turn, including when the final message
    - ends with an offer or a suggestion ("Want me to…?", "Shall I…?", "I can also…", "Let me know if…"),
    - says something is ready for review or to merge, or was opened, deployed or pushed,
    - hands the person a step to take themselves, outside this session ("retarget the MR, then merge", "open the
      two hotfix MRs", "check staging", "run this command"): nothing comes back to the agent, so put it in
      the summary as "Next: …",
    - suggests the person verify, test, check or look at something,
    - says other work goes on (a build, a generation, CI) that the agent is not waiting on.
    Put a suggested next step in the summary as "Next: …", never in ask.
  - "failed" only when the task itself could not be done. A problem with the agent'"'"'s own tools (a browser
    or service it could not reach, a subagent with no output, missing access) is "failed" only if it
    stopped the task; otherwise "done", with the problem in the summary.
- ask: only for "needs_you": the one thing the person must do, as an instruction with its options ("Choose:
  re-roll the 4 clips, crop the lower third, or ship as is"). Not a copy of the title. Otherwise empty.
- summary: never repeat the title or the ask.
- skip: for "stop" only, true when the turn changed nothing about the work (the agent only said a message
  was not for it or was handled elsewhere, acknowledged, or chatted). The card then stays as it is. Never skip a "prompt".'

# Added for a new task in a chat under the person's rule ("rule" in the JSON).
RULE_PROMPT='This request comes from a chat outside any project. The person lets only some chats onto their
dashboard, and "rule" in the JSON says which. Add "send" to your answer: true when the rule lets this request onto
the dashboard, false when the rule keeps it on this computer.
- Judge what the request is about, not single words: "kill the zombie processes" and "the health check returns
  500" are software; "what do my blood test results mean" is the person'"'"'s health.
- Building, fixing or running software is software work, even when the product deals with health, money, dating
  or other private matters.
- Follow the rule as written. When it leaves the request in doubt, keep it (false) only if it would show something
  private about the person: their health, mind, money, relationships, family, legal trouble, job search or private
  life. Otherwise send it.'

# A turn that changed nothing puts back the status the card had before its prompt, so a stray message
# does not leave it "in progress". Never as an ask: the person has written in the session since.
# Two turns can end seconds apart (a background agent's report, then a second notification): the first one's status
# line is still being written when the second ends. So this waits for every status line started before it (each
# leaves a .run.<revision> file while it works) and reads the newest report then, not the one from when it started;
# otherwise the older report comes back with a newer revision and sits above the real one.
# Waits (up to 90 s) for status lines of earlier turns still being written, so what they say is in .last.
wait_runs() {
  local f r n
  for n in $(seq 1 45); do
    r=
    for f in "$state_file".run.*; do
      [ -e "$f" ] || continue
      [ "${f##*.run.}" -lt "$rev" ] 2>/dev/null && [ -n "$(find "$f" -mmin -3 2>/dev/null)" ] && r=1
    done
    [ -n "$r" ] || break
    sleep 2
  done
}
restore() {
  wait_runs
  last=$(cat "$state_file.last" 2>/dev/null || true)
  [ -n "$last" ] || exit 0
  kind=$(printf '%s' "$last" | jq -r '.kind') state=$(printf '%s' "$last" | jq -r '.state') title=$(printf '%s' "$last" | jq -r '.title')
  summary=$(printf '%s' "$last" | jq -r '.summary') request= why= waiting=
  details=
  [ "$kind" = action ] && kind=update state=done
}
# Remembers what a finished turn reported, for restore and for the model's context. A turn's status line can finish
# after a later turn's: the newer report stays.
remember() {
  [ "$what" = stop ] && command -v jq >/dev/null 2>&1 && cleared || return 0
  [ "$(jq -r '.rev // 0' "$state_file.last" 2>/dev/null || echo 0)" -gt "$rev" ] 2>/dev/null && return 0
  jq -cn --arg kind "$kind" --arg state "$state" --arg title "$title" --arg summary "$summary" --arg request "$request" --arg waiting "$waiting" \
    --argjson rev "$rev" '{kind: $kind, state: $state, title: $title, summary: $summary, request: $request, waiting: $waiting, rev: $rev}' \
    > "$state_file.last" 2>/dev/null || true
}
# A session that closes right after a turn (claude -p does) can end before that turn's status line is written: the
# closing report waits for it, so the card keeps what the turn said instead of the task name.
if [ -n "${ending:-}" ] && command -v jq >/dev/null 2>&1 && [ -n "$(find "$state_file".run.* -mmin -3 2>/dev/null)" ]; then
  (
    wait_runs
    last=$(cat "$state_file.last" 2>/dev/null || true)
    t=$(printf '%s' "$last" | jq -r '.title // ""' 2>/dev/null)
    [ -n "$t" ] && title=$t summary=$(printf '%s' "$last" | jq -r '.summary // ""' 2>/dev/null)
    post
  ) </dev/null >/dev/null 2>&1 &
  exit 0
fi
if [ -n "$noop" ]; then
  command -v jq >/dev/null 2>&1 || exit 0
  # In the background: restore may wait for an earlier turn's status line, and the agent never waits on Briefkin.
  ( restore; post ) </dev/null >/dev/null 2>&1 &
  exit 0
fi

# The model runs in the background so the agent never waits on it; the revision above keeps the order. A turn that
# ended without a message has nothing for it to read (the card keeps the task name).
if [ -z "$private" ] && [ "${BRIEFKIN_SUMMARIZER:-on}" != off ] && { { [ "$what" = prompt ] && [ -z "$resumed$short" ]; } || { [ "$what" = stop ] && [ -n "${text//[[:space:]]/}" ]; }; } &&
  command -v jq >/dev/null 2>&1 && command -v perl >/dev/null 2>&1 && [ -n "$(summarizers)" ]; then
  : > "$state_file.pending" 2>/dev/null || true
  : > "$state_file.run.$rev" 2>/dev/null || true
  # Held until the check says otherwise, so a report that slips past it (or a check that never finishes) sends nothing.
  # .checking names the newest task being checked: an older check that finishes later leaves the decision to it.
  mine="$$-$RANDOM"
  [ -n "$checking" ] && { : > "$state_file.off"; printf '%s' "$mine" > "$state_file.checking"; } 2>/dev/null
  (
    trap 'rm -f "$state_file.pending" "$state_file.run.$rev" 2>/dev/null' EXIT
    r=$(summarize)
    if [ -n "$checking" ]; then
      [ "$(cat "$state_file.checking" 2>/dev/null)" = "$mine" ] || exit 0
      [ "$(printf '%s' "$r" | jq -r '.send' 2>/dev/null)" = true ] && rm -f "$state_file.off" 2>/dev/null
      rm -f "$state_file.checking" 2>/dev/null
      [ -e "$state_file.off" ] && exit 0
    fi
    if [ -n "$r" ]; then
      if [ "$what" = stop ] && [ "$(printf '%s' "$r" | jq -r '.skip // false')" = true ]; then
        restore; post; exit 0
      fi
      t=$(printf '%s' "$r" | jq -r '.title // "" | .[0:110]')
      if [ -n "${t//[[:space:]]/}" ]; then
        title=$t summary=$(printf '%s' "$r" | jq -r '.summary // "" | .[0:600]')
        case "$what:$(printf '%s' "$r" | jq -r '.state // ""')" in
          stop:needs_you)
            request=$(printf '%s' "$r" | jq -r '.ask // "" | .[0:500]'); [ -n "${request//[[:space:]]/}" ] || request=$title
            if [ -n "$projects_session" ] && ! asked_in_thread; then
              # The question went to the thread's assistant, not to the person: not theirs to answer here.
              state=done summary="Asked the thread's assistant: $request" request=
            else
              kind=action state=blocked why=
              # server/quiet.ts matches this text: a thread ask leaves Needs you after 30 quiet minutes.
              [ -n "$projects_session" ] && why="Answer in the thread."
              [ -n "$can_reply" ] && reply=true why="Claude gets your answer the next time you write in the session."
            fi ;;
          stop:failed) state=failed ;;
          stop:*) state=done ;;
        esac
      fi
    fi
    remember
    post
  ) </dev/null >/dev/null 2>&1 &
  exit 0
fi
# No model to ask (none installed, status lines off, or BRIEFKIN_PRIVATE): a chat under a rule is held.
[ -n "$checking" ] && { : > "$state_file.off"; exit 0; }
remember
post
exit 0
