棒子不會掉:子票相依+tag+指派三格,全部用 Gitea 原生欄位
leo 2026-08-27:「子票相依是 gitea 原有機制,改 tag 和指定也是,這些全部都要」 08-26 掉的那一次(arcrun-rag#136 comment 4267「等雲端那半出貨才驗得了」躺了 14 小時)三格全缺:沒有子票、tag 沒動、沒有指派給任何人。根因不是誰忘了, 是那件事沒有落在任何一個「撈一次就看得到」的欄位上。 實測(Gitea 1.26.4):子票還開著時關母票 → HTTP 412 "cannot close this issue or pull request because it still has open dependencies" ⇒ 相依是平台保證的硬擋,不是提醒。跨 repo 也成立(201)。 - scripts/ticket 新增三個動詞:subtask(長子票+掛相依)/ handback(指派+改 tag+寫下一步,一個動作)/mine(撈一次看棒子在誰手上) - hooks/comment-carries-task-guard.sh:留言帶未完成的未來式卻沒開子票 → 擋一次 - hooks/baton-handback-guard.sh:一條線收工,三格缺哪一格當場說出來(提醒不擋) - docs/governance §16:三個維度/粒度(傾向多開票)/既有票只從今天起適用/ 與 s/* 的關係/實測輸出 - 測試 20+10 全綠,用 fixture 跑不打網路、不在票池留測試票 📌 踩到並記進閘的檔頭:hook 裡比對中文一律用 python3 的 re,不要用 grep 的字元類 (等[^。]{0,20}出貨 在真實 hook 呼叫路徑下對「等雲端那半出貨」不匹配,閘靜默失效) 票:inkstone/ISEP#30(comment 4334/4335/4346/4347) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# 交棒警察的測試——三格(指派/tag/下一步)各自缺漏時要被抓到
|
||||
# 判準:5 種該報、5 種不該報
|
||||
# 用 fixture 跑,不打網路、不在票池留下任何一張測試票。
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
H=hooks/baton-handback-guard.sh
|
||||
PASS=0; FAIL=0
|
||||
FX=$(mktemp -d); trap 'rm -rf "$FX"' EXIT
|
||||
|
||||
mkfx(){ # $1=state $2=assignees(csv) $3=labels(csv) $4=有沒有交棒留言(y/n)
|
||||
python3 - "$FX" "$1" "$2" "$3" "$4" <<'PY'
|
||||
import json, sys, os
|
||||
d, state, asg, labs, baton = sys.argv[1:6]
|
||||
issue = {"state": state,
|
||||
"assignees": [{"login": a} for a in asg.split(",") if a],
|
||||
"labels": [{"name": l} for l in labs.split(",") if l]}
|
||||
comments = [{"body": "一般進度留言"}]
|
||||
if baton == "y":
|
||||
comments.append({"body": "🏃 **棒子交回** → `claude-code`\n\n**下一步**:去驗那一格"})
|
||||
json.dump(issue, open(os.path.join(d, "issue.json"), "w"))
|
||||
json.dump(comments, open(os.path.join(d, "comments.json"), "w"))
|
||||
PY
|
||||
}
|
||||
|
||||
run(){ # $1=want $2=說明 (fixture 已備好)
|
||||
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\n做點事"}}' \
|
||||
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
||||
got=$?
|
||||
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||
printf 'want=%s got=%s %s\n' "$1" "$got" "$2"
|
||||
}
|
||||
|
||||
echo "── 該報:三格有缺 ──"
|
||||
mkfx open "" "s/doing" n ; run 2 "三格全缺(=08-26 那次掉棒的狀態)"
|
||||
mkfx open "" "s/review" y ; run 2 "缺指派:撈 assignee 撈不到它"
|
||||
mkfx open "claude-code" "" y ; run 2 "缺 tag:看不出它卡在哪一段"
|
||||
mkfx open "claude-code" "s/review" n ; run 2 "缺下一步:收下的人得重讀整串"
|
||||
mkfx open "claude-code" "s/doing" y ; run 2 "tag 還停在 s/doing,但這條線收工了"
|
||||
|
||||
echo "── 不該報 ──"
|
||||
mkfx open "claude-code" "s/review" y ; run 0 "三格都齊(交回總管等 review)"
|
||||
mkfx open "Leo" "s/stage,Human" y ; run 0 "三格都齊(交給 leo 驗收)"
|
||||
mkfx open "claude-code" "s/pending,p/high" y ; run 0 "卡在依賴上,但三格都齊"
|
||||
mkfx closed "" "" n ; run 0 "票已關=棒子到終點,沒有下一手"
|
||||
|
||||
echo "── 派工單裡沒有【工單】那行 ⇒ 不歸這支管 ──"
|
||||
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"隨便做點事"}}' \
|
||||
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
||||
got=$?
|
||||
if [ "$got" = 0 ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||
printf 'want=0 got=%s 沒有票號的派工單\n' "$got"
|
||||
|
||||
echo
|
||||
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||
[ "$FAIL" = 0 ]
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# 討論串裡的任務要長成子票——這道閘的測試
|
||||
# 判準:6 種該擋(留言裡真的藏著待辦)、10 種不該擋
|
||||
# 🔴 「不該擋」比「該擋」重要:branch-holds.md 檔頭那句——
|
||||
# 永遠在響的警報,等於訓練人忽略這個警報。
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
H=hooks/comment-carries-task-guard.sh
|
||||
PASS=0; FAIL=0
|
||||
SID=test-cctg-$$
|
||||
run(){ # $1=want $2=cmd
|
||||
rm -f "/tmp/.comment-task-guard-warned-$SID"
|
||||
printf '%s' "{\"session_id\":\"$SID\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2")}}" \
|
||||
| bash "$H" >/dev/null 2>&1
|
||||
got=$?
|
||||
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||
printf 'want=%s got=%s %.62s\n' "$1" "$got" "$2"
|
||||
}
|
||||
|
||||
echo "── 該擋:留言裡藏著一件還沒做完的事 ──"
|
||||
# 這一條就是 2026-08-26 真的掉了的那則的形狀
|
||||
run 2 'req("POST", f"{API}/repos/inkstone/arcrun-rag/issues/136/comments", {"body":"Phase 1 的驗收在雲端那半出貨之前驗不了"})'
|
||||
run 2 'curl -X POST "$API/repos/inkstone/Arcrun/issues/12/comments" -d "{\"body\":\"要等 leo 開閘才能部署\"}"'
|
||||
run 2 'req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"[blocker] 收端還沒上線"})'
|
||||
run 2 'req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"這條路徑還沒測,先擱著"})'
|
||||
run 2 'req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"TODO: 補一條端到端測試"})'
|
||||
run 2 'req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"等雲端出貨之後要回頭重驗一次"})'
|
||||
|
||||
echo "── 不該擋 ──"
|
||||
run 0 'curl -s "$API/repos/inkstone/ISEP/issues/30/comments"' # 純讀
|
||||
run 0 'req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"已驗過,8/8 全綠"})' # 沒有待辦形狀
|
||||
run 0 'scripts/ticket subtask inkstone/arcrun-rag#136 --title "身為x,我要y,我才z" -F /tmp/c.md'
|
||||
run 0 'scripts/ticket handback inkstone/arcrun-rag#136 --to claude-code --next "還沒驗的那格去驗"'
|
||||
run 0 'scripts/ticket mine'
|
||||
run 0 'scripts/ticket close inkstone/x#1 --deliverable https://example.invalid/pr/1'
|
||||
run 0 'grep -n "驗不了" hooks/comment-carries-task-guard.sh' # 只是讀這支閘
|
||||
run 0 'echo "這則留言等雲端出貨才驗得了 —— no-subtask: 它已經有子票 #143 了"' # 逃生口
|
||||
run 0 'curl -X POST "$API/repos/x/y/issues" -d "{\"title\":\"等出貨\"}"' # 開新票不歸這支管
|
||||
run 0 'req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"🏃 **棒子交回** → claude-code\n\n**下一步**:等 #143"})'
|
||||
|
||||
echo "── 同一 session 只擋一次(不鬼打牆)──"
|
||||
rm -f "/tmp/.comment-task-guard-warned-$SID"
|
||||
C='req("POST", f"{API}/repos/x/y/issues/3/comments", {"body":"驗不了"})'
|
||||
for want in 2 0; do
|
||||
printf '%s' "{\"session_id\":\"$SID\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$C")}}" \
|
||||
| bash "$H" >/dev/null 2>&1
|
||||
got=$?
|
||||
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||
printf 'want=%s got=%s 第 %s 次同樣的指令\n' "$want" "$got" "$([ "$want" = 2 ] && echo 1 || echo 2)"
|
||||
done
|
||||
|
||||
echo "── -F <檔>:內文在檔案裡也要看得到 ──"
|
||||
TMPF=$(mktemp); printf '這件事等雲端那半出貨才驗得了\n' > "$TMPF"
|
||||
run 2 "scripts/ticket say inkstone/arcrun-rag#136 -F $TMPF"
|
||||
printf '已驗完,全部綠燈\n' > "$TMPF"
|
||||
run 0 "scripts/ticket say inkstone/arcrun-rag#136 -F $TMPF"
|
||||
rm -f "$TMPF" "/tmp/.comment-task-guard-warned-$SID"
|
||||
|
||||
echo
|
||||
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||
[ "$FAIL" = 0 ]
|
||||
+233
-1
@@ -18,6 +18,12 @@ leo 2026-08-16 三句話,本工具就是它們的機械化:
|
||||
ticket close <票> --deliverable <URL> 關票(要有交付物連結)
|
||||
ticket decide <票> -F <答案檔> 記 leo 的裁決+改狀態(同一個動作)
|
||||
|
||||
三個 Gitea 原生欄位(leo 2026-08-27「這些全部都要」,缺一個就會掉棒):
|
||||
|
||||
ticket subtask <母票> --title <US> -F <檔> 討論串裡的一件事 → 看得見的子票+相依
|
||||
ticket handback <票> --to <誰> --next <一句> 收工=指派+改 tag+寫下一步,一個動作
|
||||
ticket mine [--user <誰>] 撈一次:棒子在誰手上、每根下一步是什麼
|
||||
|
||||
票的寫法:`owner/repo#N`,例:`inkstone/InkStoneCo#44`
|
||||
"""
|
||||
import json
|
||||
@@ -351,7 +357,233 @@ def cmd_decide(argv):
|
||||
print(" 兩件事是同一個動作——不會只改標籤而忘了記,也不會記了而看板還在說『等 leo』。")
|
||||
|
||||
|
||||
CMDS = {"where": cmd_where, "say": cmd_say, "new": cmd_new, "close": cmd_close, "decide": cmd_decide}
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# 三個 Gitea 原生欄位(leo 2026-08-27:「子票相依是 gitea 原有機制,改 tag 和指定
|
||||
# 也是,**這些全部都要**」)。三者正交,各回答一個問題:
|
||||
#
|
||||
# 子票相依 這件事存在嗎?做完了嗎? ← 缺了它:任務藏在討論串,撈 open 票看不到
|
||||
# tag s/* 它現在卡在哪一段? ← 缺了它:撈得到票但要重讀整串才知道在等什麼
|
||||
# 指派 現在誰該動? ← 缺了它:大家都以為是別人的事
|
||||
#
|
||||
# 08-26 那次掉棒(arcrun-rag#136 comment 4267「等雲端那半出貨才驗得了」躺了 14 小時)
|
||||
# 三格全缺:沒有子票、tag 沒動、沒指派給任何人。
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BATON_MARK = "🏃 **棒子交回**"
|
||||
|
||||
|
||||
def _labels_of(owner, repo):
|
||||
return {l["name"]: l["id"] for l in api(f"/repos/{owner}/{repo}/labels?limit=100")}
|
||||
|
||||
|
||||
def _set_labels(owner, repo, num, keep):
|
||||
ids = _labels_of(owner, repo)
|
||||
unknown = [n for n in keep if n not in ids]
|
||||
if unknown:
|
||||
die(f"🚫 這個 repo 沒有這些標籤:{unknown}\n"
|
||||
f" (標籤的唯一真相源是 ISEP 的 labels.yaml,改標籤要改那裡再 sync)")
|
||||
api(f"/repos/{owner}/{repo}/issues/{num}/labels",
|
||||
{"labels": [ids[n] for n in keep]}, method="PUT")
|
||||
|
||||
|
||||
def _deps(owner, repo, num):
|
||||
return api(f"/repos/{owner}/{repo}/issues/{num}/dependencies") or []
|
||||
|
||||
|
||||
# ── subtask ──────────────────────────────────────────────────────────────
|
||||
def cmd_subtask(argv):
|
||||
"""把「討論串裡的一件事」長成看得見的子票,並掛成母票的 Gitea 原生相依。
|
||||
|
||||
為什麼是子票而不是留言(leo 2026-08-27):
|
||||
「可以在討論串延伸子票,它完成了子票就完工,不然這筆任務就是沒完工⋯⋯
|
||||
票沒完工可以察覺嗎?」
|
||||
留言要有人回頭讀才看得見;票的 open/closed 是**狀態**,撈一次就在眼前。
|
||||
|
||||
而且相依是**硬的**,不是提醒——實測(2026-08-27,Gitea 1.26.4):
|
||||
子票還開著時 PATCH 母票 state=closed → **HTTP 412
|
||||
"cannot close this issue or pull request because it still has open dependencies"**
|
||||
⇒ 子票沒關,母票關不掉。這是平台保證,不是自律。
|
||||
|
||||
刻意不要求 `ticket where` 戳記:leaf 屬於一條**已知**的線(它的母票就是那條線),
|
||||
不是「新開一條線」——那道閘擋的是後者。
|
||||
"""
|
||||
if len(argv) < 1:
|
||||
die("用法:ticket subtask <母票 owner/repo#N> --title \"<User Story>\" -F <內文檔>\n"
|
||||
" [--repo <收件 repo,預設跟母票同一個>] [--assign <誰做>] [--label <s/xxx,預設 s/todo>]")
|
||||
powner, prepo, pnum = parse_ref(argv[0])
|
||||
|
||||
def opt(name, default=None):
|
||||
return argv[argv.index(name) + 1] if name in argv else default
|
||||
|
||||
title = opt("--title")
|
||||
fpath = opt("-F") or opt("--file")
|
||||
if not title or not fpath:
|
||||
die("🚫 缺 --title 或 -F <內文檔>")
|
||||
check_title(title)
|
||||
body = open(fpath).read()
|
||||
|
||||
missing = [s for s in REQUIRED_SECTIONS if s not in body]
|
||||
if missing:
|
||||
die(f"🚫 子票一樣是票,模板欄位不能少:{'、'.join(missing)}\n"
|
||||
" (子票通常很小,但「小」不等於「可以說不清楚」——\n"
|
||||
" leo 2026-08-27:『每個事情沒有歷史記錄才是大問題』,\n"
|
||||
" 而沒寫驗收條件的票,關掉之後就沒有歷史,只剩一個關字)")
|
||||
|
||||
repo = opt("--repo", prepo)
|
||||
label = opt("--label", "s/todo")
|
||||
assign = opt("--assign")
|
||||
|
||||
body = (f"Parent: {powner}/{prepo}#{pnum}\n"
|
||||
f"> 這張票是從母票的討論串裡長出來的一件事。\n"
|
||||
f"> **它沒關,母票關不掉**(Gitea 原生相依,實測 412 硬擋)。\n\n") + body
|
||||
|
||||
d = api(f"/repos/{ORG}/{repo}/issues", {"title": title, "body": body})
|
||||
cnum = d["number"]
|
||||
print(f"✅ 子票 {ORG}/{repo}#{cnum} 已開:{d['html_url']}")
|
||||
|
||||
_set_labels(ORG, repo, cnum, [label])
|
||||
print(f" tag:{label}(它卡在哪一段)")
|
||||
if assign:
|
||||
api(f"/repos/{ORG}/{repo}/issues/{cnum}", {"assignees": [assign]}, method="PATCH")
|
||||
print(f" 指派:{assign}(誰該動)")
|
||||
|
||||
# 🔴 相依掛不上去 = 這張子票對母票是隱形的 ⇒ 整個動作算失敗,要讓人看到
|
||||
api(f"/repos/{powner}/{prepo}/issues/{pnum}/dependencies",
|
||||
{"owner": ORG, "repo": repo, "index": cnum})
|
||||
deps = _deps(powner, prepo, pnum)
|
||||
openn = [i for i in deps if i["state"] == "open"]
|
||||
print(f" 相依:已掛上 {powner}/{prepo}#{pnum}(它存在、且擋著母票)")
|
||||
print(f"\n📌 母票 {powner}/{prepo}#{pnum} 目前有 {len(openn)} 張未關的相依:")
|
||||
for i in deps:
|
||||
mark = "◻" if i["state"] == "open" else "☑"
|
||||
print(f" {mark} {i['repository']['full_name']}#{i['number']} {i['title'][:50]}")
|
||||
print(" 在它們全關之前,母票 PATCH state=closed 會被 Gitea 擋下(412)。")
|
||||
|
||||
|
||||
# ── handback ─────────────────────────────────────────────────────────────
|
||||
def cmd_handback(argv):
|
||||
"""收工=把棒子交回去:**指派 + tag + 下一步**,一個動作三件事。
|
||||
|
||||
leo 2026-08-27:「它把事情做完後如果要檢查後續,就要把任務指定給你並改 tag,
|
||||
你收到指定用 tag 查就知道要去做,所以**每個任務結束必須要指派回總管**,
|
||||
要說明下一步怎麼做,直到最後交出正確 deliverable 並且驗證」
|
||||
|
||||
🔴 關鍵在「指派」是**欄位**不是文字——撈一次就看得到,不必讀 comment。
|
||||
08-26 那次掉棒,交回的話寫在 comment 裡,票沒指派、tag 沒動 ⇒ 棒子躺在地上 14 小時。
|
||||
"""
|
||||
if len(argv) < 1:
|
||||
die("用法:ticket handback <owner/repo#N> --to <claude-code|Leo> --next \"<下一步一句話>\"\n"
|
||||
" [--label <s/xxx>] [--evidence <URL 或一行實測>]\n"
|
||||
"(--to claude-code = 交回總管;--to Leo = 要 leo 親手做,會自動加 Human)")
|
||||
owner, repo, num = parse_ref(argv[0])
|
||||
|
||||
def opt(name, default=None):
|
||||
return argv[argv.index(name) + 1] if name in argv else default
|
||||
|
||||
to = opt("--to")
|
||||
nxt = opt("--next")
|
||||
if not to or not nxt:
|
||||
die("🚫 缺 --to 或 --next。\n"
|
||||
" **「下一步」不准省略**——收下棒子的人要能不重讀整串就知道該做什麼。\n"
|
||||
" (這正是 08-26 掉棒那次缺的:話有說,但沒有欄位在說「這張票在等你」)")
|
||||
label = opt("--label")
|
||||
ev = opt("--evidence")
|
||||
|
||||
issue = api(f"/repos/{owner}/{repo}/issues/{num}")
|
||||
cur = [l["name"] for l in issue.get("labels") or []]
|
||||
|
||||
deps = _deps(owner, repo, num)
|
||||
openn = [i for i in deps if i["state"] == "open"]
|
||||
|
||||
lines = [f"{BATON_MARK} → `{to}`", "",
|
||||
f"**下一步**:{nxt}"]
|
||||
if ev:
|
||||
lines += ["", f"**證據**:{ev}"]
|
||||
if openn:
|
||||
lines += ["", "**未關的相依**(它們全關之前這張票關不掉):"]
|
||||
lines += [f"- {i['repository']['full_name']}#{i['number']} {i['title'][:60]}" for i in openn]
|
||||
api(f"/repos/{owner}/{repo}/issues/{num}/comments", {"body": "\n".join(lines)})
|
||||
|
||||
api(f"/repos/{owner}/{repo}/issues/{num}", {"assignees": [to]}, method="PATCH")
|
||||
|
||||
keep = list(cur)
|
||||
if label:
|
||||
keep = [n for n in keep if not n.startswith("s/")] + [label]
|
||||
if to == "Leo" and "Human" not in keep:
|
||||
keep.append("Human")
|
||||
if to != "Leo" and "Human" in keep:
|
||||
keep = [n for n in keep if n != "Human"]
|
||||
_set_labels(owner, repo, num, keep)
|
||||
|
||||
print(f"✅ {owner}/{repo}#{num} 棒子交回 `{to}`")
|
||||
print(f" 指派:{to} tag:{[n for n in keep if n.startswith('s/')] or '(未動)'}"
|
||||
f"{' +Human' if 'Human' in keep else ''}")
|
||||
print(f" 下一步:{nxt}")
|
||||
if openn:
|
||||
print(f" ⚠️ 還有 {len(openn)} 張未關的相依 ⇒ 這張票現在關不掉(412),這是對的")
|
||||
print("\n📌 三件事是同一個動作——不會只留言而忘了指派,也不會指派了而看板還說 s/doing。")
|
||||
|
||||
|
||||
# ── mine ─────────────────────────────────────────────────────────────────
|
||||
def cmd_mine(argv):
|
||||
"""撈一次:**哪幾根棒子在誰手上、每根的下一步是什麼**。
|
||||
|
||||
不是輪詢——掛在本來就會做的動作上(session 開場、收工對帳)跑一次。
|
||||
"""
|
||||
user = argv[argv.index("--user") + 1] if "--user" in argv else None
|
||||
|
||||
if user in (None, "", "me"):
|
||||
who = "總管(本 token 的身份)"
|
||||
items = api("/repos/issues/search?state=open&type=issues&assigned=true&limit=100") or []
|
||||
else:
|
||||
who = user
|
||||
items = []
|
||||
for r in api(f"/orgs/{ORG}/repos?limit=100") or []:
|
||||
q = urllib.parse.urlencode({"state": "open", "assigned_by": user, "limit": 50})
|
||||
for it in api(f"/repos/{ORG}/{r['name']}/issues?{q}") or []:
|
||||
if it.get("pull_request"):
|
||||
continue
|
||||
it.setdefault("repository", {"full_name": f"{ORG}/{r['name']}"})
|
||||
items.append(it)
|
||||
|
||||
print(f"🏃 在「{who}」手上的棒子:{len(items)} 根\n")
|
||||
if not items:
|
||||
print(" (沒有——每一根都已經交出去或關掉了)")
|
||||
for it in items:
|
||||
ref = it["repository"]["full_name"] + "#" + str(it["number"])
|
||||
st = [l["name"] for l in it.get("labels") or []]
|
||||
o, rp, n = ref.split("/")[0], ref.split("/")[1].split("#")[0], it["number"]
|
||||
deps = _deps(o, rp, n)
|
||||
openn = [i for i in deps if i["state"] == "open"]
|
||||
print(f" ● {ref} {st}")
|
||||
print(f" {it['title'][:64]}")
|
||||
nxt = _last_next(o, rp, n)
|
||||
print(f" 下一步:{nxt or '❌ 沒有人寫下一步(交棒時漏了 --next)'}")
|
||||
if openn:
|
||||
print(f" ◻ 未關相依 {len(openn)}:" +
|
||||
"、".join(f"{i['repository']['full_name']}#{i['number']}" for i in openn))
|
||||
print("\n📌 終點是 leo 說的「直到最後交出正確 deliverable 並且驗證」——"
|
||||
"驗完就關票、清指派,這張表才會縮短。")
|
||||
|
||||
|
||||
def _last_next(owner, repo, num):
|
||||
"""從最後一則交棒留言抓「下一步」。抓不到就誠實回 None,不要編。"""
|
||||
try:
|
||||
cs = api(f"/repos/{owner}/{repo}/issues/{num}/comments?limit=100") or []
|
||||
except SystemExit:
|
||||
return None
|
||||
for c in reversed(cs):
|
||||
b = c.get("body") or ""
|
||||
if BATON_MARK.strip("*") .replace("**", "") in b or BATON_MARK in b:
|
||||
m = re.search(r"\*\*下一步\*\*[::]\s*(.+)", b)
|
||||
if m:
|
||||
return m.group(1).strip()[:100]
|
||||
return None
|
||||
|
||||
|
||||
CMDS = {"where": cmd_where, "say": cmd_say, "new": cmd_new, "close": cmd_close,
|
||||
"decide": cmd_decide, "subtask": cmd_subtask, "handback": cmd_handback,
|
||||
"mine": cmd_mine}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in CMDS:
|
||||
|
||||
Reference in New Issue
Block a user