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>
123 lines
3.8 KiB
Bash
Executable File
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
|