雲端工人從 open 票池持續領票,可抓 0 張時講出池子去哪了(inkstone/ISEP#130 c7801)
根因:pick 只看主線成員與逾期里程碑;org 299 張 open 裡 254 張沒掛里程碑, 舊規則實跑可抓 0 張,09-18 六趟 run 都印「正常結果,不是故障」收工。 - 第三層:主線與逾期都抓盡、且是接票的環境(--pool 或 ISEP_DRAIN=1), 再看整個 open 票池裡 s/* 恰好是 s/todo 或 s/backlog 的票(不限里程碑)。 Human/human/*、指派給別人(含 Leo)、hub、PR、s/triage、沒有 s/* 一格不放寬。 - 本機沒設 ISEP_DRAIN 時不讀票池:總管跑過 handback 不會被佇列閘叫去抓票池(避免誤攔)。 - claim 同規則;可抓 0 張時列池子大小與每種不能抓的理由各幾張,不再說成正常。 - 分頁讀到空頁才停(伺服器每頁上限可能小於 50)。 假設(寫在這裡等總管 review):s/backlog 算「決定要做」可抓;沒有 s/* 的 72 張算未驗傷不抓、只計數。 實測:真 Gitea 改前可抓 0/改後 --pool 可抓 101;test-ticket-pick 80/80。 待總管定版。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+12
-3
@@ -429,12 +429,12 @@ bash hooks/tests/gitea-arm-check.test.sh .
|
|||||||
- ①③⑤⑥ 任一紅 ⇒ 收手收得太晚,暫存區已經開始長東西(磁碟風險回來了)
|
- ①③⑤⑥ 任一紅 ⇒ 收手收得太晚,暫存區已經開始長東西(磁碟風險回來了)
|
||||||
- ② 紅 ⇒ 訊息沒給出走得通的那一行(本票整張票在講的就是這件事)
|
- ② 紅 ⇒ 訊息沒給出走得通的那一行(本票整張票在講的就是這件事)
|
||||||
- ⑧⑨⑩ 任一紅 ⇒ **誤攔/複製錯**:正常用法被弄壞,或複本裡混進了 `hooks/` 以外的東西
|
- ⑧⑨⑩ 任一紅 ⇒ **誤攔/複製錯**:正常用法被弄壞,或複本裡混進了 `hooks/` 以外的東西
|
||||||
### A31 — 雲端工人抓票的規則是機械可判的:53 條
|
### A31 — 雲端工人抓票的規則是機械可判的:80 條
|
||||||
```
|
```
|
||||||
bash scripts/test-ticket-pick.sh
|
bash scripts/test-ticket-pick.sh
|
||||||
```
|
```
|
||||||
**該看到**:`通過 53 條,失敗 0 條`。**全程離線**:假池子(3 個 repo、3 個 open milestone、
|
**該看到**:`通過 80 條,失敗 0 條`。**全程離線**:假池子(5 個 repo、3 個 open milestone、
|
||||||
12 張票、1 條相依邊)餵給換掉的 `api()`,時鐘走 `ISEP_COUNTDOWN_NOW`,`TICKET_HOST` 指到連不上的位址
|
52 張票、1 條相依邊;`big` 那個 repo 的假伺服器每頁只給 30 張,驗分頁)餵給換掉的 `api()`,時鐘走 `ISEP_COUNTDOWN_NOW`,`TICKET_HOST` 指到連不上的位址
|
||||||
——任何一條真的走到網路會當場炸,不會靜靜變成假綠。**不開票、不改票、不留任何東西在 Gitea 上。**
|
——任何一條真的走到網路會當場炸,不會靜靜變成假綠。**不開票、不改票、不留任何東西在 Gitea 上。**
|
||||||
|
|
||||||
**它在守什麼**(`inkstone/ISEP#131`):Routine 08-06 起讀 `journeys.md` 本 sprint 段,
|
**它在守什麼**(`inkstone/ISEP#131`):Routine 08-06 起讀 `journeys.md` 本 sprint 段,
|
||||||
@@ -447,6 +447,9 @@ Gitea 主線里程碑抓票(`inkstone/InkStoneCo#118`),但那是一段給
|
|||||||
「在主線上」=hub 里程碑裡、或被主線成員當相依(跨 repo 邊,inkstone/ISEP#133);
|
「在主線上」=hub 里程碑裡、或被主線成員當相依(跨 repo 邊,inkstone/ISEP#133);
|
||||||
別 repo 裡同名的里程碑**不算**主線。主線抓盡才抓「逾期的 open 里程碑」(一個里程碑物件一組);
|
別 repo 裡同名的里程碑**不算**主線。主線抓盡才抓「逾期的 open 里程碑」(一個里程碑物件一組);
|
||||||
backlog/沒里程碑且沒有邊的一律不抓
|
backlog/沒里程碑且沒有邊的一律不抓
|
||||||
|
⇒ 帶 --pool 或環境 ISEP_DRAIN=1(接票的環境):兩層都抓盡後再看整個 open 票池,
|
||||||
|
s/* 恰好是 s/todo 或 s/backlog 就可抓(不限里程碑),其他格一格不放寬(inkstone/ISEP#130)
|
||||||
|
可抓 0 張時列出池子大小與每種「不能抓的理由」各幾張,不再印「正常、不是故障」
|
||||||
認領 claim = 指派 + s/doing + 第一行【身份】的留言,一個動作
|
認領 claim = 指派 + s/doing + 第一行【身份】的留言,一個動作
|
||||||
完成 handback --label s/review(本來就有的那半,不重做)
|
完成 handback --label s/review(本來就有的那半,不重做)
|
||||||
```
|
```
|
||||||
@@ -464,6 +467,12 @@ Gitea 主線里程碑抓票(`inkstone/InkStoneCo#118`),但那是一段給
|
|||||||
- ③「該擋」六條任一紅 ⇒ 規則只長在 `pick` 這端,手動 `claim` 一張 Human 的票照樣過
|
- ③「該擋」六條任一紅 ⇒ 規則只長在 `pick` 這端,手動 `claim` 一張 Human 的票照樣過
|
||||||
- ③「身份名不在名單上 → 一通都沒打」紅 ⇒ 名單以外的名字碰到了 Gitea
|
- ③「身份名不在名單上 → 一通都沒打」紅 ⇒ 名單以外的名字碰到了 Gitea
|
||||||
- ④ 第一條紅 ⇒ 連不上 Gitea 時回了離開碼 1,雲端會把「斷線」當「沒票」收工
|
- ④ 第一條紅 ⇒ 連不上 Gitea 時回了離開碼 1,雲端會把「斷線」當「沒票」收工
|
||||||
|
- ⑤「主線與逾期都抓盡、帶 --pool → 回 mira#25」紅 ⇒ 又回到 09-18 的形狀:幾百張 open 票,工人說沒事做
|
||||||
|
- ⑤「--all 票池恰好是這幾張」紅 ⇒ 要嘛放寬了不該碰的(Human/指派 Leo/triage/沒 s/*/hub),要嘛漏了該抓的
|
||||||
|
- ⑤「ISEP_DRAIN=1 = --pool」紅 ⇒ 雲端工人照 `cloud-worker.md` 打 `ticket pick`、佇列閘自己跑的 pick,都看不到票池
|
||||||
|
- ②「沒帶 --pool → 票池一通都沒讀」紅 ⇒ **誤攔**:本機總管跑過 `handback` 後每次收工都會被佇列閘點名去抓票池
|
||||||
|
- ⑤「可抓 0 張時列理由表、不說正常」紅 ⇒ 「0 張」又被說成正常收工(c7797)
|
||||||
|
- ⑤「分頁」紅 ⇒ 伺服器每頁上限比 50 小時,第二頁之後的票看不到
|
||||||
|
|
||||||
📌 **真實 Gitea 唯讀實跑**(2026-09-07,isep-hand,不改任何票):
|
📌 **真實 Gitea 唯讀實跑**(2026-09-07,isep-hand,不改任何票):
|
||||||
`ticket pick --mainline InkStoneCo/system-dev/mainline.json --all` 在主線
|
`ticket pick --mainline InkStoneCo/system-dev/mainline.json --all` 在主線
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
| `delivery-police.sh` | AI 宣稱「這件事做完了」,卻看不到任何實測證據(畫面截圖、指令輸出、HTTP 狀態碼…)就擋下。 | 🛑 擋 |
|
| `delivery-police.sh` | AI 宣稱「這件事做完了」,卻看不到任何實測證據(畫面截圖、指令輸出、HTTP 狀態碼…)就擋下。 | 🛑 擋 |
|
||||||
| `wiki-first-police.sh` | AI 做完事卻沒有把結論寫回 wiki 就想收工,就擋下——下次(或別的 AI)查 wiki 會查不到這次做過什麼。 | 🛑 擋 |
|
| `wiki-first-police.sh` | AI 做完事卻沒有把結論寫回 wiki 就想收工,就擋下——下次(或別的 AI)查 wiki 會查不到這次做過什麼。 | 🛑 擋 |
|
||||||
| `unpushed-police.sh` | AI 改好的東西還留在本機、沒有真的推送出去給別人用,卻想收工,就擋下——「改對了但沒送到」跟沒改是一樣的。 | 🛑 擋 |
|
| `unpushed-police.sh` | AI 改好的東西還留在本機、沒有真的推送出去給別人用,卻想收工,就擋下——「改對了但沒送到」跟沒改是一樣的。 | 🛑 擋 |
|
||||||
| `queue-drain-guard.sh` | **在接票的 session(跑過 `ticket pick/claim/handback`,或環境設了 `ISEP_DRAIN=1`),池子裡還有現在就能抓的票,就不准收工**——它自己跑一次 `ticket pick`,有票就把票號和認領指令交回去。這是 ISEP 唯一一支看「佇列」而不是看「這回合寫了什麼」的收工閘(`inkstone/ISEP#33`:雲端三趟 run 都是做完一件、還有票就停)。做不了的票要把理由寫回票、改欄位,pick 就不再回它。跟 leo 聊天的 session 不會被咬;同一張連續指名 3 次、讀不到佇列 3 次、或一個 session 擋滿 40 次就放行並留痕,不會鎖死。 | 🛑 擋(有上限) |
|
| `queue-drain-guard.sh` | **在接票的 session(跑過 `ticket pick/claim/handback`,或環境設了 `ISEP_DRAIN=1`),池子裡還有現在就能抓的票,就不准收工**——它自己跑一次 `ticket pick`,有票就把票號和認領指令交回去。「池子」多大看環境:設了 `ISEP_DRAIN=1`(雲端工人)時 pick 會看到整個 open 票池的 `s/todo`/`s/backlog`;本機沒設時只看主線與逾期里程碑,所以總管跑過 `handback` 也不會被叫去抓票池(`inkstone/ISEP#130`)。這是 ISEP 唯一一支看「佇列」而不是看「這回合寫了什麼」的收工閘(`inkstone/ISEP#33`:雲端三趟 run 都是做完一件、還有票就停)。做不了的票要把理由寫回票、改欄位,pick 就不再回它。跟 leo 聊天的 session 不會被咬;同一張連續指名 3 次、讀不到佇列 3 次、或一個 session 擋滿 40 次就放行並留痕,不會鎖死。 | 🛑 擋(有上限) |
|
||||||
| `baton-handback-guard.sh` | **背景派出去的工人交件時**(主對話收到完工通知),檢查那張工單票有沒有把棒子交回來(指派/標籤/下一步三格),缺就擋一次要 AI 補。工人還在跑的時候一聲都不出(`inkstone/ISEP#153`)。同一條線只點名一次;補完三格就不再出現。前景派工的那一半在下面 H 組。 | 🛑 擋(每條線至多一次) |
|
| `baton-handback-guard.sh` | **背景派出去的工人交件時**(主對話收到完工通知),檢查那張工單票有沒有把棒子交回來(指派/標籤/下一步三格),缺就擋一次要 AI 補。工人還在跑的時候一聲都不出(`inkstone/ISEP#153`)。同一條線只點名一次;補完三格就不再出現。前景派工的那一半在下面 H 組。 | 🛑 擋(每條線至多一次) |
|
||||||
|
|
||||||
> 🔴 **`claim-verify-police.sh` 已於 `inkstone/ISEP#60` 移除**(連同下面原本的 G 組 `subagent-claim-worksheet.sh`)。
|
> 🔴 **`claim-verify-police.sh` 已於 `inkstone/ISEP#60` 移除**(連同下面原本的 G 組 `subagent-claim-worksheet.sh`)。
|
||||||
|
|||||||
@@ -33,6 +33,9 @@
|
|||||||
# 不用 $CLAUDE_PROJECT_DIR:雲端沒有它,21 支閘的 log 因此靜默寫進 /.claude/hooks/(ISEP#130 c6873)。
|
# 不用 $CLAUDE_PROJECT_DIR:雲端沒有它,21 支閘的 log 因此靜默寫進 /.claude/hooks/(ISEP#130 c6873)。
|
||||||
# 狀態與 log 一律寫 ${ISEP_DRAIN_STATE_DIR:-${TMPDIR:-/tmp}},那裡在哪台都寫得進去。
|
# 狀態與 log 一律寫 ${ISEP_DRAIN_STATE_DIR:-${TMPDIR:-/tmp}},那裡在哪台都寫得進去。
|
||||||
#
|
#
|
||||||
|
# 「池子」多大由 pick 決定:環境 ISEP_DRAIN=1 時 pick 會往下看到整個 open 票池(inkstone/ISEP#130);
|
||||||
|
# 本機沒設時只看主線與逾期——總管跑過 handback 不會因此被叫去抓票池。
|
||||||
|
#
|
||||||
# 迴歸測試:hooks/tests/queue-drain-guard.test.sh
|
# 迴歸測試:hooks/tests/queue-drain-guard.test.sh
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ PASS=0; FAIL=0
|
|||||||
export TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x
|
export TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x
|
||||||
export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
|
export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
|
||||||
export ISEP_COUNTDOWN_NOW=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,9,7,0,0,tzinfo=d.timezone.utc).timestamp()))')
|
export ISEP_COUNTDOWN_NOW=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,9,7,0,0,tzinfo=d.timezone.utc).timestamp()))')
|
||||||
unset ISEP_MAINLINE_FILE ISEP_IDENTITY CLAUDE_PROJECT_DIR
|
unset ISEP_MAINLINE_FILE ISEP_IDENTITY CLAUDE_PROJECT_DIR ISEP_DRAIN ISEP_WORKER_LOGIN
|
||||||
|
|
||||||
MAIN="$TMP/mainline.json"
|
MAIN="$TMP/mainline.json"
|
||||||
cat > "$MAIN" <<'JSON'
|
cat > "$MAIN" <<'JSON'
|
||||||
@@ -57,9 +57,20 @@ POOL = {
|
|||||||
"Arcrun": [I("Arcrun", 6, ["s/todo", "p/high"], "main", created="2026-08-27T00:00:00Z"), # ★ 主線裡最優先
|
"Arcrun": [I("Arcrun", 6, ["s/todo", "p/high"], "main", created="2026-08-27T00:00:00Z"), # ★ 主線裡最優先
|
||||||
I("Arcrun", 7, ["s/todo"], "late"), # 逾期里程碑:主線抓盡才輪到
|
I("Arcrun", 7, ["s/todo"], "late"), # 逾期里程碑:主線抓盡才輪到
|
||||||
I("Arcrun", 8, ["s/todo"], "future")], # 不是主線也沒逾期:永遠不抓
|
I("Arcrun", 8, ["s/todo"], "future")], # 不是主線也沒逾期:永遠不抓
|
||||||
"mira": [I("mira", 9, ["s/todo", "s/doing"], "main")], # 09-07 實查 mira#6 的形狀:有人在做
|
"mira": [I("mira", 9, ["s/todo", "s/doing"], "main"), # 09-07 實查 mira#6 的形狀:有人在做
|
||||||
|
# ── 票池那一層才看得到的(inkstone/ISEP#130):都沒有里程碑 ──
|
||||||
|
I("mira", 20, ["s/triage"], None), # 還沒決定要不要做:不抓
|
||||||
|
I("mira", 21, [], None), # 沒有 s/*(還沒驗傷):不抓,但要被點名
|
||||||
|
I("mira", 22, ["s/todo"], None, assignees=("Leo",)), # 指派 Leo:不抓
|
||||||
|
I("mira", 23, ["s/backlog", "Human"], None), # Human:不抓
|
||||||
|
I("mira", 24, ["s/review"], None), # 等總管:不抓
|
||||||
|
I("mira", 25, ["s/backlog", "p/high"], None, created="2026-09-05T00:00:00Z"), # ★ 票池裡最優先
|
||||||
|
I("mira", 26, ["s/todo", "hub"], None)], # hub:容器不是任務
|
||||||
|
# 分頁:伺服器每頁上限 30(比 pick 要的 50 小)——只看「不滿一頁」就停的寫法會漏掉第 31 張之後的
|
||||||
|
"big": [I("big", n, [], None) for n in range(1, 31)] + [I("big", 31, ["s/backlog"], None)],
|
||||||
}
|
}
|
||||||
MSN = {"ISEP": ["main", "late", "future"], "Arcrun": ["main", "late", "future"], "mira": ["main"]}
|
MSN = {"ISEP": ["main", "late", "future"], "Arcrun": ["main", "late", "future"], "mira": ["main"]}
|
||||||
|
SERVER_PAGE_MAX = 30
|
||||||
# 相依邊(inkstone/ISEP#133):主線錨在 Arcrun#48,Arcrun#6 把 ISEP#1 當相依 ⇒ ISEP#1 在主線上;
|
# 相依邊(inkstone/ISEP#133):主線錨在 Arcrun#48,Arcrun#6 把 ISEP#1 當相依 ⇒ ISEP#1 在主線上;
|
||||||
# ISEP#13 掛在 ISEP 的同名里程碑但沒有任何邊 ⇒ 不在主線上。mira#9 同理(同名、沒邊)。
|
# ISEP#13 掛在 ISEP 的同名里程碑但沒有任何邊 ⇒ 不在主線上。mira#9 同理(同名、沒邊)。
|
||||||
DEPS = {"Arcrun#6": ["ISEP#1"]}
|
DEPS = {"Arcrun#6": ["ISEP#1"]}
|
||||||
@@ -84,9 +95,13 @@ def fake_api(path, payload=None, method=None):
|
|||||||
repo = path.split("/")[3]
|
repo = path.split("/")[3]
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
q = urllib.parse.parse_qs(path.split("?", 1)[1])
|
q = urllib.parse.parse_qs(path.split("?", 1)[1])
|
||||||
want = q.get("milestones", [""])[0]
|
rows = [i for i in POOL[repo] if f"{repo}#{i['number']}" not in DROP]
|
||||||
return [i for i in POOL[repo] if (i["milestone"] or {}).get("title") == want
|
if "milestones" in q:
|
||||||
and f"{repo}#{i['number']}" not in DROP]
|
want = q["milestones"][0]
|
||||||
|
return [i for i in rows if (i["milestone"] or {}).get("title") == want]
|
||||||
|
# 不帶里程碑=整個 repo 的 open 票(票池那一層),照 Gitea 的分頁
|
||||||
|
page = int(q.get("page", ["1"])[0]); per = min(int(q.get("limit", ["50"])[0]), SERVER_PAGE_MAX)
|
||||||
|
return rows[(page - 1) * per: page * per]
|
||||||
if path.endswith("/labels?limit=100"):
|
if path.endswith("/labels?limit=100"):
|
||||||
return [{"name": n, "id": k} for k, n in
|
return [{"name": n, "id": k} for k, n in
|
||||||
enumerate(["s/todo", "s/doing", "s/review", "p/high", "Human"], 1)]
|
enumerate(["s/todo", "s/doing", "s/review", "p/high", "Human"], 1)]
|
||||||
@@ -182,7 +197,10 @@ export DROP='["Arcrun#6","ISEP#1"]'
|
|||||||
p "★ 主線兩張都被拿走後 → 回逾期里程碑的 Arcrun#7" "True" "'inkstone/Arcrun#7' in out and rc is None" "$ARGS"
|
p "★ 主線兩張都被拿走後 → 回逾期里程碑的 Arcrun#7" "True" "'inkstone/Arcrun#7' in out and rc is None" "$ARGS"
|
||||||
export DROP='["Arcrun#6","ISEP#1","Arcrun#7"]'
|
export DROP='["Arcrun#6","ISEP#1","Arcrun#7"]'
|
||||||
p "★ 主線與逾期都抓盡 → 離開碼 1(真的沒有,不是故障)" "1" "rc" "$ARGS"
|
p "★ 主線與逾期都抓盡 → 離開碼 1(真的沒有,不是故障)" "1" "rc" "$ARGS"
|
||||||
p "訊息明講「不要去抓 backlog」(出路是收工回報,不是換規則)" "True" "'backlog' in out and '收工' in out" "$ARGS"
|
p "沒帶 --pool 時訊息明講「這一趟沒有看票池」並給出 --pool 這條路(ISEP#130:不再說成正常收工)" "True" \
|
||||||
|
"'沒有看票池' in out and '--pool' in out and '不是故障' not in out" "$ARGS"
|
||||||
|
p "沒帶 --pool、主線與逾期都抓盡 → 票池一通都沒讀(本機總管不會因此被點名去抓票池)" "False" \
|
||||||
|
"any('page=' in p for _,p,_ in rec)" "$ARGS"
|
||||||
p "抓盡時**沒有**回 Arcrun#8(未來里程碑)" "False" "'Arcrun#8' in out" "$ARGS"
|
p "抓盡時**沒有**回 Arcrun#8(未來里程碑)" "False" "'Arcrun#8' in out" "$ARGS"
|
||||||
unset DROP
|
unset DROP
|
||||||
|
|
||||||
@@ -236,6 +254,53 @@ p "擋下時訊息講得出理由與出路(pick)" "True" "'Human' in out and
|
|||||||
p "身份名不在名單上 → 在打任何 API 之前就擋(一通都沒打)" "True" "rc==2 and len(rec)==0" \
|
p "身份名不在名單上 → 在打任何 API 之前就擋(一通都沒打)" "True" "rc==2 and len(rec)==0" \
|
||||||
"[\"cmd_claim\",\"inkstone/Arcrun#6\",\"--mainline\",\"$MAIN\",\"--name\",\"路人\"]"
|
"[\"cmd_claim\",\"inkstone/Arcrun#6\",\"--mainline\",\"$MAIN\",\"--name\",\"路人\"]"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "── ⑤ 票池那一層(inkstone/ISEP#130 c7801:幾百張 open 票,工人卻回「沒事做」)──"
|
||||||
|
POOLA="[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\"]"
|
||||||
|
POOLALL="[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\",\"--all\",\"--json\"]"
|
||||||
|
p "pickable:s/backlog 在票池那一層可抓" "True" "m.pickable(I('ISEP',4,['s/backlog'],None),None,None,m.POOL_STATUSES)[0]"
|
||||||
|
p "pickable:s/triage 在票池那一層仍不可抓(要不要做還沒決定)" "False" "m.pickable(I('x',1,['s/triage'],None),None,None,m.POOL_STATUSES)[0]"
|
||||||
|
p "pickable:沒有 s/* 仍不可抓,理由講得出「沒有 s/*」" "True" "'沒有 s/*' in m.pickable(I('x',1,[],None),None,None,m.POOL_STATUSES)[1]"
|
||||||
|
p "pickable:票池裡的 backlog 掛 Human → 不可抓(不該碰的一格都沒放寬)" "False" "m.pickable(I('x',1,['s/backlog','Human'],None),None,None,m.POOL_STATUSES)[0]"
|
||||||
|
p "pickable:票池裡指派給 Leo → 不可抓" "False" "m.pickable(I('x',1,['s/todo'],None,assignees=('Leo',)),None,'claude-code',m.POOL_STATUSES)[0]"
|
||||||
|
p "順序:同優先序 s/todo 先於 s/backlog" "True" \
|
||||||
|
"m.pick_order(I('a',9,['s/todo'],None)) < m.pick_order(I('a',1,['s/backlog'],None))"
|
||||||
|
p "主線有票時帶 --pool 也照樣先回主線的 Arcrun#6,票池一通都沒讀(能省就省)" "True" \
|
||||||
|
"'inkstone/Arcrun#6' in out and not any('page=' in p for _,p,_ in rec)" "$POOLA"
|
||||||
|
export DROP='["Arcrun#6","ISEP#1","Arcrun#7"]'
|
||||||
|
p "★ 主線與逾期都抓盡、帶 --pool → 回票池裡最優先的 mira#25(p/high 的 backlog)" "True" \
|
||||||
|
"'inkstone/mira#25' in out and rc is None" "$POOLA"
|
||||||
|
p "★ --all:票池可抓的恰好是這幾張,照 p/high → s/todo → s/backlog 排(Human/指派 Leo/triage/沒 s/*/review/s/doing/hub/PR 都不在)" \
|
||||||
|
"['inkstone/mira#25', 'inkstone/ISEP#5', 'inkstone/Arcrun#8', 'inkstone/ISEP#13', 'inkstone/ISEP#4', 'inkstone/big#31']" \
|
||||||
|
"[r['ref'] for r in json.loads(out) if r['group']=='票池']" "$POOLALL"
|
||||||
|
p "★ 分頁:伺服器每頁只給 30 張時,第 31 張(big#31)照樣讀得到" "True" "'inkstone/big#31' in out" "$POOLALL"
|
||||||
|
p "票池那一層也只讀不寫" "True" "all(mm=='GET' for mm,_,_ in rec)" "$POOLALL"
|
||||||
|
export ISEP_DRAIN=1
|
||||||
|
p "★ 環境 ISEP_DRAIN=1(雲端工人的環境)= 帶 --pool,指令不必改" "True" \
|
||||||
|
"'inkstone/mira#25' in out and rc==0" "[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--json\"]"
|
||||||
|
unset ISEP_DRAIN
|
||||||
|
unset DROP
|
||||||
|
export DROP='["Arcrun#6","ISEP#1","Arcrun#7","mira#25","Arcrun#8","ISEP#13","ISEP#5","ISEP#4","big#31"]'
|
||||||
|
p "★ 票池也抓盡 → 離開碼 1" "1" "rc" "$POOLA"
|
||||||
|
p "★ 可抓 0 張時列出池子大小與「不能抓的理由」各幾張(沒 s/* 的 31 張被點名),而且不說成「正常/不是故障」" "True" \
|
||||||
|
"'open 票池' in out and '31 張' in out and '沒有 s/*' in out and '不是故障' not in out and '正常' not in out" "$POOLA"
|
||||||
|
p "--json 抓盡時同一張理由表寫到 stderr(stdout 仍是 null,佇列閘照舊解析得動)" "True" \
|
||||||
|
"out.startswith('null') and '不能抓' not in out.split(chr(10))[0] and '沒有 s/*' in out" \
|
||||||
|
"[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\",\"--json\"]"
|
||||||
|
unset DROP
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " (認領票池裡的票)"
|
||||||
|
CLP="[\"cmd_claim\",\"inkstone/ISEP#4\",\"--mainline\",\"$MAIN\",\"--name\",\"isep-hand\",\"--pool\"]"
|
||||||
|
p "★ --pool 認領 backlog 的 ISEP#4 → 過,s/backlog 換成 s/doing" "True" \
|
||||||
|
"rc is None and [sorted(pl['labels']) for mm,p,pl in rec if mm=='PUT' and p.endswith('/issues/4/labels')]==[[2]]" "$CLP"
|
||||||
|
p "認領留言寫明是從 open 票池抓的、狀態從 s/backlog 變 s/doing" "True" \
|
||||||
|
"any('open 票池' in pl['body'] and 's/backlog → s/doing' in pl['body'] for mm,p,pl in rec if mm=='POST' and p.endswith('/comments'))" "$CLP"
|
||||||
|
blocked "--pool 也擋掛 Human 的 mira#23" inkstone/mira#23 --pool
|
||||||
|
blocked "--pool 也擋指派 Leo 的 mira#22" inkstone/mira#22 --pool
|
||||||
|
blocked "--pool 也擋 s/triage 的 mira#20" inkstone/mira#20 --pool
|
||||||
|
blocked "--pool 也擋沒有 s/* 的 mira#21" inkstone/mira#21 --pool
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "── ④ 動詞接得上(不帶假池子,走到網路才炸)──"
|
echo "── ④ 動詞接得上(不帶假池子,走到網路才炸)──"
|
||||||
out=$(python3 "$T" pick --mainline "$MAIN" 2>&1); got=$?
|
out=$(python3 "$T" pick --mainline "$MAIN" 2>&1); got=$?
|
||||||
|
|||||||
+130
-22
@@ -30,7 +30,8 @@ leo 2026-08-16 三句話,本工具就是它們的機械化:
|
|||||||
|
|
||||||
雲端工人抓票(inkstone/ISEP#131,規則機械可判,不靠 prompt 叮嚀):
|
雲端工人抓票(inkstone/ISEP#131,規則機械可判,不靠 prompt 叮嚀):
|
||||||
|
|
||||||
ticket pick [--mainline <主線檔>] [--all] [--json] 回一張現在可以抓的票(主線抓盡才抓逾期)
|
ticket pick [--mainline <主線檔>] [--all] [--json] [--pool]
|
||||||
|
回一張現在可以抓的票(主線 → 逾期里程碑 → --pool 時 open 票池)
|
||||||
ticket claim <票> [--as <登入名>] [--name <身份>] 認領=指派+s/doing+【身份】留言,一個動作
|
ticket claim <票> [--as <登入名>] [--name <身份>] 認領=指派+s/doing+【身份】留言,一個動作
|
||||||
ticket pick --claim … 上面兩件連著做
|
ticket pick --claim … 上面兩件連著做
|
||||||
|
|
||||||
@@ -894,17 +895,20 @@ def subtask_example(parent=None, draft=None):
|
|||||||
# 用法裡寫死 `ticket` 或 `scripts/ticket`,讀的人照著貼就又踩回這張票的坑。
|
# 用法裡寫死 `ticket` 或 `scripts/ticket`,讀的人照著貼就又踩回這張票的坑。
|
||||||
USAGE = {
|
USAGE = {
|
||||||
"pick":
|
"pick":
|
||||||
"用法:{bin} pick [--mainline <主線 json 檔>] [--all] [--json] [--claim [--as <登入名>] [--name <身份>]]\n"
|
"用法:{bin} pick [--mainline <主線 json 檔>] [--all] [--json] [--pool] [--claim [--as <登入名>] [--name <身份>]]\n"
|
||||||
" 回**一張**現在可以抓的票(主線抓盡才輪到逾期里程碑;不抓 backlog)。主線的成員=hub 里程碑裡的票+\n"
|
" 回**一張**現在可以抓的票:主線 → 主線抓盡才輪到逾期里程碑 → 帶 --pool(或環境 ISEP_DRAIN=1)時\n"
|
||||||
|
" 再輪到整個 open 票池的 s/todo/s/backlog(不限里程碑;Human、指派給別人、hub 照樣不抓,inkstone/ISEP#130)。\n"
|
||||||
|
" 可抓 0 張時列出池子裡每種「不能抓的理由」各幾張。主線的成員=hub 里程碑裡的票+\n"
|
||||||
" hub 票+它們的相依(跨 repo)——別 repo 的同名里程碑不算主線(inkstone/ISEP#133)\n"
|
" hub 票+它們的相依(跨 repo)——別 repo 的同名里程碑不算主線(inkstone/ISEP#133)\n"
|
||||||
" --mainline 不給時依序找:$ISEP_MAINLINE_FILE → ~/.claude/isep-countdown/mainline.json"
|
" --mainline 不給時依序找:$ISEP_MAINLINE_FILE → ~/.claude/isep-countdown/mainline.json"
|
||||||
" → $CLAUDE_PROJECT_DIR/system-dev/mainline.json\n"
|
" → $CLAUDE_PROJECT_DIR/system-dev/mainline.json\n"
|
||||||
" 離開碼:0 = 有票(印在 stdout);1 = 真的沒有可抓的票;2 = 讀不到(讀不到 ≠ 沒有)",
|
" 離開碼:0 = 有票(印在 stdout);1 = 真的沒有可抓的票;2 = 讀不到(讀不到 ≠ 沒有)",
|
||||||
"claim":
|
"claim":
|
||||||
"用法:{bin} claim <owner/repo#N> [--as <登入名,預設本 token 的使用者>] [--name <身份名,預設 subagent>]"
|
"用法:{bin} claim <owner/repo#N> [--as <登入名,預設本 token 的使用者>] [--name <身份名,預設 subagent>]"
|
||||||
" [--mainline <主線 json 檔>]\n"
|
" [--mainline <主線 json 檔>] [--pool]\n"
|
||||||
" 認領=指派給自己 + 換成 s/doing + 第一行【身份】的留言,**一個動作**。\n"
|
" 認領=指派給自己 + 換成 s/doing + 第一行【身份】的留言,**一個動作**。\n"
|
||||||
" 抓票規則在這裡再驗一次:不是 s/todo、已有人、掛 Human、不在主線也不逾期 ⇒ 擋下(離開碼 2)",
|
" 抓票規則在這裡再驗一次:不是 s/todo、已有人、掛 Human、不在主線也不逾期 ⇒ 擋下(離開碼 2);\n"
|
||||||
|
" 帶 --pool(或 ISEP_DRAIN=1)時 open 票池裡的 s/todo/s/backlog 也認領得了",
|
||||||
"subtask":
|
"subtask":
|
||||||
"用法:{bin} subtask <母票 owner/repo#N> --title \"<User Story>\" -F <內文檔>\n"
|
"用法:{bin} subtask <母票 owner/repo#N> --title \"<User Story>\" -F <內文檔>\n"
|
||||||
" [--repo <收件 repo,預設跟母票同一個>] [--label <s/xxx,預設 s/todo>]\n"
|
" [--repo <收件 repo,預設跟母票同一個>] [--label <s/xxx,預設 s/todo>]\n"
|
||||||
@@ -1270,7 +1274,12 @@ def cmd_loose(argv):
|
|||||||
#
|
#
|
||||||
# 規則(全部是 Gitea 欄位,沒有一格在猜文字):
|
# 規則(全部是 Gitea 欄位,沒有一格在猜文字):
|
||||||
# 可抓 s/* 狀態恰好是 {s/todo} + 沒有 assignee + 沒有 Human/human/* + 不是 hub + 有里程碑
|
# 可抓 s/* 狀態恰好是 {s/todo} + 沒有 assignee + 沒有 Human/human/* + 不是 hub + 有里程碑
|
||||||
# 且里程碑 = 主線;主線抓盡才抓「逾期的 open 里程碑」;backlog/沒里程碑一律不抓
|
# 且里程碑 = 主線;主線抓盡才抓「逾期的 open 里程碑」
|
||||||
|
# ⇒ 兩層都抓盡、且是接票的環境(--pool/ISEP_DRAIN=1)⇒ 第三層:整個 open 票池裡
|
||||||
|
# s/* 恰好是 s/todo 或 s/backlog 的(不限里程碑),其他格一格不放寬(inkstone/ISEP#130 c7801)
|
||||||
|
# 🔴 2026-09-18 實量:org 299 張 open、254 張沒掛里程碑;舊規則只看主線成員與逾期里程碑,
|
||||||
|
# 實跑可抓 0 張 ⇒ 09-18 六趟 run 全部「可接 0 張、正常收工」。加上票池這一層,同一刻可抓 101 張。
|
||||||
|
# 主線/逾期是**先抓誰**,不是**只抓誰**。
|
||||||
# 認領 指派 + s/doing +【身份】留言,一個動作(`claim`)
|
# 認領 指派 + s/doing +【身份】留言,一個動作(`claim`)
|
||||||
# 完成 分支 + 證據 + s/review + `handback`——那半本來就有,不重做
|
# 完成 分支 + 證據 + s/review + `handback`——那半本來就有,不重做
|
||||||
#
|
#
|
||||||
@@ -1326,7 +1335,25 @@ def human_gated(labels):
|
|||||||
return any(n == "Human" or n.startswith("human/") for n in labels)
|
return any(n == "Human" or n.startswith("human/") for n in labels)
|
||||||
|
|
||||||
|
|
||||||
def pickable(issue, milestone_title, me=None):
|
# 票池那一層接受的狀態(inkstone/ISEP#130 c7801):labels.yaml 裡「決定要做、沒人在做」的只有這兩個。
|
||||||
|
# s/todo 已排進 milestone,等人領票
|
||||||
|
# s/backlog 驗過了、確定要做,但還沒排進任何 sprint
|
||||||
|
# 其他狀態都有人或有事在管:s/triage=還沒決定要不要做(總管/leo 的事)、s/doing=有人在做、
|
||||||
|
# s/review/s/stage=等總管或 leo、s/pending=在等外部。**沒有 s/* 的票**=還沒被驗傷,一樣不抓,
|
||||||
|
# 但會被點名計數(見 pool_candidates),不再沉默地消失在「可抓 0 張」後面。
|
||||||
|
MAINLINE_STATUSES = ("s/todo",)
|
||||||
|
POOL_STATUSES = ("s/todo", "s/backlog")
|
||||||
|
|
||||||
|
|
||||||
|
def pool_mode(argv=()):
|
||||||
|
"""要不要往下看到整個 open 票池。`--pool` 或環境 `ISEP_DRAIN=1`(雲端工人的環境本來就設它)。
|
||||||
|
|
||||||
|
為什麼不是預設全開:`queue-drain-guard` 在本機只要總管跑過 `ticket handback` 就會問 pick——
|
||||||
|
票池永遠有票,總管每次收工都會被點名去抓票,那是誤攔。接票的環境自己宣告,不靠猜。"""
|
||||||
|
return "--pool" in argv or os.environ.get("ISEP_DRAIN") == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def pickable(issue, milestone_title, me=None, statuses=MAINLINE_STATUSES):
|
||||||
"""這張票現在能不能被雲端工人抓走。回 (ok, 理由)。**純函式,不打網路。**
|
"""這張票現在能不能被雲端工人抓走。回 (ok, 理由)。**純函式,不打網路。**
|
||||||
|
|
||||||
`milestone_title`=要求它掛在哪個里程碑底下(主線,或輪到的那個逾期里程碑)。
|
`milestone_title`=要求它掛在哪個里程碑底下(主線,或輪到的那個逾期里程碑)。
|
||||||
@@ -1336,14 +1363,15 @@ def pickable(issue, milestone_title, me=None):
|
|||||||
指派 `claude-code` ⇒ 舊規則說「已經指派給 claude-code」⇒ **工人把自己當成別人**,
|
指派 `claude-code` ⇒ 舊規則說「已經指派給 claude-code」⇒ **工人把自己當成別人**,
|
||||||
09-11 那趟 run(Arcrun#86 c6917)就是用這條理由判定「主線沒有可抓的票」然後收工。
|
09-11 那趟 run(Arcrun#86 c6917)就是用這條理由判定「主線沒有可抓的票」然後收工。
|
||||||
`s/todo` 的意思本來就是「沒人在做」;有人在做的形狀是 `s/doing`,那格照擋。
|
`s/todo` 的意思本來就是「沒人在做」;有人在做的形狀是 `s/doing`,那格照擋。
|
||||||
|
`statuses`=接受哪些 s/*(主線/逾期只收 s/todo;票池另收 s/backlog,見 POOL_STATUSES)。
|
||||||
理由是給人看的:`pick --all` 與 `claim` 擋下時都印它,一張票為什麼不能抓要說得出來。
|
理由是給人看的:`pick --all` 與 `claim` 擋下時都印它,一張票為什麼不能抓要說得出來。
|
||||||
"""
|
"""
|
||||||
if issue.get("pull_request"):
|
if issue.get("pull_request"):
|
||||||
return False, "這是 PR,不是票"
|
return False, "這是 PR,不是票"
|
||||||
labels = _label_names(issue)
|
labels = _label_names(issue)
|
||||||
st = sorted(n for n in labels if n.startswith("s/"))
|
st = sorted(n for n in labels if n.startswith("s/"))
|
||||||
if st != ["s/todo"]:
|
if len(st) != 1 or st[0] not in statuses:
|
||||||
return False, "狀態不是單一的 s/todo(現在是 %s)" % (st or "沒有 s/*")
|
return False, "狀態不是單一的 %s(現在是 %s)" % ("/".join(statuses), st or "沒有 s/*")
|
||||||
if issue.get("assignees") or issue.get("assignee"):
|
if issue.get("assignees") or issue.get("assignee"):
|
||||||
who = [a.get("login") for a in (issue.get("assignees") or []) if isinstance(a, dict)] \
|
who = [a.get("login") for a in (issue.get("assignees") or []) if isinstance(a, dict)] \
|
||||||
or [(issue.get("assignee") or {}).get("login")]
|
or [(issue.get("assignee") or {}).get("login")]
|
||||||
@@ -1371,11 +1399,12 @@ def pickable(issue, milestone_title, me=None):
|
|||||||
|
|
||||||
|
|
||||||
def pick_order(issue):
|
def pick_order(issue):
|
||||||
"""同一個里程碑裡先抓誰:p/high → 沒標 → p/low;再看開票時間(舊的先);最後票號。
|
"""同一組裡先抓誰:p/high → 沒標 → p/low;同優先序 s/todo 先於 s/backlog(排進 sprint 的先);
|
||||||
結果要**可重現**——同一個池子問兩次要拿到同一張。"""
|
再看開票時間(舊的先);最後票號。結果要**可重現**——同一個池子問兩次要拿到同一張。"""
|
||||||
labels = _label_names(issue)
|
labels = _label_names(issue)
|
||||||
prio = 0 if "p/high" in labels else (2 if "p/low" in labels else 1)
|
prio = 0 if "p/high" in labels else (2 if "p/low" in labels else 1)
|
||||||
return (prio, issue.get("created_at") or "", issue.get("number") or 0)
|
status = 1 if "s/backlog" in labels else 0
|
||||||
|
return (prio, status, issue.get("created_at") or "", issue.get("number") or 0)
|
||||||
|
|
||||||
|
|
||||||
def _ref_of(issue, owner=None, repo=None):
|
def _ref_of(issue, owner=None, repo=None):
|
||||||
@@ -1463,6 +1492,54 @@ def _fetch_issue(ref):
|
|||||||
return it
|
return it
|
||||||
|
|
||||||
|
|
||||||
|
def _open_issues(owner, repo):
|
||||||
|
"""一個 repo 的全部 open 票(分頁讀完;Gitea 每頁上限 50,只讀一頁會漏)。"""
|
||||||
|
out, page = [], 1
|
||||||
|
while page <= 40:
|
||||||
|
q = urllib.parse.urlencode({"state": "open", "type": "issues", "limit": 50, "page": page})
|
||||||
|
rows = [it for it in (api("/repos/%s/%s/issues?%s" % (owner, repo, q)) or []) if isinstance(it, dict)]
|
||||||
|
for it in rows:
|
||||||
|
it.setdefault("repository", {"full_name": "%s/%s" % (owner, repo)})
|
||||||
|
if not rows: # 讀到空頁才停:伺服器的每頁上限可能比 50 小,看「不滿一頁」會提早收手
|
||||||
|
break
|
||||||
|
out += rows
|
||||||
|
page += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def pool_candidates(owner, me, seen=()):
|
||||||
|
"""第三層:整個 org 的 open 票池(inkstone/ISEP#130 c7801)。回 (可抓的票…, 池子大小, {不能抓的理由: 張數})。
|
||||||
|
|
||||||
|
leo 09-18:「我看到 Gitea 有 446 張票,要它自己領工作就是持續消化工作,它說沒事做?」
|
||||||
|
⇒ 主線與逾期里程碑只是**先抓誰**,不是**只抓誰**。它們抓盡之後,池子裡「決定要做、沒人在做」的票
|
||||||
|
(POOL_STATUSES)照樣可抓。不該碰的一格都沒放寬:Human/human/*、指派給別人(含 Leo)、hub、PR。
|
||||||
|
「要 Mac/要 prod」沒有欄位可判——那是工人讀票後走 cloud-worker.md「做不了的票」:寫回票+改欄位,
|
||||||
|
pick 下一輪就不再回它(改的是 s/*,不是猜文字)。
|
||||||
|
|
||||||
|
`seen`=前兩層已經列過的票號,不重複。理由表一併回傳:**可抓 0 張時要說得出池子去哪了**。"""
|
||||||
|
found, why_count, total = [], {}, 0
|
||||||
|
for r in _repos(owner):
|
||||||
|
for it in _open_issues(owner, r):
|
||||||
|
if it.get("pull_request"):
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
ref = _ref_of(it)
|
||||||
|
if ref in seen:
|
||||||
|
continue
|
||||||
|
ok, why = pickable(it, None, me, POOL_STATUSES)
|
||||||
|
if ok:
|
||||||
|
found.append(it)
|
||||||
|
else:
|
||||||
|
why_count[why] = why_count.get(why, 0) + 1
|
||||||
|
return sorted(found, key=pick_order), total, why_count
|
||||||
|
|
||||||
|
|
||||||
|
def _me():
|
||||||
|
"""抓票者自己是誰:只指派給自己的 s/todo 票算可抓(inkstone/ISEP#33,見 pickable)
|
||||||
|
問不到(舊 token、測試替身沒有 /user)⇒ None ⇒ 照舊規則,不因為這一格讓 pick 整個死掉"""
|
||||||
|
return os.environ.get("ISEP_WORKER_LOGIN") or ((api_soft("/user") or {}).get("login"))
|
||||||
|
|
||||||
|
|
||||||
def mainline_members(ms):
|
def mainline_members(ms):
|
||||||
"""主線的成員(inkstone/ISEP#133):hub 里程碑裡的票+hub 票+它們的相依(跨 repo)。
|
"""主線的成員(inkstone/ISEP#133):hub 里程碑裡的票+hub 票+它們的相依(跨 repo)。
|
||||||
判準在 `hooks/lib/mainline.py::collect_members`,這裡只是把本檔的 `api` 遞進去。"""
|
判準在 `hooks/lib/mainline.py::collect_members`,這裡只是把本檔的 `api` 遞進去。"""
|
||||||
@@ -1481,9 +1558,7 @@ def pick_candidates(ms, now=None):
|
|||||||
owner = ms.get("owner") or ORG
|
owner = ms.get("owner") or ORG
|
||||||
anchor = "%s/%s#%s" % (ms.get("owner"), ms.get("repo"), ms.get("id"))
|
anchor = "%s/%s#%s" % (ms.get("owner"), ms.get("repo"), ms.get("id"))
|
||||||
|
|
||||||
# 抓票者自己是誰:只指派給自己的 s/todo 票算可抓(inkstone/ISEP#33,見 pickable)
|
me = _me()
|
||||||
# 問不到(舊 token、測試替身沒有 /user)⇒ me=None ⇒ 照舊規則,不因為這一格讓 pick 整個死掉
|
|
||||||
me = os.environ.get("ISEP_WORKER_LOGIN") or ((api_soft("/user") or {}).get("login"))
|
|
||||||
|
|
||||||
refs, info = mainline_members(ms)
|
refs, info = mainline_members(ms)
|
||||||
found = []
|
found = []
|
||||||
@@ -1524,8 +1599,15 @@ def cmd_pick(argv):
|
|||||||
ms, src = load_mainline(argv)
|
ms, src = load_mainline(argv)
|
||||||
as_json = "--json" in argv
|
as_json = "--json" in argv
|
||||||
show_all = "--all" in argv
|
show_all = "--all" in argv
|
||||||
|
pool = pool_mode(argv)
|
||||||
|
pool_total, pool_why = None, {}
|
||||||
try:
|
try:
|
||||||
groups = pick_candidates(ms)
|
groups = pick_candidates(ms)
|
||||||
|
# 第三層只在前兩層抓盡(或要看全部)時才讀——它要翻整個 org,能省就省
|
||||||
|
if pool and (show_all or not any(its for _, _, its in groups)):
|
||||||
|
seen = {_ref_of(it) for _, _, its in groups for it in its}
|
||||||
|
found, pool_total, pool_why = pool_candidates(ms.get("owner") or ORG, _me(), seen)
|
||||||
|
groups.append(("票池", "open 票池(%s,不限里程碑)" % "/".join(POOL_STATUSES), found))
|
||||||
except (urllib.error.URLError, OSError) as e:
|
except (urllib.error.URLError, OSError) as e:
|
||||||
die("🔴 拿不到 Gitea(%s)。讀不到 ≠ 沒有可抓的票——先修連線,不要空轉也不要抓別的。" % e)
|
die("🔴 拿不到 Gitea(%s)。讀不到 ≠ 沒有可抓的票——先修連線,不要空轉也不要抓別的。" % e)
|
||||||
|
|
||||||
@@ -1535,17 +1617,17 @@ def cmd_pick(argv):
|
|||||||
"labels": _label_names(it), "created_at": it.get("created_at")}
|
"labels": _label_names(it), "created_at": it.get("created_at")}
|
||||||
for g, t, it in flat]
|
for g, t, it in flat]
|
||||||
print(json.dumps(rows if show_all else (rows[0] if rows else None), ensure_ascii=False, indent=2))
|
print(json.dumps(rows if show_all else (rows[0] if rows else None), ensure_ascii=False, indent=2))
|
||||||
|
if not rows:
|
||||||
|
_explain_empty(pool, pool_total, pool_why, sys.stderr)
|
||||||
sys.exit(0 if rows else 1)
|
sys.exit(0 if rows else 1)
|
||||||
|
|
||||||
print("🎯 主線:「%s」(來源:%s)" % (ms["title"], src))
|
print("🎯 主線:「%s」(來源:%s)" % (ms["title"], src))
|
||||||
for g, t, its in groups:
|
for g, t, its in groups:
|
||||||
if g == "主線" or its or show_all:
|
if g in ("主線", "票池") or its or show_all:
|
||||||
print(" %s「%s」:可抓 %d 張" % (g, t, len(its)))
|
print(" %s「%s」:可抓 %d 張" % (g, t, len(its)))
|
||||||
print()
|
print()
|
||||||
if not flat:
|
if not flat:
|
||||||
print("⚪ 沒有可以抓的票——主線與逾期里程碑都抓盡了。")
|
_explain_empty(pool, pool_total, pool_why, sys.stdout)
|
||||||
print(" 出路:收工回報(這是正常結果,不是故障)。")
|
|
||||||
print(" 🔴 不要去抓 backlog/沒里程碑/掛 Human/已有人的票——那些不在規則裡,抓了就是跳線。")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
shown = flat if show_all else flat[:1]
|
shown = flat if show_all else flat[:1]
|
||||||
@@ -1559,11 +1641,27 @@ def cmd_pick(argv):
|
|||||||
cmd_claim([_ref_of(top)] + [a for a in argv if a != "--claim"])
|
cmd_claim([_ref_of(top)] + [a for a in argv if a != "--claim"])
|
||||||
return
|
return
|
||||||
print("📌 認領(指派+s/doing+【身份】留言,一個動作):")
|
print("📌 認領(指派+s/doing+【身份】留言,一個動作):")
|
||||||
print(" %s claim %s --name <你的名字>" % (self_path(), _ref_of(flat[0][2])))
|
print(" %s claim %s --name <你的名字>%s" % (self_path(), _ref_of(flat[0][2]),
|
||||||
|
" --pool" if flat[0][0] == "票池" and "--pool" in argv else ""))
|
||||||
print(" 做完:分支+證據 → `%s handback %s --to %s --label s/review --next \"…\" --evidence <URL>`"
|
print(" 做完:分支+證據 → `%s handback %s --to %s --label s/review --next \"…\" --evidence <URL>`"
|
||||||
% (self_path(), _ref_of(flat[0][2]), HANDBACK_TO))
|
% (self_path(), _ref_of(flat[0][2]), HANDBACK_TO))
|
||||||
|
|
||||||
|
|
||||||
|
def _explain_empty(pool, total, why, out):
|
||||||
|
"""可抓 0 張時講清楚池子去哪了。**不准說成「正常、不是故障」**(inkstone/ISEP#130 c7797:
|
||||||
|
09-18 六趟 run 都把「可抓 0 張」寫成正常收工,而池子裡有幾百張 open 票)。"""
|
||||||
|
w = lambda line="": print(line, file=out)
|
||||||
|
if not pool:
|
||||||
|
w("⚪ 主線與逾期里程碑可抓 0 張——**這一趟沒有看票池**,所以這不代表沒事做。")
|
||||||
|
w(" 要看整個 open 票池:加 `--pool`(雲端工人的環境設了 ISEP_DRAIN=1 就自動看)。")
|
||||||
|
return
|
||||||
|
w("⚪ 可抓 0 張:主線、逾期里程碑、open 票池(%d 張)都沒有一張符合抓票規則。" % (total or 0))
|
||||||
|
w(" 這不是「沒事做」,是池子裡的票全都卡在下面這些格子——把這張表原樣放進收工摘要/載體票,交總管分診:")
|
||||||
|
for k, n in sorted(why.items(), key=lambda kv: (-kv[1], kv[0]))[:12]:
|
||||||
|
w(" %4d 張 %s" % (n, k))
|
||||||
|
w(" 🔴 不要自己改欄位去湊一張可抓的票(把 s/triage 改 s/todo、摘 Human、改指派)——那是總管/leo 的判斷。")
|
||||||
|
|
||||||
|
|
||||||
def cmd_claim(argv):
|
def cmd_claim(argv):
|
||||||
"""認領=指派 + s/doing +【身份】留言,**一個動作**。抓票規則在這裡再驗一次。"""
|
"""認領=指派 + s/doing +【身份】留言,**一個動作**。抓票規則在這裡再驗一次。"""
|
||||||
if not argv or argv[0] in ("-h", "--help"):
|
if not argv or argv[0] in ("-h", "--help"):
|
||||||
@@ -1596,21 +1694,31 @@ def cmd_claim(argv):
|
|||||||
if not (due and due < _now()):
|
if not (due and due < _now()):
|
||||||
ok, why = False, "里程碑「%s」既不是主線「%s」也還沒逾期(別 repo 的同名里程碑不算主線)" % (
|
ok, why = False, "里程碑「%s」既不是主線「%s」也還沒逾期(別 repo 的同名里程碑不算主線)" % (
|
||||||
ims.get("title"), ms["title"])
|
ims.get("title"), ms["title"])
|
||||||
|
tier = "(主線)" if on_mainline else "(逾期里程碑,主線已抓盡)"
|
||||||
|
if not ok and pool_mode(argv):
|
||||||
|
# 第三層:open 票池(inkstone/ISEP#130)。規則同 pick 那一層,一格不放寬 Human/指派/hub。
|
||||||
|
pok, pwhy = pickable(issue, None, who, POOL_STATUSES)
|
||||||
|
if pok:
|
||||||
|
ok, tier = True, "(open 票池,主線與逾期里程碑已抓盡)"
|
||||||
|
else:
|
||||||
|
why = pwhy
|
||||||
if not ok:
|
if not ok:
|
||||||
die("""🚫 %s/%s#%d 現在不能抓:%s
|
die("""🚫 %s/%s#%d 現在不能抓:%s
|
||||||
|
|
||||||
抓票規則(inkstone/ISEP#131,全部是 Gitea 欄位):
|
抓票規則(inkstone/ISEP#131,全部是 Gitea 欄位):
|
||||||
s/* 恰好是 s/todo + 沒有 assignee + 沒有 Human/human/* + 在主線上(hub 里程碑或相依;主線抓盡才抓逾期)
|
s/* 恰好是 s/todo + 沒有 assignee + 沒有 Human/human/* + 在主線上(hub 里程碑或相依;主線抓盡才抓逾期)
|
||||||
|
帶 --pool(或環境 ISEP_DRAIN=1)時另收 open 票池裡的 s/todo/s/backlog(inkstone/ISEP#130),其他格照舊
|
||||||
出路:`%s pick` 會回一張符合規則的票;這張如果真的該你做,回票上問總管改欄位,不要硬抓。""" % (
|
出路:`%s pick` 會回一張符合規則的票;這張如果真的該你做,回票上問總管改欄位,不要硬抓。""" % (
|
||||||
owner, repo, num, why, self_path()))
|
owner, repo, num, why, self_path()))
|
||||||
|
|
||||||
api("/repos/%s/%s/issues/%d" % (owner, repo, num), {"assignees": [who]}, method="PATCH")
|
api("/repos/%s/%s/issues/%d" % (owner, repo, num), {"assignees": [who]}, method="PATCH")
|
||||||
|
before = sorted(n for n in _label_names(issue) if n.startswith("s/"))
|
||||||
keep = [n for n in _label_names(issue) if not n.startswith("s/")] + ["s/doing"]
|
keep = [n for n in _label_names(issue) if not n.startswith("s/")] + ["s/doing"]
|
||||||
_set_labels(owner, repo, num, keep)
|
_set_labels(owner, repo, num, keep)
|
||||||
lines = [ident, "",
|
lines = [ident, "",
|
||||||
"🙋 **認領** → `%s`" % who, "",
|
"🙋 **認領** → `%s`" % who, "",
|
||||||
"- 里程碑:「%s」%s" % (ims.get("title") or "(無,靠相依在主線上)", "(主線)" if on_mainline else "(逾期里程碑,主線已抓盡)"),
|
"- 里程碑:「%s」%s" % (ims.get("title") or ("(無,靠相依在主線上)" if on_mainline else "(無)"), tier),
|
||||||
"- 狀態:s/todo → s/doing",
|
"- 狀態:%s → s/doing" % "、".join(before),
|
||||||
"- 做完:分支+證據 → `s/review` + `ticket handback`(不推 main、不出貨、不改里程碑)"]
|
"- 做完:分支+證據 → `s/review` + `ticket handback`(不推 main、不出貨、不改里程碑)"]
|
||||||
api("/repos/%s/%s/issues/%d/comments" % (owner, repo, num), {"body": "\n".join(lines)})
|
api("/repos/%s/%s/issues/%d/comments" % (owner, repo, num), {"body": "\n".join(lines)})
|
||||||
print("✅ 認領 %s/%s#%d → `%s`" % (owner, repo, num, who))
|
print("✅ 認領 %s/%s#%d → `%s`" % (owner, repo, num, who))
|
||||||
|
|||||||
Reference in New Issue
Block a user