refactor: jsc-herdr 技能改名,herdr 改為 cmd、gitea-issue 改為 gitea

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-10-14 04:50:49 +00:00
co-authored by Claude Sonnet 5.5
parent 5701b8b8e5
commit 346cbbad2c
16 changed files with 18 additions and 18 deletions
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# gitea 共用函式。每個腳本都 source 這個檔。
set -euo pipefail
LABEL="in-progress"
LABEL_COLOR="0e8a16"
LABEL_DESC="Herdr 的 agent 正在實作"
INTERVAL="${HERDR_ISSUE_INTERVAL:-300}"
MODEL="${HERDR_ISSUE_MODEL:-claude-sonnet-5-5}"
SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
die() { echo "gitea: $*" >&2; exit 1; }
# tea 的預設使用者。在 clone 裡 tea 會依 remote URL 挑第一個符合的 login(可能不是預設的),
# 所以明確取 default 那個 login,所有 tea 呼叫都加 --login。可用 HERDR_ISSUE_LOGIN 覆寫。
if [ -z "${HERDR_ISSUE_LOGIN:-}" ]; then
HERDR_ISSUE_LOGIN="$(tea login list --output json | python3 -c "import sys,json
print(next((l['name'] for l in json.load(sys.stdin) if str(l.get('default')).lower()=='true'),''))")"
fi
[ -n "$HERDR_ISSUE_LOGIN" ] || { echo "gitea: tea 沒有預設 login,請先 tea login default <name>" >&2; exit 1; }
export HERDR_ISSUE_LOGIN
# 忽略所有 TLS 檢查:tea 的 insecure 只能寫在設定檔(沒有環境變數),所以把所有 login 設成 insecure: true(冪等);
# git 用 GIT_SSL_NO_VERIFY。agent 的 tea/git 也會吃到同一份設定。
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
if [ -f "$TEA_CONFIG" ] && grep -qE '^\s*insecure:\s*false' "$TEA_CONFIG"; then
sed -i -E 's/^(\s*insecure:\s*)false/\1true/' "$TEA_CONFIG"
fi
export GIT_SSL_NO_VERIFY=true
# git 走 HTTPS 時沒有帳密又不能互動輸入會失敗,所以腳本內的 git 一律用 tea 預設 login 的 token(git-credential.sh)。
export GIT_TERMINAL_PROMPT=0
git() { command git -c http.sslVerify=false -c credential.helper= -c "credential.helper=$SCRIPTS/git-credential.sh" "$@"; }
# TLS/憑證錯誤多半是暫時的(連線被重置、代理、時鐘),遇到就重試幾次,不要讓輪詢中的腳本因此中斷;
# 其他錯誤原樣回傳。(憑證驗證已關閉,仍保留重試以處理連線被重置等暫時性錯誤。)重試次數 HERDR_ISSUE_TLS_RETRIES(預設 5),間隔 3 秒。
tea() {
local out err rc=0 try max="${HERDR_ISSUE_TLS_RETRIES:-5}" ef; ef="$(mktemp)"
for ((try = 1; try <= max; try++)); do
out="$(command tea "$1" --login "$HERDR_ISSUE_LOGIN" "${@:2}" 2>"$ef")" && rc=0 || rc=$?
err="$(cat "$ef")"
if [ "$rc" -eq 0 ] || ! grep -qiE 'tls|x509|certificate|handshake' <<<"$out$err"; then break; fi
echo "gitea: tea 遇到 TLS/憑證錯誤(第 $try/$max 次),3 秒後重試" >&2
rc=${rc/#0/1}; sleep 3
done
rm -f "$ef"
[ -z "$err" ] || printf '%s\n' "$err" >&2
printf '%s\n' "$out"
return "$rc"
}
# 從 stdin 讀 JSON,執行 python 片段,d 是解析後的物件。
jq_py() { python3 -c "import sys,json
def find(o,k):
if isinstance(o,dict):
if k in o: return o[k]
for v in o.values():
r=find(v,k)
if r is not None: return r
elif isinstance(o,list):
for v in o:
r=find(v,k)
if r is not None: return r
return None
d=json.load(sys.stdin)
$1"; }
# 議題的完整 JSON(state、labels、body、title、closedAt)。
issue_json() { tea issues "$2" --repo "$1" --output json; }
issue_state() { issue_json "$1" "$2" | jq_py 'print(d["state"])'; }
issue_labels() { issue_json "$1" "$2" | jq_py 'print("\n".join(l["name"] for l in d.get("labels") or []))'; }
# 某個標籤底下還有幾個 open 議題。
open_count_with_label() {
tea issues list --repo "$1" --state open --labels "$2" --limit 500 --output json --fields index \
| jq_py 'print(len(d))'
}
ensure_label() {
if ! tea labels list --repo "$1" --output json | jq_py "sys.exit(0 if any(l['name']=='$LABEL' for l in d) else 1)"; then
tea labels create --repo "$1" --name "$LABEL" --color "$LABEL_COLOR" --description "$LABEL_DESC" >/dev/null
fi
}
# agent 狀態:idle|working|blocked|done|unknown|missing
agent_status() {
herdr agent get "issue-$1" 2>/dev/null | jq_py 'print(find(d,"agent_status") or "unknown")' 2>/dev/null || echo missing
}
# 標籤為 #N 的 tab ID,沒有就空字串。
tab_for_issue() {
herdr tab list --workspace "${HERDR_WORKSPACE_ID:?}" \
| jq_py "print(next((t['tab_id'] for t in find(d,'tabs') or [] if t.get('label')=='#$1'),''))"
}
# worktree 路徑:clone 同層的 <repo>-issue-N
worktree_path() { echo "$(dirname "$2")/$(basename "$2")-issue-$1"; }
notify() { herdr notification show "$1" --body "$2" --sound "${3:-request}" >/dev/null 2>&1 || true; }
# 目前目錄必須是 owner/repo 的 clone;印出 clone 根目錄。
clone_root_for() {
local root url
root="$(git rev-parse --show-toplevel 2>/dev/null)" || die "目前目錄不是 git repo,請在 $1 的 clone 裡執行"
url="$(git -C "$root" remote get-url origin 2>/dev/null || true)"
case "$url" in
*"$1"|*"$1.git"|*"$1/") echo "$root" ;;
*) die "目前 clone 的 origin 是 $url,不是 $1" ;;
esac
}
# head 分支為 issue-N 的最新 PR:印出「index state」或空字串。
pr_for_issue() {
tea pulls list --repo "$1" --state all --limit 500 --output json --fields index,state,mergeable,head \
| jq_py "m=[p for p in d if p.get('head')=='issue-$2']
print(' '.join([m[0]['index'],m[0]['state'],str(m[0].get('mergeable'))]) if m else '')"
}
# 擋住議題 N 的 open 依賴(Gitea 原生 issue dependencies),一行一個:同 repo 印 #M,跨 repo 印 owner/repo#M。
# tea api 遇到 HTTP 錯誤仍 exit 0,所以用回應是否為 JSON 陣列判斷成敗;失敗就 die。
open_blockers() {
tea api --repo "$1" "/repos/{owner}/{repo}/issues/$2/dependencies" | python3 -c "import sys,json
raw=sys.stdin.read()
try: d=json.loads(raw)
except Exception: d=None
if not isinstance(d,list):
sys.stderr.write('gitea: 讀取 #$2 的依賴失敗:'+raw.strip()[:300]+'\n'); sys.exit(1)
for i in d:
if i.get('state')=='open':
r=(i.get('repository') or {}).get('full_name','')
print(('#' if r in ('','$1') else r+'#')+str(i['number']))"
}