beadboard/.agents/skills/create-beads-orchestration/templates/hooks/validate-completion.sh

131 lines
5.3 KiB
Bash

#!/bin/bash
#
# SubagentStop: Enforce bead lifecycle - work verification
#
INPUT=$(cat)
AGENT_TRANSCRIPT=$(echo "$INPUT" | jq -r '.agent_transcript_path // empty')
MAIN_TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
[[ -z "$AGENT_TRANSCRIPT" || ! -f "$AGENT_TRANSCRIPT" ]] && echo '{"decision":"approve"}' && exit 0
# Extract last assistant text response
LAST_RESPONSE=$(tail -200 "$AGENT_TRANSCRIPT" | jq -rs '
[.[] | select(.message?.role == "assistant" and .message?.content != null)
| .message.content[] | select(.text != null) | .text] | last // ""
' 2>/dev/null || echo "")
# === LAYER 1: Extract subagent_type from transcript (fail open) ===
SUBAGENT_TYPE=""
if [[ -n "$AGENT_ID" && -n "$MAIN_TRANSCRIPT" && -f "$MAIN_TRANSCRIPT" ]]; then
PARENT_TOOL_USE_ID=$(grep "\"agentId\":\"$AGENT_ID\"" "$MAIN_TRANSCRIPT" 2>/dev/null | head -1 | jq -r '.parentToolUseID // empty' 2>/dev/null)
if [[ -n "$PARENT_TOOL_USE_ID" ]]; then
SUBAGENT_TYPE=$(grep "\"id\":\"$PARENT_TOOL_USE_ID\"" "$MAIN_TRANSCRIPT" 2>/dev/null | \
grep '"name":"Task"' | \
jq -r '.message.content[]? | select(.type == "tool_use" and .id == "'"$PARENT_TOOL_USE_ID"'") | .input.subagent_type // empty' 2>/dev/null | \
head -1)
fi
fi
# === LAYER 2: Check completion format (backup detection) ===
HAS_BEAD_COMPLETE=$(echo "$LAST_RESPONSE" | grep -cE "BEAD.*COMPLETE" 2>/dev/null || true)
HAS_WORKTREE_OR_BRANCH=$(echo "$LAST_RESPONSE" | grep -cE "(Worktree:|Branch:).*bd-" 2>/dev/null || true)
[[ -z "$HAS_BEAD_COMPLETE" ]] && HAS_BEAD_COMPLETE=0
[[ -z "$HAS_WORKTREE_OR_BRANCH" ]] && HAS_WORKTREE_OR_BRANCH=0
# Determine if this is a supervisor (Layer 1) or has completion format (Layer 2)
IS_SUPERVISOR="false"
[[ "$SUBAGENT_TYPE" == *"supervisor"* ]] && IS_SUPERVISOR="true"
NEEDS_VERIFICATION="false"
[[ "$IS_SUPERVISOR" == "true" ]] && NEEDS_VERIFICATION="true"
[[ "$HAS_BEAD_COMPLETE" -ge 1 && "$HAS_WORKTREE_OR_BRANCH" -ge 1 ]] && NEEDS_VERIFICATION="true"
# Skip verification if not needed
[[ "$NEEDS_VERIFICATION" == "false" ]] && echo '{"decision":"approve"}' && exit 0
# Worker supervisor is exempt
[[ "$SUBAGENT_TYPE" == *"worker"* ]] && echo '{"decision":"approve"}' && exit 0
# === VERIFICATION CHECKS ===
# Check 1: Completion format required for supervisors
if [[ "$IS_SUPERVISOR" == "true" ]] && [[ "$HAS_BEAD_COMPLETE" -lt 1 || "$HAS_WORKTREE_OR_BRANCH" -lt 1 ]]; then
cat << 'EOF'
{"decision":"block","reason":"Work verification failed: completion report missing.\n\nRequired format:\nBEAD {BEAD_ID} COMPLETE\nWorktree: .worktrees/bd-{BEAD_ID}\nFiles: [list]\nTests: pass\nSummary: [1 sentence]"}
EOF
exit 0
fi
# Extract BEAD_ID from response
BEAD_ID_FROM_RESPONSE=$(echo "$LAST_RESPONSE" | grep -oE "BEAD [A-Za-z0-9._-]+" | head -1 | awk '{print $2}')
IS_EPIC_CHILD="false"
[[ "$BEAD_ID_FROM_RESPONSE" == *"."* ]] && IS_EPIC_CHILD="true"
# Check 2: Comment required
HAS_COMMENT=$(grep -c '"bd comment\|"command":"bd comment' "$AGENT_TRANSCRIPT" 2>/dev/null) || HAS_COMMENT=0
if [[ "$HAS_COMMENT" -lt 1 ]]; then
cat << 'EOF'
{"decision":"block","reason":"Work verification failed: no comment on bead.\n\nRun: bd comment {BEAD_ID} \"Completed: [summary]\""}
EOF
exit 0
fi
# Check 3: Worktree verification
REPO_ROOT=$(cd "$(git rev-parse --git-common-dir)/.." 2>/dev/null && pwd)
WORKTREE_PATH="$REPO_ROOT/.worktrees/bd-${BEAD_ID_FROM_RESPONSE}"
if [[ ! -d "$WORKTREE_PATH" ]]; then
cat << 'EOF'
{"decision":"block","reason":"Work verification failed: worktree not found.\n\nCreate worktree first via API."}
EOF
exit 0
fi
# Check 4: Uncommitted changes
UNCOMMITTED=$(git -C "$WORKTREE_PATH" status --porcelain 2>/dev/null)
if [[ -n "$UNCOMMITTED" ]]; then
cat << 'EOF'
{"decision":"block","reason":"Work verification failed: uncommitted changes.\n\nRun in worktree:\n git add -A && git commit -m \"...\""}
EOF
exit 0
fi
# Check 5: Remote push
HAS_REMOTE=$(git -C "$WORKTREE_PATH" remote get-url origin 2>/dev/null)
if [[ -n "$HAS_REMOTE" ]]; then
BRANCH="bd-${BEAD_ID_FROM_RESPONSE}"
REMOTE_EXISTS=$(git -C "$WORKTREE_PATH" ls-remote --heads origin "$BRANCH" 2>/dev/null)
if [[ -z "$REMOTE_EXISTS" ]]; then
cat << 'EOF'
{"decision":"block","reason":"Work verification failed: branch not pushed.\n\nRun: git push -u origin bd-{BEAD_ID}"}
EOF
exit 0
fi
fi
# Check 6: Bead status
BEAD_STATUS=$(bd show "$BEAD_ID_FROM_RESPONSE" --json 2>/dev/null | jq -r '.[0].status // "unknown"')
EXPECTED_STATUS="inreview"
# Epic children also use inreview (done status not supported in bd)
if [[ "$BEAD_STATUS" != "$EXPECTED_STATUS" ]]; then
cat << EOF
{"decision":"block","reason":"Work verification failed: bead status is '${BEAD_STATUS}'.\n\nRun: bd update ${BEAD_ID_FROM_RESPONSE} --status ${EXPECTED_STATUS}"}
EOF
exit 0
fi
# Check 7: Verbosity limit
DECODED_RESPONSE=$(printf '%b' "$LAST_RESPONSE")
LINE_COUNT=$(echo "$DECODED_RESPONSE" | wc -l | tr -d ' ')
CHAR_COUNT=${#DECODED_RESPONSE}
if [[ "$LINE_COUNT" -gt 15 ]] || [[ "$CHAR_COUNT" -gt 800 ]]; then
cat << EOF
{"decision":"block","reason":"Work verification failed: response too verbose (${LINE_COUNT} lines, ${CHAR_COUNT} chars). Max: 15 lines, 800 chars."}
EOF
exit 0
fi
echo '{"decision":"approve"}'