新 repo 的工人不再靠人記得:課程工人 course-hand+開場點名「有票卻沒工人」(inkstone/ISEP#118)
- agents/course-hand.md:管 inkstone/ax-courses、course_gen、llm-wiki-template(c7358 leo「你可以製作一個」) 假設:llm-wiki-template 也歸課程工人(同為課程素材),總管不同意就把它拆出去 - hooks/lib/roster.py:`負責 repo` 可列多個(、或逗號),新增 which()/uncovered();find() 與閘本身未動 - scripts/roster which 改走 roster.which - scripts/isep-nag 第④類:org 裡有開著的票、名單上卻沒人負責的 repo,只進開場長版、不進 Telegram - 測試:roster-guard 25→31、overdue-nag 36→43;兩種突變(拿掉票數條件/只認第一個 repo)都會紅 - 文件:worker-roster.md 回答「新 repo 的工人誰在什麼時候加」;README/plugin.json 工人 8、scripts 65(樹上實數) 待總管定版(改了 agents/scripts,要升版才裝得上)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+53
-3
@@ -46,7 +46,18 @@ _NAME_RE = re.compile(r"^name:\s*(.+?)\s*$", re.M)
|
||||
_DESC_RE = re.compile(r"^description:\s*(.+?)\s*$", re.M)
|
||||
# 「**負責 repo**:inkstone/ISEP」——粗體記號與全形冒號都當可選,
|
||||
# 免得改一次排版就讓名單讀不到 repo(那會靜默失效,比擋錯更難發現)。
|
||||
_REPO_RE = re.compile(r"\*{0,2}負責\s*repo\*{0,2}\s*[::]\s*([^\s((]+)")
|
||||
# 一個工人可以管**幾個同一件事的 repo**(inkstone/ISEP#118:課程工人同時管課件內容
|
||||
# `ax-courses`、引擎 `course_gen`、學員範本 `llm-wiki-template`)——用「、」或逗號隔開。
|
||||
# 抓到行尾或第一個括號為止,再切開;只收長得像 `owner/repo` 的片段。
|
||||
_REPO_RE = re.compile(r"\*{0,2}負責\s*repo\*{0,2}\s*[::]\s*([^\n((]+)")
|
||||
_REPO_SPLIT = re.compile(r"[、,,\s]+")
|
||||
|
||||
|
||||
def _parse_repos(body):
|
||||
m = _REPO_RE.search(body)
|
||||
if not m:
|
||||
return []
|
||||
return [p for p in _REPO_SPLIT.split(m.group(1).strip()) if "/" in p]
|
||||
|
||||
|
||||
def roster_dir():
|
||||
@@ -81,11 +92,13 @@ def load(directory=None):
|
||||
if not m:
|
||||
continue
|
||||
dm = _DESC_RE.search(head)
|
||||
rm = _REPO_RE.search(body)
|
||||
repos = _parse_repos(body)
|
||||
out.append({
|
||||
"name": m.group(1).strip(),
|
||||
"description": (dm.group(1).strip() if dm else ""),
|
||||
"repo": (rm.group(1).strip() if rm else ""),
|
||||
# `repo` 維持字串(閘的注入、【身份】欄、one_liner 都吃它);比對一律用 `repos`
|
||||
"repo": "、".join(repos),
|
||||
"repos": repos,
|
||||
"path": path,
|
||||
"body": body.strip(),
|
||||
})
|
||||
@@ -113,6 +126,43 @@ def find(name, directory=None):
|
||||
return None
|
||||
|
||||
|
||||
def which(repo, directory=None, workers=None):
|
||||
"""這個 `owner/repo` 該派給誰 ⇒ 回工人清單(可能是空的)。大小寫不分。"""
|
||||
want = (repo or "").strip().lower()
|
||||
if not want:
|
||||
return []
|
||||
ws = workers if workers is not None else load(directory)
|
||||
return [w for w in ws if want in (r.lower() for r in w.get("repos") or [])]
|
||||
|
||||
|
||||
def uncovered(repo_rows, directory=None):
|
||||
"""給 Gitea `/orgs/{org}/repos` 回來的列 ⇒ 回「**有開著的票、卻沒有工人**」的 repo。
|
||||
|
||||
inkstone/ISEP#118:新 repo 的工人從來沒有人負責加——建 repo 的那一刻沒有任何東西提醒,
|
||||
要等到第一次派工撞上 roster-guard 才發現,而那時最省事的出路是開例外閘繞過去。
|
||||
|
||||
判準只看兩個事實,不看名字長相:
|
||||
· 名單上**沒有**任何工人的 `負責 repo` 包含它(唯一識別碼比對,方向是放行)
|
||||
· 它**有開著的票**(`open_issues_count > 0`,Gitea 這個數字不含 PR)
|
||||
⇒ 沒票的 repo(封存、投影用、還沒開張的)不叫:沒有工可派,就不是缺人
|
||||
封存的 repo 不叫。名單讀不到 ⇒ 回 None(**不是**「全部都缺人」——那是最貴的假情報)。
|
||||
"""
|
||||
ws = load(directory)
|
||||
if not ws:
|
||||
return None
|
||||
out = []
|
||||
for r in repo_rows or []:
|
||||
full = (r or {}).get("full_name") or ""
|
||||
if not full or r.get("archived"):
|
||||
continue
|
||||
n = int(r.get("open_issues_count") or 0)
|
||||
if n <= 0 or which(full, workers=ws):
|
||||
continue
|
||||
out.append({"repo": full, "open": n})
|
||||
out.sort(key=lambda x: (-x["open"], x["repo"]))
|
||||
return out
|
||||
|
||||
|
||||
def one_liner(w):
|
||||
repo = w.get("repo") or "(跨 repo)"
|
||||
return "%-18s %-26s %s" % (w["name"], repo, w.get("description", ""))
|
||||
|
||||
@@ -206,6 +206,55 @@ CH=$(printf '%s' "$IN" | CLAUDE_CODE_CHILD_SESSION=1 bash "$HOOK" 2>/dev/null)
|
||||
[ -z "$CH" ] && ok "㉟ 子 session 不叫(subagent 開工不是 leo 開工)" \
|
||||
|| no "㉟ 子 session 不叫" "實得:$CH"
|
||||
|
||||
echo "── G 群:有票卻沒有工人的 repo(inkstone/ISEP#118)──────────────────"
|
||||
# 假名單:只有一位管 acme/staffed 的工人,不碰真的 agents/
|
||||
mkdir -p "$TMP/roster"
|
||||
cat > "$TMP/roster/staffed.md" <<'MD'
|
||||
---
|
||||
name: staffed-hand
|
||||
description: 測試用
|
||||
---
|
||||
**負責 repo**:acme/staffed、acme/also-staffed
|
||||
## 開工前先讀
|
||||
## 紅線
|
||||
MD
|
||||
cat > "$TMP/gap.json" <<'JSON'
|
||||
{"milestones": [], "issues": [],
|
||||
"repos": [
|
||||
{"full_name":"acme/staffed","open_issues_count":4},
|
||||
{"full_name":"acme/also-staffed","open_issues_count":1},
|
||||
{"full_name":"acme/new-course","open_issues_count":5},
|
||||
{"full_name":"acme/quiet","open_issues_count":0},
|
||||
{"full_name":"acme/frozen","open_issues_count":3,"archived":true}
|
||||
]}
|
||||
JSON
|
||||
GR=$(ISEP_ROSTER_DIR="$TMP/roster" python3 "$NAG" --fixture "$TMP/gap.json" --now "$NOW" 2>&1)
|
||||
GS=$(ISEP_ROSTER_DIR="$TMP/roster" python3 "$NAG" --fixture "$TMP/gap.json" --now "$NOW" --short 2>&1)
|
||||
GJ=$(ISEP_ROSTER_DIR="$TMP/roster" python3 "$NAG" --fixture "$TMP/gap.json" --now "$NOW" --json 2>&1)
|
||||
has "$GR" "有票卻沒有工人的 repo(1 個)" && has "$GR" "acme/new-course(5 張票開著)" \
|
||||
&& ok "㊱ 新開、有票、名單上沒人的 repo 被點名(連同幾張票)" \
|
||||
|| no "㊱ 點名缺工人的 repo" "實得:$(printf '%s' "$GR" | tr '\n' ' ' | head -c 300)"
|
||||
has "$GR" "agents/" && ok "㊲ 講得出出路:去 agents/ 補工人(不是開例外閘)" || no "㊲ 出路" "沒講"
|
||||
if has "$GR" "acme/staffed(" || has "$GR" "acme/also-staffed" || has "$GR" "acme/quiet" || has "$GR" "acme/frozen"; then
|
||||
no "㊳ 不該叫的不要叫" "有人的/沒票的/封存的被點名了"
|
||||
else
|
||||
ok "㊳ 有工人的(含多 repo 的第二個)、沒票的、封存的都不叫"
|
||||
fi
|
||||
has "$GS" "查過了,沒有" && ! has "$GS" "工人" \
|
||||
&& ok "㊴ Telegram 短版不提工人(派工是總管的事,不吵 leo)" \
|
||||
|| no "㊴ 短版不提工人" "實得:$GS"
|
||||
printf '%s' "$GJ" | python3 -c '
|
||||
import json,sys
|
||||
assert json.load(sys.stdin)["findings"]["repos_without_worker"] == [{"repo":"acme/new-course","open":5}]
|
||||
' 2>/dev/null && ok "㊵ --json 帶得出同一份清單" || no "㊵ --json" "$(printf '%s' "$GJ" | head -c 200)"
|
||||
GN=$(ISEP_ROSTER_DIR="$TMP/沒有這個目錄" python3 "$NAG" --fixture "$TMP/gap.json" --now "$NOW" 2>&1)
|
||||
has "$GN" "沒查到" && ! has "$GN" "acme/staffed(" \
|
||||
&& ok "㊶ 名單讀不到 ⇒ 講「沒查到」,不把每個 repo 都說成缺人" \
|
||||
|| no "㊶ 名單讀不到" "實得:$(printf '%s' "$GN" | tr '\n' ' ' | head -c 300)"
|
||||
GO=$(python3 "$NAG" --fixture "$TMP/full.json" --now "$NOW" 2>&1)
|
||||
has "$GO" "沒查到" && ok "㊷ 舊格式 fixture(沒有 repos)⇒ 講「沒查到」,前三類照舊" \
|
||||
|| no "㊷ 舊格式相容" "實得:$(printf '%s' "$GO" | tail -3 | tr '\n' ' ')"
|
||||
|
||||
echo
|
||||
printf '通過 %d 條,失敗 %d 條\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" = 0 ]
|
||||
|
||||
@@ -200,6 +200,84 @@ else
|
||||
printf ' ❌ ⑲ 真實的 subagent_type(isep:isep-hand)認不出來——就是這個造成 8/28 的死鎖\n'; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo "── 驗收 5:課程工人(inkstone/ISEP#118):一個工人管三個 repo ──────────"
|
||||
|
||||
for r in inkstone/ax-courses inkstone/course_gen inkstone/llm-wiki-template; do
|
||||
N=$((N+1))
|
||||
if [ "$(python3 "$ROOT/scripts/roster" which "$r" 2>/dev/null)" = "course-hand" ]; then
|
||||
printf ' ✅ ⑳ `roster which %s` ⇒ course-hand\n' "$r"; PASS=$((PASS+1))
|
||||
else
|
||||
printf ' ❌ ⑳ `roster which %s` 沒回 course-hand(下一次派工又要開例外閘)\n' "$r"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# 真實派工的形狀:plugin 前綴 + 只有一行【工單】
|
||||
t 0 "㉑ 派 isep:course-hand 去 inkstone/ax-courses#6 ⇒ 放行(09-14 實撞開例外閘的那一張)" \
|
||||
"$(payload '【工單】inkstone/ax-courses#6' 'isep:course-hand' 'Agent')"
|
||||
|
||||
N=$((N+1))
|
||||
inj3=$(printf '%s' "$(payload '【工單】inkstone/ax-courses#6' 'isep:course-hand' 'Agent')" | bash "$HOOK" 2>/dev/null)
|
||||
if printf '%s' "$inj3" | python3 -c '
|
||||
import sys, json
|
||||
c = json.load(sys.stdin)["hookSpecificOutput"]["additionalContext"]
|
||||
for want in ("course-hand", "inkstone/ax-courses", "inkstone/course_gen", "pdftoppm", "## 紅線"):
|
||||
assert want in c, want
|
||||
' 2>/dev/null; then
|
||||
printf ' ✅ ㉒ 注入的卡片帶著它管的三個 repo 與課件驗收紅線(pdftoppm)\n'; PASS=$((PASS+1))
|
||||
else
|
||||
printf ' ❌ ㉒ 課程工人的注入缺東西\n'; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo "── 驗收 6:多 repo 解析與「有票卻沒工人」的判準(離線假名單)────────"
|
||||
|
||||
mkdir -p "$TMP/r6"
|
||||
cat > "$TMP/r6/multi.md" <<'MD'
|
||||
---
|
||||
name: multi-hand
|
||||
description: 測試用
|
||||
---
|
||||
**負責 repo**:acme/a、acme/b, acme/c(括號後面不算 acme/zzz)
|
||||
## 開工前先讀
|
||||
## 紅線
|
||||
MD
|
||||
cat > "$TMP/r6/single.md" <<'MD'
|
||||
---
|
||||
name: single-hand
|
||||
description: 測試用
|
||||
---
|
||||
**負責 repo**:acme/solo
|
||||
## 開工前先讀
|
||||
## 紅線
|
||||
MD
|
||||
|
||||
N=$((N+1))
|
||||
if python3 - "$ROOT" "$TMP/r6" <<'PY' 2>/dev/null
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(sys.argv[1], "hooks", "lib"))
|
||||
import roster
|
||||
d = sys.argv[2]
|
||||
assert [w["name"] for w in roster.which("acme/b", d)] == ["multi-hand"], "頓號後面的 repo 認不出來"
|
||||
assert [w["name"] for w in roster.which("acme/c", d)] == ["multi-hand"], "逗號後面的 repo 認不出來"
|
||||
assert [w["name"] for w in roster.which("ACME/SOLO", d)] == ["single-hand"], "大小寫不分壞了"
|
||||
assert roster.which("acme/zzz", d) == [], "括號裡的字被當成 repo"
|
||||
assert roster.which("acme/nobody", d) == [], "不存在的 repo 被認領了"
|
||||
rows = [
|
||||
{"full_name": "acme/a", "open_issues_count": 3}, # 有人 ⇒ 不叫
|
||||
{"full_name": "acme/lonely", "open_issues_count": 2}, # 沒人、有票 ⇒ 叫
|
||||
{"full_name": "acme/idle", "open_issues_count": 0}, # 沒人、沒票 ⇒ 不叫
|
||||
{"full_name": "acme/old", "open_issues_count": 9, "archived": True}, # 封存 ⇒ 不叫
|
||||
{"full_name": "acme/busy", "open_issues_count": 5}, # 沒人、有票 ⇒ 叫,排前面
|
||||
]
|
||||
got = roster.uncovered(rows, d)
|
||||
assert got == [{"repo": "acme/busy", "open": 5}, {"repo": "acme/lonely", "open": 2}], got
|
||||
assert roster.uncovered(rows, os.path.join(d, "不存在")) is None, "名單讀不到時沒有回 None"
|
||||
PY
|
||||
then
|
||||
printf ' ✅ ㉓ 頓號/逗號都認得、括號後不算;只叫「沒人又有票」、不叫封存與沒票;名單讀不到回 None\n'; PASS=$((PASS+1))
|
||||
else
|
||||
printf ' ❌ ㉓ 多 repo 解析或 uncovered 判準壞了\n'; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo
|
||||
printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
||||
Reference in New Issue
Block a user