refactor: 技能統一收進 jsc-skills,加入 herdr,版本統一為 0.0.1
- 新增 jsc-skills plugin,含 herdr、herdr-issue、ingest,以 /jsc-skills:<名稱> 呼叫 - ingest 自 companion 移入,移除獨立的 herdr-issue plugin - companion 版本由 0.0.3 改為 0.0.1 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6ae82d1889
commit
26fbf9ae30
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "jsc-skills",
|
||||
"version": "0.0.1",
|
||||
"description": "JSC 技能集:herdr、herdr-issue、ingest",
|
||||
"license": "MIT",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
"name": "JSC"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: herdr-issue
|
||||
description: "Run Gitea issues through Herdr: one Claude agent per issue in its own worktree and tab, watch the issue to closure, then offer the next unblocked issues. Use only when the user runs /herdr-issue or explicitly asks to drive a Gitea issue or repo link with Herdr."
|
||||
---
|
||||
|
||||
# herdr-issue
|
||||
|
||||
把 Gitea 議題交給 Herdr 裡的 Claude agent 實作。所有確定性的工作都在 `scripts/` 裡,你只在「要做決定」的時刻出手。腳本都要在該 repo 的 clone 裡執行;`S` 代表 `~/.claude/skills/herdr-issue/scripts`。
|
||||
|
||||
## 使用者身分
|
||||
|
||||
Gitea 一律以 tea 的預設 login(`tea login list` 中 default 為 true 的那個)操作:`lib.sh` 會取出它並讓所有 tea 呼叫加上 `--login`,避免在 clone 裡 tea 依 remote URL 挑到別的帳號。`start.sh` 也會把該使用者的名稱與 email 設成 worktree 的 git commit 身分。git 走 HTTPS 的帳密由 `scripts/git-credential.sh`(git credential helper)提供,用同一個 login 的 token:腳本內的 git 自動套用,`start.sh` 也會把它寫進 clone 的本地設定(只對該 Gitea 主機),所以 agent 在 worktree 裡 push/fetch 不會再出現 `could not read Username`。要換人先 `tea login default <name>`,或設 `HERDR_ISSUE_LOGIN`。
|
||||
|
||||
## 語言規則
|
||||
|
||||
本技能的所有回覆、提問、通知與輸出訊息一律使用繁體中文(程式碼、指令、識別字除外)。
|
||||
|
||||
## 前置檢查
|
||||
|
||||
1. `test "${HERDR_ENV:-}" = 1`,不成立就說明不在 Herdr 裡並停止。
|
||||
2. 解析輸入:
|
||||
- 議題連結 `https://gitea.jsc.idv.tw/<owner>/<repo>/issues/<N>` → 流程 A。
|
||||
- `#N` → 用目前 clone 的 origin 推出 owner/repo,流程 A。
|
||||
- repo 連結 `https://gitea.jsc.idv.tw/<owner>/<repo>` → 流程 B。
|
||||
3. 目前目錄必須是該 repo 的 clone(腳本會比對 origin)。不是就告訴使用者要在哪個目錄執行並停止。
|
||||
4. 提醒一句:這個 session 要保持開著,監看與下一步推薦都靠它。
|
||||
|
||||
## 流程 A:啟動一個議題
|
||||
|
||||
1. `S/start.sh <owner/repo> <N>`。它會建 worktree `../<repo>-issue-N`(分支已存在就接續)、開 tab `#N`、啟動 `issue-N` agent、送提示詞(`scripts/prompt.txt`)並等 agent 進入 `working` 或 `blocked`,確認有反應後才掛 `in-progress` 標籤。最後一行是 JSON,`state` 欄位是 agent 當時的狀態(`blocked` 表示一開始就卡在對話框,watch-issue.sh 會通知)。失敗(agent 啟動不了、送出後沒反應)時它已自己回滾:關 tab、移除 worktree、刪掉本次新建的分支,議題等同沒啟動過。把 stderr 的錯誤照實轉述,**不要重跑**,跳過第 2 步直接做「推薦下一步」;這個議題會重新出現在「可執行」清單,要重試由使用者再勾。
|
||||
2. 在**背景**執行 `S/watch-issue.sh <owner/repo> <N>`(Bash 的 `run_in_background`)。它會阻塞到議題關閉,期間 agent blocked 或消失會彈 Herdr 通知;關閉後自己關 tab、清 worktree 與分支、拿掉標籤、往上檢查規格與總覽能不能關。結束時你會被叫醒。
|
||||
3. 做一次「推薦下一步」。
|
||||
|
||||
## 阻擋規則
|
||||
|
||||
議題之間的阻擋只認 Gitea 原生的 issue dependencies(議題頁右側的「依賴」),不看內文。有任何一個 open 的依賴(跨 repo 也算)就是被阻擋:`ready.sh` 不會列出它,`start.sh` 會拒絕啟動。依賴的議題一關閉就解鎖,不看 PR 有沒有合併。讀依賴失敗時 `ready.sh` 整個停下並印出錯誤,不猜。
|
||||
|
||||
## 流程 B:從 repo 挑議題
|
||||
|
||||
1. `S/ready.sh <owner/repo>`。
|
||||
2. 「可執行」是空的就直接說「目前沒有可執行的議題」,不出多選題,但「執行中」和「掛 in-progress 但沒 agent」照常轉述。否則用多選問題列出「可執行」的議題,每項標題帶規格標籤;同一規格的議題在描述裡註明「同規格,PR 可能互相衝突」。「掛 in-progress 但沒有 agent」的議題另列一題,說明重跑會接續先前分支。
|
||||
3. 勾選的每一個議題各走流程 A 的第 1、2 步。其中一個 `start.sh` 失敗不影響其餘的,繼續啟動下一個;全部跑完後把失敗的議題與原因一併轉述,再做一次「推薦下一步」。
|
||||
|
||||
## 推薦下一步
|
||||
|
||||
`S/ready.sh <owner/repo>` 再跑一次(剛啟動的議題已經在執行中,會被排除),結果用多選問題呈現,格式同流程 B(可執行為空就直說,不出題)。沒勾就什麼都不做。
|
||||
|
||||
## 被 watch-issue.sh 叫醒時
|
||||
|
||||
把它印出的清理摘要(PR 狀態、分支去留、規格與總覽是否關閉)用兩三句轉述,然後「推薦下一步」。
|
||||
|
||||
## 腳本一覽
|
||||
|
||||
| 腳本 | 給誰 | 做什麼 |
|
||||
| --- | --- | --- |
|
||||
| `ready.sh` | 你 | 列出可執行、執行中、掛 in-progress 但沒 agent 的議題(被阻擋的不列) |
|
||||
| `start.sh` | 你 | 檢查沒被阻擋後建 worktree、開 tab、啟動 agent、送提示詞並確認 agent 有反應、掛標籤;失敗自動回滾 |
|
||||
| `watch-issue.sh` | 你(背景) | 等議題關閉,清理,往上關規格與總覽 |
|
||||
| `tick.sh` | 子 agent | 勾議題內文第 k 個核取方塊 |
|
||||
| `pr-wait.sh` | 子 agent(背景) | 等 PR 衝突、關閉或合併 |
|
||||
|
||||
TLS/憑證錯誤:`lib.sh` 的 tea 包裝會自動重試(預設 5 次、間隔 3 秒),輪詢中的 `watch-issue.sh`、`pr-wait.sh` 因此不會被一次性的連線問題打斷;另外**一律忽略 TLS 憑證檢查**:`lib.sh` 會把 tea 設定檔(`~/.config/tea/config.yml`)所有 login 的 `insecure` 設為 `true`,git 則用 `GIT_SSL_NO_VERIFY` 與 worktree 的 `http.sslVerify=false`。
|
||||
|
||||
環境變數:`HERDR_ISSUE_TLS_RETRIES`(TLS 重試次數,預設 5)、`HERDR_ISSUE_INTERVAL`(pr-wait 輪詢秒數,預設 300)、`HERDR_ISSUE_WATCH_INTERVAL`(watch-issue 輪詢秒數,預設 5)、`HERDR_ISSUE_BLOCKED_AFTER`(blocked 多久才通知,預設 60 秒)、`HERDR_ISSUE_MODEL`(預設 `claude-sonnet-5-5`)。
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# git credential helper:用 tea 預設 login 的帳號與 token 回應 HTTPS 認證,避免 git 要互動輸入帳密
|
||||
# ("could not read Username ... No such device or address")。只處理 get,且只回應該 login 的主機。
|
||||
[ "${1:-}" = get ] || exit 0
|
||||
exec python3 -c '
|
||||
import os, sys
|
||||
req = dict(l.split("=", 1) for l in sys.stdin.read().splitlines() if "=" in l)
|
||||
cfg = os.path.join(os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config"), "tea", "config.yml")
|
||||
logins, cur = [], None
|
||||
for line in open(cfg):
|
||||
s = line.strip()
|
||||
if s.startswith("- name:"):
|
||||
cur = {"name": s.split(":", 1)[1].strip()}; logins.append(cur)
|
||||
elif cur is not None and ":" in s:
|
||||
k, v = s.split(":", 1)
|
||||
if k in ("url", "token", "user", "default"): cur[k] = v.strip().strip("\"\x27")
|
||||
want = os.environ.get("HERDR_ISSUE_LOGIN")
|
||||
pick = next((l for l in logins if l["name"] == want), None) if want else None
|
||||
pick = pick or next((l for l in logins if l.get("default") == "true"), None)
|
||||
if not pick or not pick.get("token"): sys.exit(0)
|
||||
host = pick["url"].split("://", 1)[-1].rstrip("/")
|
||||
if req.get("host") != host: sys.exit(0)
|
||||
print("username=" + pick["user"]); print("password=" + pick["token"])
|
||||
'
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# herdr-issue 共用函式。每個腳本都 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 "herdr-issue: $*" >&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 "herdr-issue: 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 "herdr-issue: 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('herdr-issue: 讀取 #$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']))"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# 用法:pr-wait.sh <owner/repo> <N>
|
||||
# 阻塞到 head 為 issue-N 的 PR 有衝突、被關閉或合併,或議題被關閉。
|
||||
# 最後一行印出原因:conflict | closed | merged | issue-closed
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
REPO="${1:?}"; N="${2:?用法:pr-wait.sh <owner/repo> <N>}"
|
||||
while :; do
|
||||
read -r PR STATE MERGEABLE <<<"$(pr_for_issue "$REPO" "$N")" || true
|
||||
if [ -z "${PR:-}" ]; then
|
||||
echo "還沒有 head 為 issue-$N 的 PR,$INTERVAL 秒後再查"
|
||||
else
|
||||
case "$STATE" in
|
||||
merged) echo "PR #$PR 已合併"; echo merged; exit 0 ;;
|
||||
closed) echo "PR #$PR 被關閉(未合併)"; echo closed; exit 0 ;;
|
||||
open)
|
||||
if [ "$MERGEABLE" = "false" ]; then
|
||||
sleep 60
|
||||
read -r _ _ AGAIN <<<"$(pr_for_issue "$REPO" "$N")"
|
||||
if [ "$AGAIN" = "false" ]; then echo "PR #$PR 與 main 有衝突"; echo conflict; exit 0; fi
|
||||
fi ;;
|
||||
esac
|
||||
fi
|
||||
if [ "$(issue_state "$REPO" "$N")" = "closed" ]; then echo "議題 #$N 已關閉"; echo issue-closed; exit 0; fi
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
@@ -0,0 +1,18 @@
|
||||
請實作這個 Gitea 議題:{LINK}(先用 tea issues 讀完整內文與留言,依 Acceptance criteria 逐項完成)。
|
||||
|
||||
你在分支 issue-{N} 的 worktree 裡,從 origin/main 建立,請在這裡實作,不要另外建 worktree。
|
||||
{RESUME}
|
||||
規則:
|
||||
0. 一律以 tea 的預設使用者 {LOGIN} 操作 Gitea(tea 指令加 --login {LOGIN}),git commit 身分已設為同一人。
|
||||
所有回覆、commit 訊息、PR 標題與描述、議題留言一律使用繁體中文(程式碼與指令除外)。
|
||||
1. 議題的代辦項目是內文 Acceptance criteria 的核取方塊。每完成一項就執行
|
||||
{S}/tick.sh {REPO} {N} <第幾項>(從第一個核取方塊數起,已勾的也算)
|
||||
它會重新讀取最新內文再改,不要自己用 tea issues edit 重寫整段。
|
||||
2. 全部完成後推送分支並建立 PR 回 main:
|
||||
tea pulls create --login {LOGIN} --repo {REPO} --base main --head issue-{N} --title "<標題>" --description "<摘要>
|
||||
|
||||
Closes #{N}"
|
||||
3. 然後在背景執行 {S}/pr-wait.sh {REPO} {N},它會在 PR 有衝突、被關閉或合併時結束並印出原因:
|
||||
- conflict:git fetch origin && git merge origin/main,用 /mattpocock-skills:resolving-merge-conflicts 解決,跑測試,推送,再執行一次 pr-wait.sh。
|
||||
- closed:tea issues close --login {LOGIN} --repo {REPO} {N},結束。
|
||||
- merged 或 issue-closed:結束。
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""列出可執行的議題。輸入:open.json agents.json tabs.json owner/repo
|
||||
阻擋關係只認 Gitea 原生 issue dependencies(tea api),不看議題內文。"""
|
||||
import os, json, re, subprocess, sys
|
||||
|
||||
open_issues = json.load(open(sys.argv[1]))
|
||||
agents = json.load(open(sys.argv[2]))
|
||||
tabs = json.load(open(sys.argv[3]))
|
||||
repo = sys.argv[4]
|
||||
|
||||
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
|
||||
|
||||
def labels(i): return [l.strip() for l in (i.get("labels") or "").split() if l.strip()] if isinstance(i.get("labels"), str) else [l["name"] for l in i.get("labels") or []]
|
||||
|
||||
# Herdr 裡活著的 agent:名稱 issue-N,或掛在標籤 #N 的 tab 上
|
||||
running = set()
|
||||
for a in find(agents, "agents") or []:
|
||||
m = re.fullmatch(r"issue-(\d+)", str(a.get("name") or ""))
|
||||
if m: running.add(m.group(1))
|
||||
agent_tabs = {a.get("tab_id") for a in find(agents, "agents") or []}
|
||||
for t in find(tabs, "tabs") or []:
|
||||
m = re.fullmatch(r"#(\d+)", str(t.get("label") or ""))
|
||||
if m and t.get("tab_id") in agent_tabs: running.add(m.group(1))
|
||||
|
||||
def open_blockers(n):
|
||||
"""擋住 #n 的 open 依賴。tea api 遇到 HTTP 錯誤仍 exit 0,所以回應不是陣列就當失敗,整個停下。"""
|
||||
raw = subprocess.run(["tea", "api", "--login", os.environ["HERDR_ISSUE_LOGIN"], "--repo", repo, f"/repos/{{owner}}/{{repo}}/issues/{n}/dependencies"],
|
||||
capture_output=True, text=True).stdout
|
||||
try: d = json.loads(raw)
|
||||
except Exception: d = None
|
||||
if not isinstance(d, list):
|
||||
sys.exit(f"herdr-issue: 讀取 #{n} 的依賴失敗:{raw.strip()[:300]}")
|
||||
out = []
|
||||
for i in d:
|
||||
if i.get("state") != "open": continue
|
||||
r = (i.get("repository") or {}).get("full_name", "")
|
||||
out.append(("#" if r in ("", repo) else r + "#") + str(i["number"]))
|
||||
return out
|
||||
|
||||
def spec(i):
|
||||
return next((l for l in labels(i) if l.startswith("規格/#") or l.startswith("總覽/#")), "")
|
||||
|
||||
runnable, stale = [], []
|
||||
for i in sorted(open_issues, key=lambda x: int(x["index"])):
|
||||
ls = labels(i)
|
||||
if "ready-for-agent" not in ls: continue
|
||||
if "類型/規格" in ls or "類型/總覽" in ls: continue
|
||||
n = i["index"]
|
||||
if n in running: continue
|
||||
if "in-progress" in ls:
|
||||
stale.append(n); continue
|
||||
if open_blockers(n): continue
|
||||
runnable.append((n, spec(i), i["title"]))
|
||||
|
||||
if runnable:
|
||||
print(f"可執行({len(runnable)}):編號\t規格\t標題")
|
||||
for n, s, t in runnable:
|
||||
print(f"#{n}\t{s}\t{t}")
|
||||
else:
|
||||
print("目前沒有可執行的議題。")
|
||||
print()
|
||||
print("Herdr 執行中:" + (" ".join(f"#{n}" for n in sorted(running, key=int)) or "無"))
|
||||
if stale:
|
||||
print("掛 in-progress 但沒有 agent(需處理,重跑 start 會接續):" + " ".join(f"#{n}" for n in stale))
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# 用法:ready.sh <owner/repo>
|
||||
# 列出可執行的議題(open、ready-for-agent、Gitea 原生依賴全關、Herdr 沒在跑、沒掛 in-progress)。被阻擋的不列。
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
REPO="${1:?用法:ready.sh <owner/repo>}"
|
||||
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||
tea issues list --repo "$REPO" --state open --limit 500 --output json --fields index,title,labels,body > "$T/open.json"
|
||||
herdr agent list > "$T/agents.json"
|
||||
herdr tab list --workspace "${HERDR_WORKSPACE_ID:?}" > "$T/tabs.json"
|
||||
python3 "$SCRIPTS/ready.py" "$T/open.json" "$T/agents.json" "$T/tabs.json" "$REPO"
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# 用法:start.sh <owner/repo> <N>
|
||||
# 在目前 clone 旁建 worktree、開 Herdr tab、啟動 claude、送提示詞並確認 agent 有反應、掛 in-progress 標籤。
|
||||
# 送出提示詞後 15 秒內沒觀察到 working 或 blocked 就視為失敗並回滾(關 tab、移除 worktree、刪本次新建的分支)。
|
||||
# 最後一行印出 JSON:{"tab":..,"pane":..,"path":..,"resumed":..,"state":"working"|"blocked"}
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
REPO="${1:?}"; N="${2:?用法:start.sh <owner/repo> <N>}"
|
||||
LINK="https://gitea.jsc.idv.tw/$REPO/issues/$N"
|
||||
test "${HERDR_ENV:-}" = 1 || die "不在 Herdr 裡(HERDR_ENV≠1)"
|
||||
CLONE="$(clone_root_for "$REPO")"
|
||||
BRANCH="issue-$N"
|
||||
WT="$(worktree_path "$N" "$CLONE")"
|
||||
|
||||
if [ "$(agent_status "$N")" != missing ]; then
|
||||
die "issue-$N 的 agent 還活著(tab $(tab_for_issue "$N")),不重複啟動"
|
||||
fi
|
||||
[ "$(issue_state "$REPO" "$N")" = open ] || die "#$N 不是 open"
|
||||
BLOCKERS="$(open_blockers "$REPO" "$N")" || exit 1
|
||||
[ -z "$BLOCKERS" ] || die "#$N 被還沒關閉的議題擋住:$(echo "$BLOCKERS" | tr '\n' ' ')(Gitea 依賴設定)。等它們關閉,或到 Gitea 拿掉依賴再啟動"
|
||||
|
||||
git -C "$CLONE" fetch --quiet origin
|
||||
RESUMED=false
|
||||
if git -C "$CLONE" show-ref --verify --quiet "refs/heads/$BRANCH"; then
|
||||
RESUMED=true
|
||||
if ! git -C "$CLONE" worktree list --porcelain | grep -qx "worktree $WT"; then
|
||||
git -C "$CLONE" worktree add --quiet "$WT" "$BRANCH"
|
||||
fi
|
||||
else
|
||||
git -C "$CLONE" worktree add --quiet -b "$BRANCH" "$WT" origin/main
|
||||
fi
|
||||
|
||||
TAB=""
|
||||
# 回滾到跟沒跑過一樣:關 tab、移除 worktree;分支只在本次新建時刪除。
|
||||
rollback() {
|
||||
[ -n "$TAB" ] && herdr tab close "$TAB" >/dev/null 2>&1 || true
|
||||
if git -C "$CLONE" worktree list --porcelain | grep -qx "worktree $WT"; then
|
||||
git -C "$CLONE" worktree remove --force "$WT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
$RESUMED || git -C "$CLONE" branch -D "$BRANCH" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
TAB_JSON="$(herdr tab create --workspace "${HERDR_WORKSPACE_ID:?}" --cwd "$WT" --label "#$N" --no-focus)"
|
||||
TAB="$(jq_py 'print(find(d,"tab")["tab_id"])' <<<"$TAB_JSON")"
|
||||
PANE="$(jq_py 'print(find(d,"root_pane")["pane_id"])' <<<"$TAB_JSON")"
|
||||
|
||||
START_ERR="$(mktemp)"
|
||||
for _ in 1 2 3 4 5 6; do
|
||||
if herdr agent start "issue-$N" --kind claude --pane "$PANE" --timeout 90000 -- --model "$MODEL" >/dev/null 2>"$START_ERR"; then
|
||||
rm -f "$START_ERR"; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ -f "$START_ERR" ]; then
|
||||
cat "$START_ERR" >&2; rm -f "$START_ERR"
|
||||
rollback
|
||||
die "agent 啟動失敗,已回滾"
|
||||
fi
|
||||
|
||||
# git 身分:與 tea 預設使用者一致(只設在這個 worktree,不動 clone 的設定)
|
||||
GIT_USER="$(command tea login list --output json | jq_py "print(next((l['user'] for l in d if l['name']=='$HERDR_ISSUE_LOGIN'),''))")"
|
||||
GIT_EMAIL="$(tea api "/user" | jq_py 'print(d.get("email") or "")' 2>/dev/null || true)"
|
||||
git -C "$CLONE" config extensions.worktreeConfig true
|
||||
# agent 在 worktree 裡自己跑的 git(push、fetch)也要有帳密:把 helper 設到 clone 的本地設定(worktree 共用),只對這個主機生效
|
||||
GIT_HOST_URL="$(command tea login list --output json | jq_py "print(next((l['url'] for l in d if l['name']=='$HERDR_ISSUE_LOGIN'),''))")"
|
||||
if [ -n "$GIT_HOST_URL" ]; then
|
||||
git -C "$CLONE" config --local --replace-all "credential.$GIT_HOST_URL.helper" "$SCRIPTS/git-credential.sh"
|
||||
fi
|
||||
git -C "$WT" config --worktree http.sslVerify false
|
||||
git -C "$WT" config --worktree user.name "$GIT_USER"
|
||||
[ -z "$GIT_EMAIL" ] || git -C "$WT" config --worktree user.email "$GIT_EMAIL"
|
||||
|
||||
if $RESUMED; then RESUME_LINE="分支已有先前進度,先看 git log 和議題核取方塊再接續。"; else RESUME_LINE=""; fi
|
||||
PROMPT="$(sed -e "s|{LINK}|$LINK|g" -e "s|{REPO}|$REPO|g" -e "s|{N}|$N|g" -e "s|{S}|$SCRIPTS|g" -e "s|{LOGIN}|$HERDR_ISSUE_LOGIN|g" -e "s|{RESUME}|$RESUME_LINE|" "$SCRIPTS/prompt.txt")"
|
||||
|
||||
# 送出後必須觀察到 agent 進入 working 或 blocked;否則 Herdr 回非 0(agent_prompt_stalled、timeout、agent_blocked 等)。
|
||||
PROMPT_ERR="$(mktemp)"
|
||||
if PROMPT_OUT="$(herdr agent prompt "issue-$N" "$PROMPT" --wait --until working --until blocked --timeout 15000 2>"$PROMPT_ERR")"; then
|
||||
rm -f "$PROMPT_ERR"
|
||||
else
|
||||
echo "herdr-issue: 送出提示詞後 agent 沒有反應,已回滾" >&2
|
||||
cat "$PROMPT_ERR" >&2; rm -f "$PROMPT_ERR"
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
STATE="$(jq_py 'print(find(d,"agent_status") or "")' <<<"$PROMPT_OUT" 2>/dev/null || true)"
|
||||
case "$STATE" in working|blocked) ;; *) STATE="$(agent_status "$N")" ;; esac
|
||||
|
||||
ensure_label "$REPO"
|
||||
tea issues edit "$N" --repo "$REPO" --remove-labels "$LABEL" >/dev/null 2>&1 || true
|
||||
tea issues edit "$N" --repo "$REPO" --add-labels "$LABEL" >/dev/null
|
||||
|
||||
printf '{"tab":"%s","pane":"%s","path":"%s","resumed":%s,"state":"%s"}\n' "$TAB" "$PANE" "$WT" "$RESUMED" "$STATE"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# 用法:tick.sh <owner/repo> <N> <第幾項>
|
||||
# 把議題內文第 k 個核取方塊(已勾的也算在內)改成「[x]」。每次都重新讀取最新內文再改。
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
REPO="${1:?}"; N="${2:?}"; K="${3:?用法:tick.sh <owner/repo> <N> <第幾項>}"
|
||||
NEW="$(issue_json "$REPO" "$N" | python3 -c "
|
||||
import sys,json,re
|
||||
body=json.load(sys.stdin)['body']
|
||||
k=int(sys.argv[1]); i=[0]
|
||||
def rep(m):
|
||||
i[0]+=1
|
||||
return '[x]' if i[0]==k else m.group(0)
|
||||
out=re.sub(r'\[[ xX]\]', rep, body)
|
||||
if i[0]<k: sys.exit('只有 %d 個核取方塊,沒有第 %d 個' % (i[0],k))
|
||||
sys.stdout.write(out)
|
||||
" "$K")"
|
||||
tea issues edit "$N" --repo "$REPO" --description "$NEW" >/dev/null
|
||||
echo "#$N 第 $K 項已勾選,剩 $(grep -o '\[ \]' <<<"$NEW" | wc -l) 項"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# 用法:watch-issue.sh <owner/repo> <N>(在 clone 裡以背景執行)
|
||||
# 每 HERDR_ISSUE_WATCH_INTERVAL 秒(預設 5)查議題;agent blocked 超過 HERDR_ISSUE_BLOCKED_AFTER 秒(預設 60,只通知一次)或消失就彈 Herdr 通知;議題關閉時清理並往上檢查規格、總覽。
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
REPO="${1:?}"; N="${2:?用法:watch-issue.sh <owner/repo> <N>}"
|
||||
CLONE="$(clone_root_for "$REPO")"
|
||||
BRANCH="issue-$N"
|
||||
WT="$(worktree_path "$N" "$CLONE")"
|
||||
LAST=""
|
||||
WATCH_INTERVAL="${HERDR_ISSUE_WATCH_INTERVAL:-5}" # 輪詢秒數(不耗額度,只查 Gitea 與 Herdr)
|
||||
BLOCKED_AFTER="${HERDR_ISSUE_BLOCKED_AFTER:-60}" # blocked 連續超過幾秒才通知
|
||||
BLOCKED_SINCE=0
|
||||
NOTIFIED=false
|
||||
|
||||
# 若 <label> 底下沒有 open 議題,留言並關閉議題 X;印出 closed|open|skip
|
||||
close_if_done() { # $1=X $2=label
|
||||
[ "$(issue_state "$REPO" "$1")" = open ] || { echo skip; return; }
|
||||
if [ "$(open_count_with_label "$REPO" "$2")" = 0 ]; then
|
||||
tea comment "$1" --repo "$REPO" "底下的議題全部關閉,由 herdr-issue 自動關閉。" >/dev/null
|
||||
tea issues close "$1" --repo "$REPO" >/dev/null
|
||||
echo closed
|
||||
else
|
||||
echo open
|
||||
fi
|
||||
}
|
||||
|
||||
while [ "$(issue_state "$REPO" "$N")" != closed ]; do # 查詢失敗會回空字串,要繼續等,不能當成已關閉
|
||||
S="$(agent_status "$N")"
|
||||
NOW="$(date +%s)"
|
||||
if [ "$S" = blocked ]; then
|
||||
[ "$LAST" = blocked ] || { BLOCKED_SINCE=$NOW; NOTIFIED=false; }
|
||||
if ! $NOTIFIED && [ $((NOW - BLOCKED_SINCE)) -ge "$BLOCKED_AFTER" ]; then
|
||||
notify "#$N 等你回答" "issue-$N 已 blocked 超過 $BLOCKED_AFTER 秒"
|
||||
NOTIFIED=true
|
||||
fi
|
||||
elif [ "$S" = missing ] && [ "$LAST" != missing ] && [ -n "$LAST" ]; then
|
||||
notify "#$N 的 agent 不見了" "issue-$N 的 agent 已結束,議題仍 open"
|
||||
fi
|
||||
LAST="$S"
|
||||
sleep "$WATCH_INTERVAL"
|
||||
done
|
||||
|
||||
echo "#$N 已關閉,開始清理"
|
||||
LABELS="$(issue_labels "$REPO" "$N")"
|
||||
TAB="$(tab_for_issue "$N")"
|
||||
[ -n "$TAB" ] && herdr tab close "$TAB" >/dev/null && echo "關閉 tab $TAB"
|
||||
tea issues edit "$N" --repo "$REPO" --remove-labels "$LABEL" >/dev/null 2>&1 || true
|
||||
|
||||
git -C "$CLONE" fetch --quiet origin || true
|
||||
read -r PR STATE _ <<<"$(pr_for_issue "$REPO" "$N")" || true
|
||||
DELETE_BRANCH=false
|
||||
case "${STATE:-none}" in
|
||||
merged) DELETE_BRANCH=true; echo "PR #$PR 已合併" ;;
|
||||
closed|open) echo "PR #$PR 狀態 $STATE,保留分支" ;;
|
||||
*) if [ "$(git -C "$CLONE" rev-list --count "origin/main..$BRANCH" 2>/dev/null || echo 0)" = 0 ]; then DELETE_BRANCH=true; echo "沒有 PR 也沒有 commit"; else echo "沒有 PR 但分支有 commit,保留"; fi ;;
|
||||
esac
|
||||
if git -C "$CLONE" worktree list --porcelain | grep -qx "worktree $WT"; then
|
||||
git -C "$CLONE" worktree remove --force "$WT" && echo "移除 worktree $WT"
|
||||
fi
|
||||
$DELETE_BRANCH && git -C "$CLONE" branch -D "$BRANCH" >/dev/null 2>&1 && echo "刪除分支 $BRANCH"
|
||||
|
||||
SPEC="$(grep -o '規格/#[0-9]*' <<<"$LABELS" | head -1 | tr -d '規格/#')"
|
||||
if [ -n "$SPEC" ]; then
|
||||
R="$(close_if_done "$SPEC" "規格/#$SPEC")"; echo "規格 #$SPEC:$R"
|
||||
if [ "$R" = closed ]; then
|
||||
OV="$(issue_labels "$REPO" "$SPEC" | grep -o '總覽/#[0-9]*' | head -1 | tr -d '總覽/#')"
|
||||
[ -n "$OV" ] && echo "總覽 #$OV:$(close_if_done "$OV" "總覽/#$OV")"
|
||||
fi
|
||||
fi
|
||||
echo "#$N 清理完成"
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
name: herdr
|
||||
description: "Control Herdr, a terminal multiplexer for coding agents. Use only when the user explicitly mentions Herdr or asks to use Herdr to inspect or control panes, tabs, workspaces, commands, or another agent. Do not use merely because a task could benefit from a background terminal, delegation, or parallel work. Requires HERDR_ENV=1."
|
||||
---
|
||||
|
||||
# Herdr
|
||||
|
||||
Herdr organizes terminals into workspaces, tabs, and panes, recognizes coding agents running inside panes, and exposes the current session through the `herdr` CLI.
|
||||
|
||||
Before issuing any control command, verify that this agent is running inside a Herdr-managed pane:
|
||||
|
||||
```bash
|
||||
test "${HERDR_ENV:-}" = 1
|
||||
```
|
||||
|
||||
If the check fails, say that you are not running inside Herdr and stop. Do not inspect or control the focused Herdr session from outside Herdr.
|
||||
|
||||
When the check passes, the `herdr` binary in `PATH` talks to the current session. Use it to inspect neighboring work, create terminal layout, start agents and commands, read output, and wait for state changes.
|
||||
|
||||
## Learn the current CLI
|
||||
|
||||
The installed binary is the authority for command syntax. Start with:
|
||||
|
||||
```bash
|
||||
herdr --help
|
||||
```
|
||||
|
||||
Then print the relevant command group by running the group without a subcommand:
|
||||
|
||||
```bash
|
||||
herdr agent
|
||||
herdr pane
|
||||
herdr workspace
|
||||
herdr tab
|
||||
herdr worktree
|
||||
herdr terminal
|
||||
herdr notification
|
||||
herdr integration
|
||||
herdr session
|
||||
herdr machine
|
||||
```
|
||||
|
||||
Do not run bare `herdr` for discovery; it launches or attaches the TUI. Do not probe a mutating nested command by omitting arguments. Commands such as `herdr workspace create` are valid with defaults and will execute.
|
||||
|
||||
Most control commands return JSON. Read identifiers and state from those responses instead of predicting them.
|
||||
|
||||
## Understand layout, panes, and agents
|
||||
|
||||
Choose the primitive that matches the job:
|
||||
|
||||
- Workspace, tab, and pane topology organize terminal locations.
|
||||
- Pane commands control raw terminals, shells, tests, servers, input, and output.
|
||||
- Agent commands control the recognized coding agent currently occupying a pane.
|
||||
|
||||
A pane exists whether or not it contains an agent. `agent start` requires an existing available shell pane and never creates, splits, or moves layout. Use pane commands for ordinary processes. Use agent commands when Herdr must validate agent identity or interpret `idle`, `working`, `blocked`, `done`, and `unknown` lifecycle states.
|
||||
|
||||
Agent commands accept either a unique live agent name or the pane ID currently hosting that agent. They do not accept terminal IDs or bare agent-kind labels. Names must match `[a-z][a-z0-9_-]{0,31}` and be unique among live agents. A name follows the current pane occupant and is cleared when that agent exits, is released, or is replaced.
|
||||
|
||||
`idle` and `done` both mean the agent is ready for input. The CLI/API uses the server's seen state to distinguish them; explicit focus commands mark the target seen, while reads do not. Each TUI client tracks viewed completions independently, so its Done badge can differ from the CLI or another client's badge. `blocked` means Herdr recognized an approval or question UI. `unknown` means an agent is present but Herdr cannot classify it confidently; it does not prove completion.
|
||||
|
||||
## Use IDs and caller context
|
||||
|
||||
Public IDs are opaque stable handles:
|
||||
|
||||
- workspace: `w1`
|
||||
- tab: `w1:t1`
|
||||
- pane: `w1:p1`
|
||||
|
||||
Closed tab and pane IDs are not reused. A pane moved into another workspace receives a new workspace-qualified pane ID. After `pane move`, continue with `.result.move_result.pane.pane_id` or the live agent name. The old value is reported as `.result.move_result.previous_pane_id`; only the moved process's inherited caller context keeps resolving that old ID, so do not use it as a general agent target.
|
||||
|
||||
Herdr injects the caller's context into each managed pane:
|
||||
|
||||
```bash
|
||||
printf '%s\n' "$HERDR_WORKSPACE_ID" "$HERDR_TAB_ID" "$HERDR_PANE_ID"
|
||||
```
|
||||
|
||||
Prefer `--current` when a pane command should target the calling pane. An omitted `pane split` target uses the calling pane when `HERDR_PANE_ID` is available, otherwise the focused pane. Other commands may use the UI-focused pane, which can belong to the user or another client.
|
||||
|
||||
Discover live state with:
|
||||
|
||||
```bash
|
||||
herdr workspace list
|
||||
herdr tab list --workspace "$HERDR_WORKSPACE_ID"
|
||||
herdr pane current --current
|
||||
herdr pane list --workspace "$HERDR_WORKSPACE_ID"
|
||||
herdr agent list
|
||||
```
|
||||
|
||||
Creation responses expose the IDs to use next. `workspace create` returns `.result.workspace`, `.result.tab`, and `.result.root_pane`. `tab create` returns `.result.tab` and `.result.root_pane`. `pane split` returns the new pane as `.result.pane`.
|
||||
|
||||
IDs and live agent names are scoped to one server. Two saved SSH machines can both have `w1:p1` or an agent named `reviewer`. Selecting a machine in the TUI does not retarget commands running in your pane: without `--machine`, they still use the inherited session and socket context.
|
||||
|
||||
To control a saved SSH machine, use the same global prefix for discovery and every later command:
|
||||
|
||||
```bash
|
||||
herdr --machine <label-or-id> agent list
|
||||
herdr --machine <label-or-id> pane list
|
||||
herdr --machine <label-or-id> agent prompt <remote-agent-name> "Reply with your current status." --wait --timeout 120000
|
||||
```
|
||||
|
||||
The selector must be an enabled saved profile ID or a unique, case-sensitive label, not an arbitrary SSH hostname. Commands use that profile's remote session without an open TUI. Do not combine `--machine` with `--session` or `--remote`. Discover IDs on that machine; inherited local IDs and `--current` do not identify remote panes.
|
||||
|
||||
Both installations must support machine API forwarding, and the remote server must already be running and API-compatible. Forwarding never installs, starts, or restarts a server and never falls back to Local. Local configuration, session management, installation commands, and interactive attachment are not forwarded. Remote worktree paths must be absolute, `~`, or start with `~/`; plugin link paths must be absolute. A connection failure does not prove a mutation was not applied: inspect remote state before retrying.
|
||||
|
||||
`herdr machine list` lists saved connection profiles, not a cross-machine pane inventory; add `--json` for scripts. Only add, remove, enable, or disable profiles when the user asks. Removing a profile disconnects the client but does not stop remote sessions. Interactive `machine add` discovers running remote sessions and may ask the user to pick one; non-interactive setup uses the remote default session unless `--remote-session` is explicitly supplied. Use `herdr machine status [<label-or-id>] --json` to diagnose a failing machine. `herdr machine reconnect` needs the user to complete SSH authentication in their terminal; do not run it for them. Setup asks before stopping an incompatible server and defaults to No; do not approve replacement without the user's consent. Experimental handoff is not part of `machine add`.
|
||||
|
||||
## Start and coordinate an agent
|
||||
|
||||
Default to a sibling pane in the current tab and the current working directory. Do not create a workspace, tab, worktree, or different cwd unless the user explicitly requests that topology or location.
|
||||
|
||||
Honor a direction requested by the user. Otherwise inspect the caller pane:
|
||||
|
||||
```bash
|
||||
herdr pane layout --pane "$HERDR_PANE_ID"
|
||||
```
|
||||
|
||||
Split a wide pane to the right and a narrow or tall pane down. Avoid repeated same-direction splits that create unusably narrow columns or short rows. Keep the user's focus in the calling pane and explicitly preserve the caller's working directory:
|
||||
|
||||
```bash
|
||||
herdr pane split --current --direction right --cwd "$PWD" --no-focus
|
||||
```
|
||||
|
||||
Replace `right` with `down` when appropriate. Read the new pane ID from `.result.pane.pane_id`.
|
||||
|
||||
An available shell pane must be at its interactive prompt, with the shell itself in the foreground and no foreground command, editor, or agent running. Start a supported agent in that pane with a useful unique name:
|
||||
|
||||
```bash
|
||||
herdr agent start reviewer --kind codex --pane <returned-pane-id>
|
||||
```
|
||||
|
||||
Use the kind requested by the user. Run `herdr agent` to inspect the installed kind list and options. Pass native agent arguments only after `--`:
|
||||
|
||||
```bash
|
||||
herdr agent start reviewer --kind codex --pane <returned-pane-id> -- <agent-args...>
|
||||
```
|
||||
|
||||
A successful `agent start` returns only after Herdr detects the expected agent in the same pane and considers it ready for interactive input. If the agent is blocked during startup, the command returns `agent_not_ready` immediately but keeps the name available for `agent read` and `agent send-keys`. Wait until the agent becomes idle before prompting it. Startup defaults to a 30-second timeout.
|
||||
|
||||
Submit work through the agent surface:
|
||||
|
||||
```bash
|
||||
herdr agent prompt reviewer "Review the current diff and report only actionable findings." --wait --timeout 120000
|
||||
```
|
||||
|
||||
`agent prompt` honors the pane's live bracketed-paste mode and sends text followed by encoded Enter as one ordered submission. It reports successful submission only after both have been written; that alone does not prove the agent started a turn. For Codex on Windows, Herdr sends a paste boundary before Enter so submission does not depend on prompt size. It rejects an agent already waiting at an approval or question dialog with `agent_blocked` before sending any input. Inspect the blocked UI and ask the user before answering it. For normal agent work, `--wait` is enough: it waits for the first settled `idle`, `done`, or `blocked` state. Do not repeat those defaults with `--until`.
|
||||
|
||||
With `--wait`, a prompt sent from a non-working state must produce observed `working` or `blocked` activity. After submission, Herdr waits up to five seconds for that activity; unrelated `idle`, `done`, or session changes do not satisfy this gate. It returns `agent_prompt_stalled` if no activity is observed, or `timeout` if the caller's timeout expires first. The caller timeout includes submission time. Without a timeout, the settled-state wait is indefinite after activity is observed. This wait tracks lifecycle state, not an individual turn; if the agent is already working, completion of the active turn may satisfy it.
|
||||
|
||||
Use `--until` only for a state-specific workflow, such as waiting for an already-running agent to request input:
|
||||
|
||||
```bash
|
||||
herdr agent wait reviewer --until blocked --timeout 120000
|
||||
```
|
||||
|
||||
Without `--until`, standalone `agent wait` uses the same settled-state defaults as `agent prompt --wait`.
|
||||
|
||||
Use logical keys for interactive agent UI controls:
|
||||
|
||||
```bash
|
||||
herdr agent send-keys reviewer esc
|
||||
herdr agent send-keys reviewer ctrl+c
|
||||
```
|
||||
|
||||
Herdr validates all keys before writing any bytes. Read the result through the resolved agent:
|
||||
|
||||
```bash
|
||||
herdr agent get reviewer
|
||||
herdr agent read reviewer --source recent-unwrapped --lines 120
|
||||
```
|
||||
|
||||
If a wait fails or returns `blocked`, inspect `agent get` and `agent read` before deciding what input to send. A timeout or stalled response does not prove the prompt was never delivered; do not blindly submit it again. Use the pane surface only when raw terminal control is intentional.
|
||||
|
||||
## Run an ordinary command in another pane
|
||||
|
||||
Create a sibling pane with the same geometry rule, preserve the caller's working directory, and keep user focus unchanged:
|
||||
|
||||
```bash
|
||||
herdr pane split --current --direction right --cwd "$PWD" --no-focus
|
||||
```
|
||||
|
||||
Read the new pane ID from `.result.pane.pane_id`, then run and inspect the command:
|
||||
|
||||
```bash
|
||||
herdr pane run <returned-pane-id> "just test"
|
||||
herdr pane wait-output <returned-pane-id> --match "test result" --timeout 120000
|
||||
herdr pane read <returned-pane-id> --source recent-unwrapped --lines 120
|
||||
```
|
||||
|
||||
`pane run` atomically sends command text and Enter. `pane wait-output` searches the selected snapshot immediately, so output that already exists can match. Use `--match <text>` for a literal substring or `--regex <pattern>` for a Rust regular expression. Omitting `--timeout` allows an indefinite wait.
|
||||
|
||||
Use the read source that matches the task:
|
||||
|
||||
- `visible`: the currently rendered viewport.
|
||||
- `recent`: recent rendered output, including soft wraps.
|
||||
- `recent-unwrapped`: recent output with soft wraps joined; prefer it for logs and transcripts.
|
||||
- `detection`: the plain-text bottom-buffer snapshot used for agent detection.
|
||||
|
||||
Use `--format ansi` when colors and terminal styling are evidence. Otherwise use text.
|
||||
|
||||
`--lines` asks Herdr for more rows from the pane's available screen and host scrollback. Alternate-screen rows do not enter ordinary host scrollback. For supported idle agents, Herdr can collect application-owned history and restore the viewport afterward, but not every application or response can be recovered this way.
|
||||
|
||||
If a larger recent read still does not reveal the completed response, ask the agent to write it as Markdown in a temporary directory and reply only with the file path, then read that file on the same machine. Use this only as a fallback; do not request file output in the initial prompt.
|
||||
|
||||
## Safety and coordination rules
|
||||
|
||||
- Use `--no-focus` for background work unless the user asked to switch context.
|
||||
- Use `--current`, an explicit pane ID, or a unique agent name. Do not rely on another client's focused pane.
|
||||
- Parse IDs from JSON responses. Do not derive them from sidebar order or examples.
|
||||
- Do not close workspaces, tabs, panes, or sessions you did not create unless the user explicitly asked. `workspace close --group` closes the primary workspace and its linked worktree workspaces; never add it merely to bypass `workspace_group_close_required`.
|
||||
- Use `--trust-repository` only after the user has verified the repository. It grants per-request Git trust; it is not a routine retry for a failed worktree command.
|
||||
- Client and server versions can differ after an update. Check `herdr status` before relying on new server features. A missing method is not permission to stop or upgrade a server.
|
||||
- Never run `herdr server stop` from an active session unless the user explicitly intends to stop the server and its pane processes.
|
||||
- Never kill the main Herdr process. Use named test sessions for experiments that need an isolated server.
|
||||
- CLI server errors are JSON on stderr with exit status 1. CLI syntax errors exit with status 2.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: ingest
|
||||
description: 解析一章輕小說(txt 或 md),抽出世界觀與人物,逐一詢問合併、新建或略過,並寫入 companion 角色資料。使用者提供小說章節檔或要求建立/更新角色設定時使用。
|
||||
---
|
||||
|
||||
# 小說章節解析
|
||||
|
||||
輸入:單章 txt 或 md 與章節編號。不保存整章原文,只存結構化摘要與簡短對白範例(每句 ≤80 字)。
|
||||
|
||||
## 流程
|
||||
|
||||
1. **讀取與分段**:超過約 6000 字的章節依段落/場景切成多段,逐段抽取後合併為一份結果,出現順序(`order`)沿用全章連續編號。
|
||||
2. **抽取**:時間、地點、環境、用語(寫入共用世界觀庫);人物、對白、獨白(寫入角色)。人物標 `main`(有戲份的主要角色)或 `minor`(路人)。
|
||||
3. **整理成 `ParseResult`**(型別見 `hooks/companion/core/types.ts`):
|
||||
- 記憶分四層:`episodic` 情節、`semantic` 語意、`emotional` 情感、`procedural` 程序;每筆含 `importance`(1–10)與 `order`。
|
||||
- 基本資訊欄位:birthday、zodiac、bloodType、hair、face、height、weight、measurements。每欄附 `source`(`novel` > `official` > `wiki`)與 `confidence`(0–1);原文沒寫的先搜尋,查不到填 `value: "unknown"`,不可猜測。
|
||||
4. **逐一詢問人物**:呼叫 `planMerge` 取得清單(主要角色在前,路人最後批次問)。每位問「合併 / 新建 / 略過」,並顯示疑似同一人的理由(名字、別名、特徵)。
|
||||
5. **套用**:呼叫 `applyParseResult(fs, root, result, decisions, resolutions)`。
|
||||
- 同一章重跑不會重複寫入(以章節編號冪等);合併只追加不覆蓋。
|
||||
- 回傳的 `conflicts` 是基本資訊衝突,**逐欄位**問使用者保留或取代,答案放進 `resolutions["人物.欄位"]`(`keep` / `replace`)後再套用一次。更新既有值前一律先問。
|
||||
6. **口癖與個性**:用 `summarizeTics` 由對白與獨白歸納語尾、口頭禪、稱呼;個性先用屬性標籤取先驗(`initPersonality`),再把模型推論交給 `applyInference`(自動限制在先驗 ±20,每個維度必須附章節與對白證據)。使用者手動調整的維度用 `overrideDim` 鎖定,之後不再自動更新。
|
||||
|
||||
## 注意
|
||||
|
||||
- 模型從對白推得的個性可信度偏低,只當先驗。
|
||||
- 不要在 repo 內保存受版權保護的長篇設定或台詞;資料寫在使用者家目錄的 `~/.companion` 資料夾。
|
||||
- 範例完整流程見 `docs/ingest-walkthrough.md`。
|
||||
Reference in New Issue
Block a user