雲端工人會分診、會組里程碑,組了就照里程碑做(inkstone/ISEP#159)

leo 09-18:「它看到票時要能夠組成 milestone 提出版本,所以不只是看票,還要組票。」
雲端只會撿(ISEP#130),沒有任何機制分診或組里程碑;open 302 張裡 74 張沒 s/*、257 張沒掛里程碑。

- ticket triage:沒 s/* 的票 → s/backlog/s/triage/s/pending+票上一則說明(--why 必填),一個動作;
  --dup-of:說明+close/duplicate+關+在留下的那張留一則指回來(留下的那張若是 leo 的就不寫)
- ticket compose:把已分診的票組成 InkStoneCo 里程碑(開工規約 1–7 條):既有載體+前置相依、
  s/backlog → s/todo、名字不准是版本號、期限 1–60 天、不開新票;先全部讀過才寫,理由一次列齊
- 不碰:Human/human/*、指派給別人(含 Leo)——判準是標籤與 assignee 登入名
- pick --pool:還沒到期的 InkStoneCo 里程碑的前置排在散票之前;可做的都抓盡時回待分診的票,
  每列帶 next;queue-drain-guard 照 next 印(待分診是 triage,不是 claim)
- 全局票池只有一個讀法 pool_issues(),接口留給 ISEP#154(global_index 寫卡驗完後只換這一個函式)
- InkStoneCo 掛在 open 里程碑的票=載體(容器):不進票池、不算待分診

假設(寫在這裡等總管 review):
- 分診只收「沒有 s/*」的票;s/triage 仍是總管/leo 的判斷,工人只能把拿不準的丟進 s/triage
- 載體撈不到時,工人用 ticket where+ticket new 補一張 InkStoneCo User Story 當載體(開工規約第 2 條的「真缺口」)
- 期限上限 60 天(9999 與過去日期都擋)

實測:test-ticket-triage 80/80、test-ticket-pick 84/84、queue-drain-guard 28/28;
真 Gitea 代跑一趟:分診 4 張、建 InkStoneCo milestone 68(載體 InkStoneCo#136,前置 ISEP#47、arcrun-rag#199),
pick --pool 第一組即為它的前置;一次違規三格的 compose 真的零寫入擋下。
待總管定版。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 18:43:07 +08:00
parent 51c668f9c8
commit 2bbcda2ac3
8 changed files with 900 additions and 41 deletions
+300
View File
@@ -0,0 +1,300 @@
#!/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 <說明> <want> <python 片段> [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-handinkstone/ISEP-' and 's/backlog' in b and '還在用、要修' in b)(W[0][2]['body'])" "$TR"
p "標籤換成 s/backlogid 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/duplicateid 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 <說明> <argv json> [要在訊息裡看到的字]
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 <說明> <要在訊息裡看到的字> <argv...>
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#80s/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 開),不含 HumanLeo" \
"['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