#!/bin/sh
# Savi Comms:  agent commit attribution.
#
# Decision Desk ask_001a0ab6fa59747c179eb5f (the owner, 2026-09-16):  the author
# field stays the person's;  the agent that wrote the commit is named in an
# "Agent: <name>" trailer.  A commit made by a human, with no agent session
# identity in the environment, is left exactly as written.
#
# Two more trailers an agent writes by hand, in the same block:
#   Executes: <ask id>   this commit carries that decision out.
#   Closes: <task id>    this commit finishes that task in the agent's queue;
#                        once it is on origin/main the broker marks it complete
#                        and tells everyone waiting on it.
#   Refs: <task id>      this commit is part of that task and does not finish
#                        it;  nothing closes.  The commit-msg hook refuses an
#                        agent commit carrying neither line for an open task.
#   Whats-New: <one sentence a buyer reads>
#                        this commit changes something a buyer sees.  Start
#                        the sentence with "Improved:" or "Fixed:" to file it
#                        under that heading;  otherwise it goes under New.
#   Roadmap: <item id>   this commit finishes that roadmap item, and what the
#                        item's sentence promises works today.  Partial is not
#                        done.
# python scripts/cut_release.py collects the last two into What's New and the
# roadmap, and scripts/restart_and_verify.sh refuses to deploy until it has.
# An id anywhere else in the message closes nothing.
#
# Placed in a repository by services/git_hooks.py:  the installer, the broker's
# start, onboarding, every wake, and scripts/install_agent_trailer.sh.

msg_file="$1"
[ -n "$msg_file" ] && [ -f "$msg_file" ] || exit 0

# Already carries the trailer (amend, rebase, squash):  leave it alone.
grep -qi '^Agent:[[:space:]]' "$msg_file" && exit 0

# An agent session is one that carries a comms token or runs inside Claude Code.
# A person at their own terminal has neither.
in_session=""
[ -n "$SAVI_COMMS_TOKEN" ] && in_session=1
[ -n "$CLAUDECODE" ] && in_session=1
[ -n "$SAVI_COMMIT_AS_PERSON" ] && in_session=""

agent="$SAVI_AGENT_NAME"
if [ -z "$agent" ] && [ -n "$in_session" ] && command -v comms >/dev/null 2>&1; then
    agent=$(comms whoami 2>/dev/null | sed -n 's/.*"agent"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
fi

if [ -z "$agent" ]; then
    # A person committing:  write nothing, exactly as before.
    [ -n "$in_session" ] || exit 0
    # An agent session that cannot say which agent it is.  Until 2026-10-02 this
    # wrote nothing and exited 0, so the commit landed unattributed with no
    # warning, and the trailer is a guardrail on standing push grants.
    echo "REFUSED:  this commit comes from an agent session, and the session cannot say which agent it is, so it would land with no Agent trailer.  Check that comms is on PATH and that comms whoami answers, then commit again.  A person committing from a Claude Code session runs it with SAVI_COMMIT_AS_PERSON=1." >&2
    exit 1
fi

# Use git's interpret-trailers to handle the trailer block correctly.
# It ensures proper blank-line separation between body and trailer,
# and works correctly for both single-line and multi-line messages.
git interpret-trailers --in-place --trailer "Agent: $agent" "$msg_file"

# The Closes: nudge.  A commit that finishes a task closes it only when it
# carries a Closes: trailer, and almost none did, because nothing at commit
# time said which tasks were open.  So the agent's own tasks reading working
# that the message does not yet close are listed here:  as commented lines in
# the message when an editor opens it (git strips them), and on stderr when the
# message came from -m or -F, where a comment line would land in the commit.
# Which tasks those are is the broker's answer (`comms task closes-hint`,
# services/task_closes.commit_hint);  this hook only lays the lines out.  Any
# failure, an unreachable broker, a slow one, an older comms, no python3, is
# silent:  this never fails or delays a commit by more than a few seconds.
# SAVI_COMMS_NO_CLOSES_HINT=1 turns it off.
if [ -z "$SAVI_COMMS_NO_CLOSES_HINT" ] && command -v comms >/dev/null 2>&1 \
        && command -v python3 >/dev/null 2>&1; then
    python3 - "$msg_file" "$2" <<'HINT' || true
import json, subprocess, sys
msg_file, source = sys.argv[1], (sys.argv[2] if len(sys.argv) > 2 else "")
try:
    out = subprocess.run(["comms", "task", "closes-hint", msg_file], capture_output=True,
                         text=True, timeout=5).stdout
    working = json.loads(out).get("working") or []
except Exception:
    sys.exit(0)
rows = [(str(t.get("id")), str(t.get("title") or "")) for t in working
        if isinstance(t, dict) and t.get("id")]
if not rows:
    sys.exit(0)
if source in ("", "template"):
    lines = ["", "# Your tasks reading working.  If this commit finishes one, uncomment its",
             "# Closes: line so the task closes when the commit reaches origin/main:"]
    for tid, title in rows:
        lines += [f"#   {title}", f"# Closes: {tid}"]
    with open(msg_file, "a", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")
else:
    sys.stderr.write("Your tasks reading working.  If this commit finishes one, add its line "
                     "to the message (git commit --amend) so it closes when the commit reaches "
                     "origin/main:\n")
    for tid, title in rows:
        sys.stderr.write(f"  Closes: {tid}    {title}\n")
HINT
fi
exit 0
