#!/bin/bash # 雲端工人「分診+組里程碑」的測試(inkstone/ISEP#159) # # 票上的驗收: # 1. 沒狀態的票數下降,每張被動過的票上都有一則說明(分到哪、為什麼;重複的指向哪張) # 2. 提出新里程碑:載體票在 InkStoneCo、執行票掛成前置、名字不是版本號、有期限、deliverable=版本 # 3. 不碰 Human、不碰指派給 Leo 的票 # 4. 組了之後照里程碑往下做(pick --pool 先回它的前置票) # # 全程離線:api() 換成錄音機餵假池子;TICKET_HOST 指到連不上的位址,漏接一條會當場炸,不會假綠。 cd "$(dirname "$0")/.." || exit 1 T=scripts/ticket TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0 export TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x ISEP_MEMBER_BACKOFF=0 export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR" # 時鐘定在 2026-09-18 00:00 UTC export ISEP_COUNTDOWN_NOW=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,9,18,0,0,tzinfo=d.timezone.utc).timestamp()))') unset ISEP_MAINLINE_FILE ISEP_IDENTITY CLAUDE_PROJECT_DIR ISEP_DRAIN export ISEP_WORKER_LOGIN=claude-code # 抓票者自己是誰(真跑時問 /user;離線測試不打網路) MAIN="$TMP/mainline.json" cat > "$MAIN" <<'JSON' {"ref":"inkstone/InkStoneCo#65","owner":"inkstone","repo":"InkStoneCo","id":65, "title":"主線 M","due_on":"2026-09-25T23:59:59Z","members":[]} JSON cat > "$TMP/pool.py" <<'PY' import json, os, re, urllib.parse MS = {"main": {"id": 65, "title": "主線 M", "due_on": "2026-09-25T23:59:59Z", "state": "open"}, "mine": {"id": 70, "title": "組好的 M", "due_on": "2026-09-30T23:59:59Z", "state": "open"}, "sub": {"id": 9, "title": "子 repo 的舊里程碑", "due_on": "2026-10-30T23:59:59Z", "state": "open"}} def I(repo, n, labels, ms=None, assignees=(), created="2026-09-01T00:00:00Z", pr=False, state="open", title=None): d = {"number": n, "title": title or f"票 {repo}#{n}", "state": state, "labels": [{"name": l} for l in labels], "assignees": [{"login": a} for a in assignees], "milestone": MS[ms] if ms else None, "created_at": created, "repository": {"full_name": f"inkstone/{repo}"}} if pr: d["pull_request"] = {"merged": False} return d POOL = { "InkStoneCo": [I("InkStoneCo", 55, [], None, title="身為一天只看兩眼的人…"), # ★ 載體候選:沒里程碑、沒人 I("InkStoneCo", 132, ["Human", "s/pending"], "main", assignees=("Leo",)), # 主線載體(Leo 的) I("InkStoneCo", 80, ["s/backlog"], "mine"), # 組好的 M 的載體(有 s/* 也不是任務) I("InkStoneCo", 81, ["Human"], None), # 掛 Human 的載體候選 I("InkStoneCo", 82, [], "main"), I("InkStoneCo", 83, [], None, assignees=("Leo",)), # 指派 Leo 的載體候選 I("InkStoneCo", 84, ["s/backlog"], None)], # 已是「組好的 M」載體的前置——不能再當載體 # 已掛在主線里程碑的載體候選 "ISEP": [I("ISEP", 1, [], None, created="2026-08-01T00:00:00Z"), # ★ 待分診(最舊) I("ISEP", 2, [], None), # 待分診 I("ISEP", 3, ["Human"], None), # 沒 s/*、掛 Human:不碰 I("ISEP", 4, [], None, assignees=("Leo",)), # 沒 s/*、指派 Leo:不碰 I("ISEP", 5, ["s/backlog", "type/bug"], None), # ★ 已分診:可組進里程碑 I("ISEP", 6, ["s/triage"], None), # 還沒決定:不可組 I("ISEP", 7, ["s/backlog"], None), # 已是組好的 M 的前置 I("ISEP", 8, ["s/backlog"], "sub"), # 掛著子 repo 里程碑 I("ISEP", 9, [], None, pr=True), # PR I("ISEP", 10, [], None, assignees=("claude-code",))], # 只指派給自己:可分診 "Arcrun": [I("Arcrun", 20, ["s/todo"], None), # ★ 已分診:可組進里程碑 I("Arcrun", 21, ["s/todo"], None), # 組好的 M 的前置(pick 用) I("Arcrun", 22, ["s/backlog", "Human"], None), # Human 前置:不碰 I("Arcrun", 23, ["s/backlog"], None, assignees=("Leo",))], # 指派 Leo 的前置:不碰 # Human 前置:不碰 } MSN = {"InkStoneCo": ["main", "mine"], "ISEP": ["sub"]} DEPS = {"InkStoneCo#80": ["ISEP#7", "Arcrun#21", "InkStoneCo#84"]} def _by_ref(ref): repo, n = ref.split("#") return next(i for i in POOL[repo] if i["number"] == int(n)) BLOCKS = {} for _p, _cs in DEPS.items(): for _c in _cs: BLOCKS.setdefault(_c, []).append(_p) LABELS = ["s/triage", "s/backlog", "s/todo", "s/doing", "s/review", "s/pending", "close/duplicate", "Human", "type/bug", "p/high"] DROP = set(json.loads(os.environ.get("DROP", "[]"))) rec = [] def fake_api(path, payload=None, method=None): verb = method or ("POST" if payload is not None else "GET") rec.append((verb, path, payload)) if path.startswith("/orgs/inkstone/repos"): return [{"name": r} for r in POOL] if path == "/user": return {"login": "claude-code"} if path.endswith("/labels?limit=100"): return [{"name": n, "id": k} for k, n in enumerate(LABELS, 1)] if "/milestones" in path: repo = path.split("/")[3] if verb == "POST": return {"id": 71, "title": payload["title"]} return [MS[k] for k in MSN.get(repo, [])] if "/issues?" in path: repo = path.split("/")[3] q = urllib.parse.parse_qs(path.split("?", 1)[1]) rows = [i for i in POOL[repo] if f"{repo}#{i['number']}" not in DROP] if "milestones" in q: want = q["milestones"][0] return [i for i in rows if (i["milestone"] or {}).get("title") == want] rows = [i for i in rows if i["state"] == "open"] page = int(q.get("page", ["1"])[0]); per = int(q.get("limit", ["50"])[0]) return rows[(page - 1) * per: page * per] m = re.match(r"/repos/inkstone/([\w-]+)/issues/(\d+)/(dependencies|blocks)(\?.*)?$", path) if m: if verb == "POST": return {} table = DEPS if m.group(3) == "dependencies" else BLOCKS return [_by_ref(r) for r in table.get(f"{m.group(1)}#{m.group(2)}", [])] m = re.match(r"/repos/inkstone/([\w-]+)/issues/(\d+)$", path) if m and verb == "GET": return _by_ref(f"{m.group(1)}#{m.group(2)}") return {} PY export POOL_PY="$TMP/pool.py" probe() { python3 - "$1" "$2" <<'PY' import sys, os, json, io, contextlib, importlib.util as u from importlib.machinery import SourceFileLoader as L spec = u.spec_from_loader("t", L("t", "scripts/ticket")) m = u.module_from_spec(spec); spec.loader.exec_module(m) ns = {} exec(open(os.environ["POOL_PY"]).read(), ns) m.api = ns["fake_api"]; rec = ns["rec"] argv = json.loads(sys.argv[2]) if sys.argv[2] else None buf = io.StringIO(); rc = None if argv is not None: with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf): try: getattr(m, argv[0])(argv[1:]) except SystemExit as e: rc = e.code W = [(mm, p, pl) for mm, p, pl in rec if mm in ("POST", "PATCH", "PUT", "DELETE")] print(eval(sys.argv[1], {"m": m, "rec": rec, "W": W, "rc": rc, "out": buf.getvalue(), "I": ns["I"], "json": json})) PY } p() { # p <說明> [argv json] got=$(probe "$3" "${4:-}" 2>&1) if [ "$got" = "$2" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi printf 'want=%s got=%s %s\n' "$2" "$got" "$1" } j() { python3 -c 'import json,sys;print(json.dumps(sys.argv[1:],ensure_ascii=False))' "$@"; } echo "── ① 規則本體(純函式)──" p "沒 s/*、沒人、沒 Human → 待分診" "True" "m.triageable(I('ISEP',1,[]),'claude-code')[0]" p "★ 掛 Human → 不碰(驗收 3)" "False" "m.triageable(I('ISEP',3,['Human']),'claude-code')[0]" p "human/exec 同組 → 不碰" "False" "m.triageable(I('ISEP',3,['human/exec']),'claude-code')[0]" p "★ 指派給 Leo → 不碰(驗收 3)" "False" "m.triageable(I('ISEP',4,[],assignees=('Leo',)),'claude-code')[0]" p "只指派給自己 → 可分診" "True" "m.triageable(I('ISEP',10,[],assignees=('claude-code',)),'claude-code')[0]" p "不知道自己是誰、票有指派 → 不碰(不猜)" "False" "m.triageable(I('ISEP',10,[],assignees=('claude-code',)),None)[0]" p "已經有 s/*(連 s/triage 也算)→ 不是待分診" "False" "m.triageable(I('ISEP',6,['s/triage']),'claude-code')[0]" p "PR → 不是票" "False" "m.triageable(I('ISEP',9,[],pr=True),'claude-code')[0]" p "已關 → 不碰" "False" "m.triageable(I('ISEP',1,[],state='closed'),'claude-code')[0]" p "InkStoneCo 裡掛在 open 里程碑的票(載體)→ 不是待分診(它是容器)" "False" "m.triageable(I('InkStoneCo',82,[],'main'),'claude-code')[0]" p "別的 repo 掛著里程碑、沒 s/* → 照樣待分診(載體只認 InkStoneCo)" "True" "m.triageable(I('ISEP',8,[],'sub'),'claude-code')[0]" p "名字以版本號開頭(1.4.52 雲端零件包)→ 算版本號名字" "True" "m._version_like('1.4.52 雲端零件包')" p "v 開頭也算" "True" "m._version_like('v0.30 什麼')" p "名字裡提到版本但不是開頭(封測不再斷,含 1.4 修正)→ 不算(誤攔比漏擋嚴重)" "False" "m._version_like('封測不再斷,含 1.4 修正')" echo echo "── ② triage:一個動作=說明+欄位(驗收 1)──" TR=$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why "還在用、要修" --name isep-hand) p "★ 先留說明、再改標籤:寫入恰好是 [留言, 改標籤]" "[('POST', '/repos/inkstone/ISEP/issues/1/comments'), ('PUT', '/repos/inkstone/ISEP/issues/1/labels')]" \ "[(mm,p) for mm,p,_ in W]" "$TR" p "★ 說明第一行是【身份】,內文講得出分到哪、為什麼" "True" \ "(lambda b: b.split(chr(10))[0]=='【身份】isep-hand/inkstone/ISEP/-' and 's/backlog' in b and '還在用、要修' in b)(W[0][2]['body'])" "$TR" p "標籤換成 s/backlog(id 2)" "[2]" "W[1][2]['labels']" "$TR" p "離開碼 0" "None" "rc" "$TR" TD=$(j cmd_triage inkstone/ISEP#2 --dup-of inkstone/ISEP#5 --why "同一件事" --name isep-hand) p "★ 重複:留說明→標 close/duplicate→關→在留下的那張留一則指回來" \ "[('POST', '/repos/inkstone/ISEP/issues/2/comments'), ('PUT', '/repos/inkstone/ISEP/issues/2/labels'), ('PATCH', '/repos/inkstone/ISEP/issues/2'), ('POST', '/repos/inkstone/ISEP/issues/5/comments')]" \ "[(mm,p) for mm,p,_ in W]" "$TD" p "★ 重複那則說明指向留下的那張(驗收 1:重複的指向哪張)" "True" "'inkstone/ISEP#5' in W[0][2]['body']" "$TD" p "關的是 state=closed、標的是 close/duplicate(id 7)" "True" "W[2][2]=={'state':'closed'} and W[1][2]['labels']==[7]" "$TD" TDL=$(j cmd_triage inkstone/ISEP#2 --dup-of inkstone/InkStoneCo#132 --why "同一件事") p "★ 重複的對象是 Leo 的票 → 這張照關,但**不在 Leo 的票上寫任何字**(驗收 3)" "True" \ "rc is None and not any('/InkStoneCo/' in p for _,p,_ in W) and '沒在' in out" "$TDL" p "只指派給自己的 ISEP#10 → 分診得了" "None" "rc" "$(j cmd_triage inkstone/ISEP#10 --to s/triage --why 要總管看)" echo echo " (該擋的:擋下時一筆都沒寫)" nb() { # nb <說明> [要在訊息裡看到的字] p "$1 → 離開碼 2、零寫入${3:+、訊息有「$3」}" "True" "rc==2 and not W${3:+ and '$3' in out}" "$2" } nb "★ 掛 Human 的 ISEP#3(驗收 3)" "$(j cmd_triage inkstone/ISEP#3 --to s/backlog --why x)" "掛著 Human" nb "★ 指派給 Leo 的 ISEP#4(驗收 3)" "$(j cmd_triage inkstone/ISEP#4 --to s/backlog --why x)" "指派給 Leo" nb "已分診過的 ISEP#6" "$(j cmd_triage inkstone/ISEP#6 --to s/backlog --why x)" "已經分診過" nb "PR" "$(j cmd_triage inkstone/ISEP#9 --to s/backlog --why x)" p "triage --sign cloud-worker ⇒ 說明內文署 [cloud-worker]" "True" "'[cloud-worker]' in W[0][2]['body']" \ "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why x --sign cloud-worker)" nb "沒寫 --why(說明是驗收 1 的本體)" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog)" "--why" nb "--to s/todo(要經 compose 排進里程碑才給)" "$(j cmd_triage inkstone/ISEP#1 --to s/todo --why x)" "compose" nb "--to 與 --dup-of 都給" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --dup-of inkstone/ISEP#5 --why x)" nb "--dup-of 指到自己" "$(j cmd_triage inkstone/ISEP#1 --dup-of inkstone/ISEP#1 --why x)" p "參數錯(沒 --why)→ 一通 API 都沒打" "0" "len(rec)" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog)" p "身份名不在名單上 → 一通 API 都沒打" "True" "rc==2 and len(rec)==0" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why x --name 路人)" echo echo " (--list:工人拿什麼來判斷)" p "★ --list 列出待分診的票(InkStoneCo 還沒分診的也算),不列 Human/Leo/已分診/PR" \ "['inkstone/ISEP#1', 'inkstone/ISEP#10', 'inkstone/ISEP#2', 'inkstone/InkStoneCo#55']" \ "[r['ref'] for r in json.loads(out)]" "$(j cmd_triage --list --json)" p "--list --all 連已分診的 open 票也列(找重複要看全部),並標出哪些可分診" "True" \ "(lambda rows: len(rows)>10 and {r['ref'] for r in rows if r['triageable']}=={'inkstone/ISEP#1','inkstone/ISEP#2','inkstone/ISEP#10','inkstone/InkStoneCo#55'})(json.loads(out))" \ "$(j cmd_triage --list --all --json)" p "--list 文字版講得出「不碰」的有幾張,並給出下一步那一行" "True" \ "'不碰' in out and ' triage inkstone/ISEP#1 ' in out" "$(j cmd_triage --list)" p "--list 只讀不寫" "True" "not W" "$(j cmd_triage --list)" echo echo "── ③ compose:組里程碑(驗收 2)──" CP=$(j cmd_compose --title "雲端自己把票池消化成版本" --goal "leo 不必一張張叫它做" --due 2026-10-01 \ --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 inkstone/Arcrun#20 --name isep-hand) p "★ 里程碑建在 inkstone/InkStoneCo(開工規約第 7 條)" "True" \ "[p for mm,p,_ in W if mm=='POST' and p.endswith('/milestones')]==['/repos/inkstone/InkStoneCo/milestones']" "$CP" p "★ 有期限(23:59:59Z),不是 9999" "2026-10-01T23:59:59Z" \ "[pl['due_on'] for mm,p,pl in W if p.endswith('/milestones')][0]" "$CP" p "★ 描述寫明這一輪要讓什麼變成真的、deliverable=總管驗過的新版本、載體與前置" "True" \ "(lambda d: '這一輪要讓什麼變成真的:leo 不必' in d and 'deliverable=' in d and '新版本' in d and 'inkstone/InkStoneCo#55' in d and 'inkstone/ISEP#5' in d)([pl['description'] for mm,p,pl in W if p.endswith('/milestones')][0])" "$CP" p "★ 載體票掛上新里程碑(id 71)" "True" \ "('PATCH','/repos/inkstone/InkStoneCo/issues/55',{'milestone':71}) in W" "$CP" p "★ 兩張執行票都掛成載體票的前置(/dependencies)" "True" \ "[pl for mm,p,pl in W if mm=='POST' and p=='/repos/inkstone/InkStoneCo/issues/55/dependencies']==[{'owner':'inkstone','repo':'ISEP','index':5},{'owner':'inkstone','repo':'Arcrun','index':20}]" "$CP" p "★ s/backlog → s/todo(已排進里程碑,等人領票),type/bug 留著" "[3, 9]" \ "sorted([pl['labels'] for mm,p,pl in W if mm=='PUT' and p=='/repos/inkstone/ISEP/issues/5/labels'][0])" "$CP" p "本來就 s/todo 的 Arcrun#20 不重貼標籤" "False" "any(p.endswith('/Arcrun/issues/20/labels') for _,p,_ in W)" "$CP" p "每張前置與載體都留了一則說明(第一行【身份】)" "True" \ "sorted(p for mm,p,pl in W if mm=='POST' and p.endswith('/comments') and pl['body'].startswith('【身份】'))==['/repos/inkstone/Arcrun/issues/20/comments','/repos/inkstone/ISEP/issues/5/comments','/repos/inkstone/InkStoneCo/issues/55/comments']" "$CP" p "★ 不開任何新票(開工規約第 2 條)" "False" \ "any(mm=='POST' and p.endswith('/issues') for mm,p,_ in W)" "$CP" p "離開碼 0,印出里程碑網址" "True" "rc is None and '/inkstone/InkStoneCo/milestone/71' in out" "$CP" CPS=$(j cmd_compose --title "雲端自己把票池消化成版本" --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 --sign cloud-worker) p "--sign cloud-worker ⇒ 每則留言內文都署 [cloud-worker];--sign 的值不會被當成前置票" "True" \ "rc is None and all('[cloud-worker]' in pl['body'] for mm,p,pl in W if p.endswith('/comments')) and not any('cloud-worker' in str(pl) for mm,p,pl in W if p.endswith('/dependencies'))" "$CPS" p "沒給 --sign ⇒ 不署一個寫死的名字(誰跑的就署誰)" "False" \ "any('[cloud-worker]' in pl['body'] for mm,p,pl in W if p.endswith('/comments'))" "$CP" echo echo " (該擋的:先全部驗過才寫——擋下時一筆都沒寫,而且理由一次列齊)" cb() { # cb <說明> <要在訊息裡看到的字> what=$1; want=$2; shift 2 p "$what → 離開碼 2、零寫入、訊息有「$want」" "True" "rc==2 and not W and '$want' in out" "$(j cmd_compose "$@")" } OK=(--goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#55) cb "★ 名字是版本號(開工規約第 5 條)" "版本號" --title "1.4.52 雲端零件包" "${OK[@]}" inkstone/ISEP#5 cb "沒名字" "--title" "${OK[@]}" inkstone/ISEP#5 cb "★ 沒期限(開工規約第 6 條)" "--due" --title 好名字 --goal g --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 cb "期限 9999" "天內" --title 好名字 --goal g --due 9999-01-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 cb "期限在過去" "天內" --title 好名字 --goal g --due 2026-09-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 cb "載體不在 InkStoneCo(不准在子 repo 建里程碑)" "InkStoneCo" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/ISEP#1 inkstone/ISEP#5 cb "沒有前置" "前置" --title 好名字 "${OK[@]}" cb "★ 載體掛 Human(驗收 3)" "載體 inkstone/InkStoneCo#81:掛著 Human" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#81 inkstone/ISEP#5 cb "★ 載體指派給 Leo(驗收 3)" "載體 inkstone/InkStoneCo#83:指派給 Leo" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#83 inkstone/ISEP#5 cb "載體本身是別的 open 里程碑載體的前置(那一輪還在跑)" "那一輪還在跑" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#84 inkstone/ISEP#5 cb "載體已在別的 open 里程碑" "已經掛在里程碑" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#82 inkstone/ISEP#5 cb "同名 open 里程碑已在" "同名" --title "組好的 M" "${OK[@]}" inkstone/ISEP#5 cb "★ 前置掛 Human(驗收 3)" "前置 inkstone/Arcrun#22:掛著 Human" --title 好名字 "${OK[@]}" inkstone/Arcrun#22 cb "★ 前置指派給 Leo(驗收 3)" "前置 inkstone/Arcrun#23:指派給 Leo" --title 好名字 "${OK[@]}" inkstone/Arcrun#23 cb "前置還沒分診(沒 s/*)" "還沒分診" --title 好名字 "${OK[@]}" inkstone/ISEP#1 cb "前置是 s/triage(要不要做還沒決定)" "還沒分診" --title 好名字 "${OK[@]}" inkstone/ISEP#6 cb "前置已經是別的里程碑載體的前置(一張票同時只排一輪)" "只排一輪" --title 好名字 "${OK[@]}" inkstone/ISEP#7 cb "前置掛著子 repo 的里程碑" "已經掛在里程碑" --title 好名字 "${OK[@]}" inkstone/ISEP#8 p "★ 好幾格不對時一次列齊(Human 前置+沒分診前置,兩個理由都在)" "True" \ "rc==2 and not W and 'Arcrun#22:掛著 Human' in out and 'ISEP#1 還沒分診' in out" \ "$(j cmd_compose --title 好名字 "${OK[@]}" inkstone/Arcrun#22 inkstone/ISEP#1)" p "參數層的錯(版本號名字)在打任何 API 之前就擋" "0" "len(rec)" \ "$(j cmd_compose --title 1.2.3 "${OK[@]}" inkstone/ISEP#5)" echo echo "── ④ 組了之後照里程碑往下做(驗收:再照里程碑往下做)──" POOLA=$(j cmd_pick --mainline "$MAIN" --pool --all --json) p "★ --pool:還沒到期的 InkStoneCo 里程碑「組好的 M」一組,成員是載體的前置(ISEP#7 的 s/backlog 不抓,Arcrun#21 s/todo 可抓)" \ "['inkstone/Arcrun#21']" "[r['ref'] for r in json.loads(out) if r['group']=='里程碑']" "$POOLA" p "★ 載體票本身(InkStoneCo#80,s/backlog)不當成可做的票——里程碑那組與票池那組都沒有它" "False" \ "any(r['ref']=='inkstone/InkStoneCo#80' for r in json.loads(out))" "$POOLA" p "里程碑那一組排在票池之前(先照里程碑做,再撿散票)" "True" \ "(lambda g: g.index('里程碑') < g.index('票池'))([r['group'] for r in json.loads(out)])" "$POOLA" p "★ 待分診那一組最舊的先(ISEP#1 08-01 開),不含 Human/Leo" \ "['inkstone/ISEP#1', 'inkstone/ISEP#10', 'inkstone/ISEP#2', 'inkstone/InkStoneCo#55']" \ "[r['ref'] for r in json.loads(out) if r['group']=='待分診']" "$POOLA" p "每一列都帶下一步(里程碑那一列是 claim、待分診那一列是 triage)" "True" \ "all((' triage ' in r['next']) == (r['group']=='待分診') for r in json.loads(out))" "$POOLA" p "沒帶 --pool(本機總管)→ 不看還沒到期的里程碑、也不回待分診(誤攔比漏擋嚴重)" "True" \ "rc==1 and '里程碑「組好的' not in out and '待分診' not in out" "$(j cmd_pick --mainline "$MAIN")" p "pick 只讀不寫" "True" "not W" "$POOLA" echo echo "── ⑤ 動詞接得上 ──" out=$(python3 "$T" usage triage 2>&1); got=$? if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--dup-of'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi printf 'ticket usage triage 印得出用法\n' out=$(python3 "$T" usage compose 2>&1); got=$? if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--carrier'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi printf 'ticket usage compose 印得出用法\n' out=$(python3 "$T" triage inkstone/ISEP#1 --to s/backlog --why x 2>&1); got=$? if [ "$got" != 0 ] && ! printf '%s' "$out" | grep -q '✅'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi printf 'triage 連不上 Gitea → 非零離開、不印成功(got=%s)\n' "$got" echo echo "通過 $PASS 條,失敗 $FAIL 條" [ "$FAIL" = 0 ] || exit 1