Files
timmy-config/bin/quality-verify.sh
STEP35 Burn Agent f32ff627c1 feat(jidoka): implement auto-halt gate for quality drops — stop the line on defect
Implements Jidoka (自働化) — automation with a human touch.
When the agent loop produces defective work, the line stops.

Implementation:
- bin/jidoka-gate.sh — gate script that checks quality of last N completions
- bin/quality-verify.sh — per-issue quality checker (PR exists, has files, mergeable, completion marker)
- Integrated into agent-loop.sh, claude-loop.sh, gemini-loop.sh — runs every JIDOKA_CHECK_INTERVAL completions (default 10)
- If >= JIDOKA_FAIL_THRESHOLD of SAMPLE_SIZE checks fail, a halt flag is created at ~/.hermes/logs/{agent}-jidoka-halt
- Telegram alert is sent on halt via existing bot token mechanism
- bin/claudemax-watchdog.sh updated to respect the halt flag — will NOT restart a halted agent

Configuration via environment:
- JIDOKA_CHECK_INTERVAL (default 10) — completions between gate checks
- JIDOKA_SAMPLE_SIZE (default 5) — how many recent closed issues to sample
- JIDOKA_FAIL_THRESHOLD (default 3) — failures needed to trigger halt

Accepts issue #346 as Closes.

Refs: #345 (Epic: Five Japanese Wisdoms)

Co-authored-by: Timmy <step35@burn.in>
2026-04-29 01:25:09 -04:00

123 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# quality-verify.sh — Verify quality of a completed issue/PR pair.
#
# Usage: quality-verify.sh <issue_num>
# Returns 0 = PASS, 1 = FAIL
#
# Checks:
# 1. Branch still exists on remote
# 2. PR exists
# 3. PR has >0 file changes
# 4. PR is mergeable (no conflicts)
# 5. Issue contains a completion comment marker
set -uo pipefail
ISSUE_NUM="${1:?Usage: $0 <issue_num>}"
REPO_OWNER="Timmy_Foundation"
REPO_NAME="timmy-config"
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="$(cat "${HOME}/.config/gitea/token" 2>/dev/null | tr -d '\n')"
LOG_DIR="${HOME}/.hermes/logs"
mkdir -p "$LOG_DIR"
if [ -z "$GITEA_TOKEN" ]; then
echo "FAIL: #${ISSUE_NUM} — Cannot read Gitea token" >&2
exit 1
fi
# Get branch name from issue
BRANCH=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUM}" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
issue = json.load(sys.stdin)
labels = issue.get('labels', [])
# Try to infer branch from label or title
for lab in labels:
name = lab.get('name','')
if name.startswith('agent:') or name.startswith('issue:'):
print(name)
break
else:
print(f"issue-{issue['number']}")
" 2>/dev/null)
if [ -z "$BRANCH" ] || [ "$BRANCH" = "None" ]; then
BRANCH="issue-${ISSUE_NUM}"
fi
# Check PR linked to branch
PR_NUM=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls?state=all&head=${REPO_OWNER}:${BRANCH}&limit=1" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
prs = json.load(sys.stdin)
print(prs[0]['number'] if prs else '')
" 2>/dev/null)
if [ -z "$PR_NUM" ] || [ "$PR_NUM" = "None" ]; then
# Try issue events for PR reference
PR_NUM=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUM}/events" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
for ev in json.load(sys.stdin):
if ev.get('type') == 'pull_request':
print(ev.get('pull_request', {}).get('number', ''))
break
" 2>/dev/null)
fi
if [ -z "$PR_NUM" ] || [ "$PR_NUM" = "None" ]; then
echo "FAIL: #${ISSUE_NUM} — No PR found for branch ${BRANCH}" >&2
exit 1
fi
# File count (exclude deletions)
FILE_COUNT=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUM}/files" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
files = json.load(sys.stdin)
count = sum(1 for f in files if not f.get('deleted_file'))
print(count)
" 2>/dev/null)
if [ "${FILE_COUNT:-0}" -le 0 ]; then
echo "FAIL: #${ISSUE_NUM} — PR #${PR_NUM} has no real file changes" >&2
exit 1
fi
# Mergeable check
MERGEABLE=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUM}" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
pr = json.load(sys.stdin)
print('true' if pr.get('mergeable') else 'false')
" 2>/dev/null)
if [ "$MERGEABLE" != "true" ]; then
echo "FAIL: #${ISSUE_NUM} — PR #${PR_NUM} is not mergeable (${MERGEABLE})" >&2
exit 1
fi
# Completion comment exists?
HAS_COMPLETION=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUM}/comments" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json, re
comments = json.load(sys.stdin)
markers = ['completion', 'done', 'complete', 'fixed', 'resolved', 'merged', '✓', 'DONE']
for c in reversed(comments):
body = c.get('body','').lower()
if any(m in body for m in markers):
print('found')
break
" 2>/dev/null)
if [ -z "$HAS_COMPLETION" ] || [ "$HAS_COMPLETION" = "None" ]; then
echo "FAIL: #${ISSUE_NUM} — No completion marker in issue comments" >&2
exit 1
fi
echo "PASS: #${ISSUE_NUM} — PR #${PR_NUM} mergeable with ${FILE_COUNT} files"
exit 0