From c3c810c97bfa66c11f04a6afe329d15a16dceb0a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:27 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(gitea):=20=E6=96=B0=E5=A2=9E=20pr-get?= =?UTF-8?q?=20=E8=88=87=20pr-edit=20=E5=85=A9=E5=80=8B=20PR=20=E5=AD=90?= =?UTF-8?q?=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:`tools/gitea.sh` 新增 `pr-get` 與 `pr-edit`。`pr-get` 印出一支 PR 的標題、base 分支與描述,前三行固定 `title`、`base`、`body` 三個標記,第 4 行起是描述原文;`pr-edit` 從檔案讀描述,一次更新標題與描述。 Why:分支已經有 PR 時,每次認可之後都要比對標題、描述與前置 PR 依賴三項,有差才更新。原本只有 `pr-create` 與 `pr-status`,讀不到既有 PR 的標題與描述,比不了就只能重開一支 PR 或整支蓋掉,兩種都會把審查中的討論打斷。 How:`pr-get` 把唯一會多行的描述放在輸出最後一段,呼叫端用 `head -n1`、`sed -n 2p`、`tail -n +4` 就取得到三個欄位,全程不必解析 JSON。`pr-edit` 的描述走檔案而不走參數,多行內容與全形標點才裝得下;描述檔不存在回 `2`,API 呼叫失敗回 `4`,成功印 `OK {owner}/{repo}#{編號}`。 Who:`jsc-git:pr` 的既有 PR 校準步驟,以及任何要先比對 PR 現況再決定改不改的呼叫端。 --- tools/gitea.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tools/gitea.sh b/tools/gitea.sh index aaf3fce..b5b952a 100755 --- a/tools/gitea.sh +++ b/tools/gitea.sh @@ -15,6 +15,8 @@ # gitea.sh wiki-url / # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用 # gitea.sh pr-create / <body-file> # 建立 PR,印出 PR URL # gitea.sh pr-status <owner>/<repo> <pr-index> # 印出「{state} {merged} {mergeable}」 +# gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供比對用 +# gitea.sh pr-edit <owner>/<repo> <pr-index> <title> <body-file> # 更新 PR 的標題與描述 # gitea.sh pr-comments <owner>/<repo> <pr-index> # 印出所有留言(issue 留言、審查評語、行內留言),依時間排序 # gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index> # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 @@ -219,6 +221,42 @@ import json,sys p=json.load(sys.stdin) print("%s %s %s" % (p.get("state","?"), str(p.get("merged")).lower(), str(p.get("mergeable")).lower())) ' ;; + pr-get) + # 印出 PR 的標題、base 分支與描述,供呼叫端比對本機資料是否已經跟 PR 不一致。 + # 格式固定三段,描述放最後一段,因為只有它會多行: + # 第 1 行 title<TAB>{標題} + # 第 2 行 base<TAB>{base 分支} + # 第 3 行 body (單獨一個字,當描述的起始標記) + # 第 4 行起 描述原文,一直到檔尾 + # 呼叫端取標題用 head -n1 | cut -f2-,取 base 用 sed -n 2p | cut -f2-, + # 取描述用 tail -n +4,全程不必解析 JSON。 + or="${1:?owner/repo required}"; idx="${2:?pr index required}" + if ! out=$(req GET "/repos/$or/pulls/$idx"); then + printf '%s\n' "$out" >&2; exit 4 + fi + printf '%s' "$out" | python3 -c ' +import json,sys +p=json.load(sys.stdin) +print("title\t%s" % (p.get("title") or "")) +print("base\t%s" % ((p.get("base") or {}).get("ref") or "")) +print("body") +sys.stdout.write(p.get("body") or "") +' ;; + pr-edit) + # 更新 PR 的標題與描述。描述從檔案讀,才裝得下多行內容與全形標點。 + or="${1:?owner/repo required}"; idx="${2:?pr index required}" + title="${3:?title required}"; body_file="${4:?body file required}" + [ -f "$body_file" ] || { echo "找不到描述檔: $body_file" >&2; exit 2; } + tmp=$(mktemp) + python3 -c ' +import json,sys +print(json.dumps({"title":sys.argv[1],"body":open(sys.argv[2],encoding="utf-8").read()})) +' "$title" "$body_file" > "$tmp" + if ! out=$(req PATCH "/repos/$or/pulls/$idx" "$tmp"); then + rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4 + fi + rm -f "$tmp" + echo "OK $or#$idx" ;; pr-comments) # 印出 PR 的所有留言,每行「{時間}<TAB>{作者}<TAB>{類型}<TAB>{內容單行化}」。 # 三個來源都要讀:issue 留言、review 本體的評語、review 內逐行的程式碼留言。 From 5471b9763560389156a1c943c4946bc907572f4b Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Thu, 27 Aug 2026 11:20:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(pr-watch):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=9B=AF=20PR=20=E5=88=B0=E5=90=88=E4=BD=B5=E7=9A=84=E8=BC=AA?= =?UTF-8?q?=E8=A9=A2=E8=85=B3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 `tools/pr-watch.sh`,盯一支 PR 直到它合併或關閉。退出碼 `0` 是已合併或已關閉、`10` 是有新留言要呼叫端接手、`2` 是參數錯誤或第一輪就連不上 Gitea、`3` 是查不到該 PR。輪詢間隔預設 60 秒,`JSC_PR_WATCH_INTERVAL` 可覆寫。 Why:等 PR 合併原本靠各技能自己寫輪詢迴圈,間隔與退場條件每支都不一樣,還常常自動逾時退場,PR 明明還開著就被當成收尾。改用一支共用腳本之後,等待行為只有一種,而且盯到 PR 收尾為止。 How:資料一律經同目錄的 `gitea.sh`(`pr-status`、`pr-comments`),不自行拼 API。已回報過的留言把最後一筆時間戳寫進狀態檔,預設 `$JSC_HOME/pr-watch/{owner}-{repo}-{編號}.seen`,同一則不重複回報;狀態檔還不存在時把既有留言整批回報一次,寧可重複也不漏掉開始盯之前的審查意見。PR 收尾那一輪若還有沒回報的留言,照樣印出來再退出,退出碼仍是 `0`。`pr-status` 對不存在的 PR 會印「? none none」並正常結束,所以 state 只認 `open` 與 `closed`、merged 只認 `true` 與 `false`,對不上就回 `3`,PR 編號打錯不會變成永遠輪詢一支不存在的 PR。 Who:`jsc-sdlc:implement` 開完工作包 PR 之後的等待合併,以及任何要盯 PR 的技能。 --- tools/pr-watch.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100755 tools/pr-watch.sh diff --git a/tools/pr-watch.sh b/tools/pr-watch.sh new file mode 100755 index 0000000..0bfdc52 --- /dev/null +++ b/tools/pr-watch.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env sh +# pr-watch.sh — 盯著一支 PR,直到它合併或關閉。 +# 用法: pr-watch.sh <owner>/<repo> <pr-index> [state-file] +# 規則: 資料一律經同目錄的 gitea.sh(pr-status、pr-comments),不自行拼 API。 +# 輪詢間隔預設 60 秒,JSC_PR_WATCH_INTERVAL 可覆寫,單位是秒的正整數。 +# 不自動逾時退場,盯到 PR 合併或關閉為止。 +# 已回報過的留言不重複回報:最後處理過的留言時間戳寫進狀態檔, +# 預設 $JSC_HOME/pr-watch/{owner}-{repo}-{index}.seen,JSC_HOME 未設定時用 ~/.jsc。 +# 狀態檔還不存在時,把既有留言整批當成新留言回報一次。寧可重複回報, +# 也不要漏掉開始盯之前就留下的審查意見。 +# 每輪先看 PR 狀態,再看留言。PR 收尾那一輪若還有沒回報的留言, +# 照樣印出來再退出,退出碼仍是 0:留言不會因為 PR 剛好合併就消失。 +# 輸出: 全部繁中,留言行維持 gitea.sh pr-comments 的原格式 +# 「{時間}<TAB>{作者}<TAB>{類型}<TAB>{內容}」,呼叫端可直接用 cut -f 取欄位。 +# 退出碼: 0 = PR 已合併或關閉,印出最終狀態;10 = 有新留言待呼叫端跑決策樹; +# 2 = 參數錯誤或第一輪就連不上 Gitea;3 = 查不到該 PR。 +# 護欄: gitea.sh pr-status 對不存在的 PR 會印「? none none」並且 exit 0。 +# 所以 state 只認 open 與 closed、merged 只認 true 與 false,對不上就回 3。 +# 少了這道白名單,PR 編號打錯會變成永遠輪詢一支不存在的 PR。 +# 第一輪查詢失敗算環境沒接好,直接回 2;之後才失敗算連線抖動,警告一句繼續輪詢。 +set -u + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +GITEA="$script_dir/gitea.sh" + +usage() { + echo "用法: pr-watch.sh <owner>/<repo> <pr-index> [state-file]" >&2 + exit 2 +} + +[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage +REPO="$1"; INDEX="$2"; STATE="${3:-}" + +case "$REPO" in + */*) ;; + *) echo "錯誤: 第一個參數要寫成 {owner}/{repo},收到「$REPO」。" >&2; exit 2 ;; +esac +case "$INDEX" in + ''|*[!0-9]*) echo "錯誤: PR 編號要是數字,收到「$INDEX」。" >&2; exit 2 ;; +esac + +INTERVAL="${JSC_PR_WATCH_INTERVAL:-60}" +case "$INTERVAL" in + ''|*[!0-9]*) echo "錯誤: JSC_PR_WATCH_INTERVAL 要是正整數秒,收到「$INTERVAL」。" >&2; exit 2 ;; +esac +[ "$INTERVAL" -gt 0 ] || { echo "錯誤: JSC_PR_WATCH_INTERVAL 要大於 0。" >&2; exit 2; } + +if [ -z "$STATE" ]; then + jsc_home="${JSC_HOME:-$HOME/.jsc}" + owner=${REPO%%/*}; name=${REPO#*/} + STATE="$jsc_home/pr-watch/$owner-$name-$INDEX.seen" +fi + +TMP_NEW=$(mktemp) || { echo "錯誤: 無法建立暫存檔。" >&2; exit 2; } +trap 'rm -f "$TMP_NEW"' EXIT +trap 'rm -f "$TMP_NEW"; exit 130' INT TERM + +SEEN="" +[ -f "$STATE" ] && SEEN=$(head -n1 "$STATE" 2>/dev/null) + +save_seen() { # 時間戳 -> 寫回狀態檔 + ts="$1" + [ -n "$ts" ] || return 0 + dir=$(dirname -- "$STATE") + mkdir -p "$dir" 2>/dev/null || { echo "警告: 建不出狀態檔目錄 $dir,這批留言下次會重複回報。" >&2; return 0; } + printf '%s\n' "$ts" > "$STATE" 2>/dev/null \ + || echo "警告: 寫不進狀態檔 $STATE,這批留言下次會重複回報。" >&2 + SEEN="$ts" +} + +collect_new() { # 新留言寫進 $TMP_NEW;有東西才回 0 + # 時間戳一律由同一台 Gitea 產生,格式與時區都一致,所以字串比大小就夠用; + # pr-comments 自己也是照這個字串排序的。 + "$GITEA" pr-comments "$REPO" "$INDEX" 2>/dev/null \ + | awk -F '\t' -v seen="$SEEN" 'NF >= 4 && ($1 "") > (seen "")' > "$TMP_NEW" + [ -s "$TMP_NEW" ] +} + +flush_new() { # 印出新留言並記住最後一筆的時間戳 + cat "$TMP_NEW" + save_seen "$(awk -F '\t' 'END { print $1 }' "$TMP_NEW")" +} + +first_round=1 +while :; do + if ! status=$("$GITEA" pr-status "$REPO" "$INDEX" 2>/dev/null); then + if [ "$first_round" -eq 1 ]; then + echo "錯誤: 連不上 Gitea 或 gitea.sh 執行失敗。請確認 GITEA_HOST 與 GITEA_TOKEN,再重新盯。" >&2 + exit 2 + fi + echo "警告: 這一輪查不到 PR 狀態,$INTERVAL 秒後再試。" >&2 + sleep "$INTERVAL" + continue + fi + first_round=0 + + state=$(printf '%s\n' "$status" | awk '{ print $1 }') + merged=$(printf '%s\n' "$status" | awk '{ print $2 }') + case "$state" in + open|closed) ;; + *) echo "錯誤: 查不到 PR $REPO#$INDEX。請確認存取庫與 PR 編號。" >&2; exit 3 ;; + esac + case "$merged" in + true|false) ;; + *) echo "錯誤: 查不到 PR $REPO#$INDEX。請確認存取庫與 PR 編號。" >&2; exit 3 ;; + esac + + if [ "$state" = closed ]; then + if [ "$merged" = true ]; then + printf '最終狀態: PR %s#%s 已合併。\n' "$REPO" "$INDEX" + else + printf '最終狀態: PR %s#%s 已關閉,沒有合併。\n' "$REPO" "$INDEX" + fi + if collect_new; then + echo "收尾前還有沒回報的留言:" + flush_new + fi + exit 0 + fi + + if collect_new; then + printf '新留言 %s 則(%s#%s),請接手跑決策樹:\n' "$(wc -l < "$TMP_NEW" | tr -d ' ')" "$REPO" "$INDEX" + flush_new + exit 10 + fi + + sleep "$INTERVAL" +done From 73cca6e6eaff01f80614ea601c3e77f4ed6109a0 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Thu, 27 Aug 2026 11:20:27 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(gitea):=20README=20=E8=88=87=20AGENTS?= =?UTF-8?q?=20=E8=A3=9C=E4=B8=8A=E7=9B=AF=20PR=20=E8=88=87=20PR=20?= =?UTF-8?q?=E6=A0=A1=E6=BA=96=E7=9A=84=E7=94=A8=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:README 的指令清單補上 `pr-get`、`pr-edit` 與 `pr-watch.sh` 三項與各自的退出碼,環境變數表補 `JSC_HOME` 與 `JSC_PR_WATCH_INTERVAL`;AGENTS.md 新增一條規則,盯 PR 等合併一律用 `tools/pr-watch.sh`,不要自己寫輪詢迴圈。 Why:新指令沒寫進 README,呼叫端就只能翻腳本原始碼。`pr-watch.sh` 的退出碼 `10` 更要講清楚:它不是錯誤,是「有新留言,換你處理」的交棒訊號,看成錯誤就會把留言吞掉。 How:README 依既有版面把三項排進指令區塊,另補一段說明退出碼 `10` 的設計意圖。AGENTS.md 把新規則插進第 5 條,原第 5 條順延為第 6 條。 Who:讀 `jsc-gitea` 說明的人,以及依 AGENTS.md 行事的代理。 --- AGENTS.md | 3 ++- README.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 7538fce..829108d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,8 @@ 2. 技能位於 `skills/{name}/SKILL.md`;處理任務前先比對需求與各技能的 `description`,相符就載入並依其步驟執行。 3. 技能準則的唯一來源:`plugins/meta` 存取庫的 `references/guidelines.md`。 4. 所有 hook 只放在 `jsc-hooks`;gitea 操作一律經由 `jsc-gitea` 的 `tools/gitea.sh`;問使用者一律依 `jsc-ask:ask` 的決策樹規則。 -5. 主 agent 不需要處理細節的流程,一律建立 sub agent 處理。 +5. 盯 PR 等合併用 `tools/pr-watch.sh`,不要自己寫輪詢迴圈。它回 10 就代表有新留言,把留言接回決策樹處理完,再重新盯一次。 +6. 主 agent 不需要處理細節的流程,一律建立 sub agent 處理。 ## 呼叫慣例 diff --git a/README.md b/README.md index 8e68ee6..9ad8065 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ gitea.sh wiki-put <owner>/<repo> <page> <file> # 自動判斷新建或更新 gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用 gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> gitea.sh pr-status <owner>/<repo> <pr-index> # 印出 {state} {merged} {mergeable} +gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供呼叫端比對有沒有差 + # 前三行固定 title、base、body 三個標記,第 4 行起是描述原文 +gitea.sh pr-edit <owner>/<repo> <pr-index> <title> <body-file> + # 更新 PR 的標題與描述;描述從檔案讀,裝得下多行 gitea.sh pr-comments <owner>/<repo> <pr-index> # 印出所有留言(issue 留言、審查評語、行內留言),依時間排序 gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index> # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 @@ -45,8 +49,14 @@ hash-id <text> # 與 gitea.sh hash-id 相同 repo-sync.sh <owner>/<repo> [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因} # 基準分支的優先序只在這支腳本裡;dirty 會把解析好的分支帶出來當 PR 的 base check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則 +pr-watch.sh <owner>/<repo> <pr-index> [state-file] + # 盯著一支 PR,直到它合併或關閉,不自動逾時退場 + # 退出碼 0 = 已合併或關閉、10 = 有新留言要接手、2 = 參數錯誤、3 = 查不到該 PR + # 已回報過的留言記在狀態檔,預設 $JSC_HOME/pr-watch/{owner}-{repo}-{index}.seen ``` +`pr-watch.sh` 的退出碼 10 是設計重點:腳本只負責偵測,決策交回呼叫端。收到 10 就把印出來的留言丟進決策樹,處理完再重新盯一次。 + 議題與 HTML 產出: ``` @@ -123,6 +133,8 @@ Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZ | `GITEA_TOKEN` | Gitea API token;缺少或遇 401/403 時自動退回 tea CLI 登入 token | 詢問使用者 | | `JSC_WIKI_REPO_{TYPE}` | 各類型 wiki 頁的 `{owner}/{repo}`;TYPE = QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR | 退回 `JSC_WIKI_REPO`,不做跨類型代用 | | `JSC_WIKI_REPO` | 共用預設的 wiki `{owner}/{repo}` | 詢問使用者 | +| `JSC_HOME` | `pr-watch.sh` 狀態檔的根目錄 | 預設 `~/.jsc` | +| `JSC_PR_WATCH_INTERVAL` | `pr-watch.sh` 的輪詢間隔秒數(正整數) | 預設 60 | ## Hash 規則 From 5892feea531600c29369fcacd0dd849f5fa4f35c Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Thu, 27 Aug 2026 11:20:27 +0800 Subject: [PATCH 4/4] =?UTF-8?q?chore(manifest):=20=E4=B8=89=E4=BB=BD=20man?= =?UTF-8?q?ifest=20=E7=89=88=E6=9C=AC=E5=8D=87=E5=88=B0=200.1.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:`plugin.json`、`.claude-plugin/plugin.json`、`.codex-plugin/plugin.json` 的 `version` 由 0.1.4 改為 0.1.5。 Why:本次新增 `pr-get`、`pr-edit` 與 `pr-watch.sh`,屬於行為變更,版本要跟著往上走,各 CLI 才知道要更新。 How:三份只改 `version` 一個欄位,其餘內容不動,三份保持同一版號。 Who:`jsc-gitea` 外掛的套件描述檔。 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2421e23..8b857f3 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.4", + "version": "0.1.5", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 0646968..7f78991 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.4", + "version": "0.1.5", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index db3fb62..06aefb8 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.4", + "version": "0.1.5", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills/" }