Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee4af97765 | |||
| d2f706a16c | |||
| 25da674615 | |||
| 01dafb01b5 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "isep",
|
"name": "isep",
|
||||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(88 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、65 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(88 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、8 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、65 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||||
"version": "0.30.2",
|
"version": "0.30.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"inkstone",
|
"inkstone",
|
||||||
"guardrails",
|
"guardrails",
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
| | 數量 | 是什麼 |
|
| | 數量 | 是什麼 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `hooks/` | 62 支 + `hooks.json` | 全部機械閘(PreToolUse/Stop/SubagentStop/SessionStart/PostToolUse/UserPromptSubmit 共 88 條註冊)。**一支一行的白話盤點在 `docs/hooks-inventory.md`,那裡才是這兩個數字的家** |
|
| `hooks/` | 62 支 + `hooks.json` | 全部機械閘(PreToolUse/Stop/SubagentStop/SessionStart/PostToolUse/UserPromptSubmit 共 88 條註冊)。**一支一行的白話盤點在 `docs/hooks-inventory.md`,那裡才是這兩個數字的家** |
|
||||||
| `agents/` | 7 位 | **工人名單**(`inkstone/ISEP#86`)——派工時指名派給誰,規約見 `docs/governance/worker-roster.md` |
|
| `agents/` | 8 位 | **工人名單**(`inkstone/ISEP#86`)——派工時指名派給誰,規約見 `docs/governance/worker-roster.md` |
|
||||||
| `commands/` | 7 支 | `/wiki-recall` `/wiki-capture` `/cp-write` `/issue-handle` … |
|
| `commands/` | 7 支 | `/wiki-recall` `/wiki-capture` `/cp-write` `/issue-handle` … |
|
||||||
| `skills/` | 2 支 | `ship-check`(東西要出去之前)/`deep-recall`(把散落的枝葉還原成一棵樹)。🔴 **這兩支的內容不一定是在這裡寫的**——誰是真相源查 `docs/file-ownership.tsv` |
|
| `skills/` | 2 支 | `ship-check`(東西要出去之前)/`deep-recall`(把散落的枝葉還原成一棵樹)。🔴 **這兩支的內容不一定是在這裡寫的**——誰是真相源查 `docs/file-ownership.tsv` |
|
||||||
| `scripts/` | 62 支 | `ticket`/`roster`/`isep-nag`/`wiki-compress`/`github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) |
|
| `scripts/` | 65 支 | `ticket`/`roster`/`isep-nag`/`wiki-compress`/`github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) |
|
||||||
|
|
||||||
> 🔴 這五個數字**每次都要在自己的樹上實數**,不准沿用上一版、也不准用加減推
|
> 🔴 這五個數字**每次都要在自己的樹上實數**,不准沿用上一版、也不准用加減推
|
||||||
> (`system-dev/wiki/mistakes.md`:那是 leo 的驗收介面,多報就是假綠):
|
> (`system-dev/wiki/mistakes.md`:那是 leo 的驗收介面,多報就是假綠):
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: course-hand
|
||||||
|
description: 課程的工人——課件內容(ax-courses)、簡報引擎(course_gen)、學員範本(llm-wiki-template)。動這三個 repo 派它。
|
||||||
|
---
|
||||||
|
|
||||||
|
你是 **course-hand**,leo 課程這一族 repo 的工人。
|
||||||
|
|
||||||
|
**負責 repo**:inkstone/ax-courses、inkstone/course_gen、inkstone/llm-wiki-template
|
||||||
|
|
||||||
|
三個 repo 是同一件事的三段:`ax-courses` 放課件原始碼(內容),`course_gen` 是把它
|
||||||
|
build 成 PPTX/PDF 的引擎,`llm-wiki-template` 是學員照著做的範本(GitHub 上有公開版)。
|
||||||
|
|
||||||
|
## 開工前先讀
|
||||||
|
|
||||||
|
- 票所在那個 repo 的 `CLAUDE.md`
|
||||||
|
- `ax-courses`:`system-dev/wiki/status.md`,以及同目錄**最新一份** `HANDOVER-*.md`(先看它的 R 段)
|
||||||
|
- `course_gen`:`docs/CONTENT-GUIDE.md`(投影片怎麼寫才對)、`docs/SOP.md`(出一場課的流程與驗收三層)、
|
||||||
|
`docs/PLACEMENT.md`(東西放哪)
|
||||||
|
- `llm-wiki-template`:`README.md` 與 `README.en.md`(n8n 審核者讀的是英文版)
|
||||||
|
|
||||||
|
## 紅線
|
||||||
|
|
||||||
|
- **課件交付前要把 PDF 轉成圖逐頁看過**(`pdftoppm`)。`make build` 字量全過、`qa.py` 全綠
|
||||||
|
**都不算驗過**——2026-09-13 D3 就是全綠交出去,28 頁裡 8 頁是空的灰框。
|
||||||
|
- **`.pptx` 從來不是原始碼。** 內容改 `slides.md`,版型改 `templates/`;課件原始碼住 `ax-courses`,
|
||||||
|
引擎住 `course_gen`,不要把內容寫進引擎 repo。
|
||||||
|
- **不要在共用工作目錄切分支**——一條線用 `scripts/worktree open` 開自己的分身。
|
||||||
|
- 推 main 會被判成 subagent 擋下:**交回總管**,不要蓋戳記硬推、也不要找別的路推上去。
|
||||||
|
- `course_gen` 的 SOP §1 第 9 步寫的 `make capture` **不存在**,不要照著跑然後回報失敗。
|
||||||
|
- `llm-wiki-template` 的公開版在 GitHub:**任何寫到 GitHub 的動作都要 leo 解保險**(D20),自己不推。
|
||||||
|
- 版型取捨、課程骨架、要不要補圖這類**品味題**:把選項和各自的後果寫上票,交 leo 裁,不自己定。
|
||||||
+13
-4
@@ -553,11 +553,11 @@ python3 scripts/debt-worklist list --top 5
|
|||||||
**失敗**:前幾名一眼看不出為什麼在那裡 ⇒ 那些權重就是錯的,去改 `score()`,
|
**失敗**:前幾名一眼看不出為什麼在那裡 ⇒ 那些權重就是錯的,去改 `score()`,
|
||||||
**不要改成「總管覺得」**——這份清單的價值就在於它不靠記憶。
|
**不要改成「總管覺得」**——這份清單的價值就在於它不靠記憶。
|
||||||
|
|
||||||
### A21 — 沒人會叫的事會自己叫:36 條
|
### A21 — 沒人會叫的事會自己叫:43 條
|
||||||
```
|
```
|
||||||
bash hooks/tests/overdue-nag.test.sh
|
bash hooks/tests/overdue-nag.test.sh
|
||||||
```
|
```
|
||||||
**該看到**:`通過 36 條,失敗 0 條`。**全程離線**——資料走 `--fixture`、時鐘走 `--now`、
|
**該看到**:`通過 43 條,失敗 0 條`。**全程離線**——資料走 `--fixture`、時鐘走 `--now`、
|
||||||
網路用 `ISEP_NOTIFY_OFFLINE=1` 關掉。**不打 Gitea、不打實例、不留任何測試票。**
|
網路用 `ISEP_NOTIFY_OFFLINE=1` 關掉。**不打 Gitea、不打實例、不留任何測試票。**
|
||||||
|
|
||||||
**它在守什麼**(`inkstone/ISEP#93`,2026-08-27 實查):一張票掛著等 leo 三天了、
|
**它在守什麼**(`inkstone/ISEP#93`,2026-08-27 實查):一張票掛著等 leo 三天了、
|
||||||
@@ -577,6 +577,11 @@ bash hooks/tests/overdue-nag.test.sh
|
|||||||
**閘說會擋就不准從 python 這條路繞過去**——繞得過的閘等於不存在
|
**閘說會擋就不准從 python 這條路繞過去**——繞得過的閘等於不存在
|
||||||
- ㉛ 紅 ⇒ 探針把 `/tmp/.prod-write-ok` 燒掉了(那是總管單次用完即丟的授權)
|
- ㉛ 紅 ⇒ 探針把 `/tmp/.prod-write-ok` 燒掉了(那是總管單次用完即丟的授權)
|
||||||
- ㉟ 紅 ⇒ 每條 subagent 開工都吵 leo 一次
|
- ㉟ 紅 ⇒ 每條 subagent 開工都吵 leo 一次
|
||||||
|
- **G 群(㊱–㊷,`inkstone/ISEP#118`)**:新開的 repo 有票卻沒有工人,開場要點名
|
||||||
|
- ㊱㊵ 紅 ⇒ 缺工人的 repo 沒被點名 ⇒ 又回到「第一次派工撞閘才發現、開例外閘繞過」
|
||||||
|
- ㊳ 紅 ⇒ **誤報**:有工人的、沒票的、封存的被點名,每次開場念一串不相干的 repo
|
||||||
|
- ㊴ 紅 ⇒ 補工人這件總管的事跑進 leo 的 Telegram
|
||||||
|
- ㊶㊷ 紅 ⇒ 名單或 org 清單讀不到時,把「沒查到」講成「每個都缺人」或「都有人」
|
||||||
|
|
||||||
> 🔴 **跑測試前先確認 `CLAUDE_CODE_CHILD_SESSION` 沒有殘留**:這支閘刻意放行子 session,
|
> 🔴 **跑測試前先確認 `CLAUDE_CODE_CHILD_SESSION` 沒有殘留**:這支閘刻意放行子 session,
|
||||||
> 在一條 subagent 裡跑,F 群會全綠而且是**假綠**。測試檔自己會把它清掉。
|
> 在一條 subagent 裡跑,F 群會全綠而且是**假綠**。測試檔自己會把它清掉。
|
||||||
@@ -1130,11 +1135,11 @@ bash hooks/tests/dispatch-format-guard.test.sh
|
|||||||
- **F 群㉚–㉟ 任一紅 ⇒ 誤攔**。㉚特別重要:subagent 往上回報(`to: "main"`)是**交件**,
|
- **F 群㉚–㉟ 任一紅 ⇒ 誤攔**。㉚特別重要:subagent 往上回報(`to: "main"`)是**交件**,
|
||||||
擋它等於擋掉交件本身;㉟反過來,子 session 自己往下派工時規矩要照樣管它,不然是逃逸艙口
|
擋它等於擋掉交件本身;㉟反過來,子 session 自己往下派工時規矩要照樣管它,不然是逃逸艙口
|
||||||
|
|
||||||
### A16 — 工人有名字:指名派工、沒有這個人、注入它的檔案:24 條
|
### A16 — 工人有名字:指名派工、沒有這個人、注入它的檔案:31 條
|
||||||
```
|
```
|
||||||
bash hooks/tests/roster-guard.test.sh
|
bash hooks/tests/roster-guard.test.sh
|
||||||
```
|
```
|
||||||
**該看到**:`24 通過 / 0 失敗`。**離線、不打網路、不花錢**——純結構判斷(名字在不在名單裡),
|
**該看到**:`31 通過 / 0 失敗`(本頁先前寫 24,實跑是 25,⑲ 加進來時沒回頭改)。**離線、不打網路、不花錢**——純結構判斷(名字在不在名單裡),
|
||||||
沒有語意判官,每次結果一樣。
|
沒有語意判官,每次結果一樣。
|
||||||
|
|
||||||
**它在守什麼**(`inkstone/ISEP#86`):「身為派工的人,我要工人有名字,
|
**它在守什麼**(`inkstone/ISEP#86`):「身為派工的人,我要工人有名字,
|
||||||
@@ -1150,6 +1155,10 @@ bash hooks/tests/roster-guard.test.sh
|
|||||||
- ⑯⑰ 紅 ⇒ 注入壞了。那是「你是誰/先讀什麼/你的紅線」唯一的送達方式——
|
- ⑯⑰ 紅 ⇒ 注入壞了。那是「你是誰/先讀什麼/你的紅線」唯一的送達方式——
|
||||||
它壞了不會有人立刻發現,派工照樣送出去,只是工人不知道自己是誰
|
它壞了不會有人立刻發現,派工照樣送出去,只是工人不知道自己是誰
|
||||||
- ⑱ 紅 ⇒ 驗收 3 沒過:票上的【身份】欄吃不下工人名字(或把原本三個角色弄壞了)
|
- ⑱ 紅 ⇒ 驗收 3 沒過:票上的【身份】欄吃不下工人名字(或把原本三個角色弄壞了)
|
||||||
|
- ⑳㉑㉒ 紅 ⇒ `inkstone/ISEP#118` 沒過:課程三個 repo 找不到 `course-hand`,
|
||||||
|
或用真實形狀 `isep:course-hand` 派工被擋 ⇒ 派 ax-courses 又要開例外閘
|
||||||
|
- ㉓ 紅 ⇒ 一個工人管多個 repo 的解析壞了(只認第一個),或「有票卻沒工人」的判準
|
||||||
|
開始點名沒票/封存的 repo(誤報)。2026-09-14 突變驗過:拿掉票數條件、只認第一個 repo,這條都會紅
|
||||||
|
|
||||||
### A17 — 未經調查不寫診斷:26 條
|
### A17 — 未經調查不寫診斷:26 條
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
| 段 | 問題 | 為什麼一定要有 |
|
| 段 | 問題 | 為什麼一定要有 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `**負責 repo**` | 它管哪一個 repo | 派工時挑人的唯一判準;也是【身份】欄第二格 |
|
| `**負責 repo**` | 它管哪個(或哪幾個)repo | 派工時挑人的唯一判準;也是【身份】欄第二格。同一件事的幾個 repo 用「、」隔開(例:`course-hand`) |
|
||||||
| `## 開工前先讀` | 它要先讀什麼 | 這是**每個工人各自不同**的部分——共通規定補不了 |
|
| `## 開工前先讀` | 它要先讀什麼 | 這是**每個工人各自不同**的部分——共通規定補不了 |
|
||||||
| `## 紅線` | 它不准做什麼 | 全機共通的紅線在共通規定裡;這裡只寫這個 repo 特有的 |
|
| `## 紅線` | 它不准做什麼 | 全機共通的紅線在共通規定裡;這裡只寫這個 repo 特有的 |
|
||||||
| frontmatter `description` | 一句話它是誰 | `scripts/roster list` 印的就是這一行 |
|
| frontmatter `description` | 一句話它是誰 | `scripts/roster list` 印的就是這一行 |
|
||||||
@@ -39,3 +39,24 @@ bash hooks/tests/roster-guard.test.sh # 全綠
|
|||||||
```
|
```
|
||||||
|
|
||||||
**不必改任何 hook**——名單是資料,閘讀的是這個目錄。
|
**不必改任何 hook**——名單是資料,閘讀的是這個目錄。
|
||||||
|
|
||||||
|
## 新開一個 repo 之後,工人是誰在什麼時候加的(inkstone/ISEP#118)
|
||||||
|
|
||||||
|
**以前的答案是「靠人記得」,所以一定會忘。** 實查 2026-09-14:org 裡 20 個 repo,
|
||||||
|
名單只蓋到 6 個;`llm-wiki-template`(09-01 建)與 `ax-courses`(09-07 建)
|
||||||
|
都是**第一次派工撞上 `roster-guard.sh`** 才發現沒人,而那一刻最省事的出路是
|
||||||
|
開 `/tmp/.roster-ok-*` 例外閘繞過去——繞過去之後,下一次還是會撞。
|
||||||
|
|
||||||
|
**現在的答案**:
|
||||||
|
|
||||||
|
- **什麼時候**:建 repo 之後,**開第一張票後的下一個 session 開場**。
|
||||||
|
- **誰叫**:`scripts/isep-nag`(掛在 `overdue-nag-guard.sh` 上,開場跑一次)的第 ④ 類
|
||||||
|
「**有票卻沒有工人的 repo**」——拿 org 的 repo 清單比對本目錄的 `負責 repo`。
|
||||||
|
- **誰加**:總管。在這個目錄加一個工人檔,或把 repo 寫進既有工人的 `負責 repo`。
|
||||||
|
|
||||||
|
判準只有兩個事實:名單上沒有人負責它、它有開著的票。
|
||||||
|
**沒票的 repo 不叫**(封存、投影用、還沒開張的——沒有工可派,就不是缺人),
|
||||||
|
所以這一段不會變成每天念一串不相干東西的噪音。
|
||||||
|
**只進開場給總管看的長版,不進 Telegram**——補工人是總管的事,不吵 leo。
|
||||||
|
|
||||||
|
手動看一次:`python3 scripts/isep-nag`(`--json` 裡的 `repos_without_worker`)。
|
||||||
|
|||||||
@@ -264,7 +264,7 @@
|
|||||||
| `session-start-recall.sh` | 對話一開始就自動把「全局現況」(Gitea 各 repo 的票、KBDB 的藏書地圖)推到 AI 眼前,不必等它自己想到要查。 | 📝 記錄(context 注入) |
|
| `session-start-recall.sh` | 對話一開始就自動把「全局現況」(Gitea 各 repo 的票、KBDB 的藏書地圖)推到 AI 眼前,不必等它自己想到要查。 | 📝 記錄(context 注入) |
|
||||||
| `scripts/settings-allow-sync --quiet` | (不是閘,是腳本)對話一開始把 ISEP 的權限白名單(`docs/permissions-allow.json`:`ticket`/`mainline`/`gate-ok`/`gitea-pr-merge`/`stage`(inkstone/ISEP#137 起,雲端對 stage 寫入的正門)五個正門工具,外加 Arcrun 的 `stage-deploy-artifacts.py`)寫進**這台機器**的 `~/.claude/settings.json`。只加不減、冪等;目標檔壞掉就不碰它、講一聲。**為什麼**:leo 09-07 親手加的四條只在本機,雲端沒有 ⇒ 同樣的動作被分類器擋(09-04 run log `permission_denials=6`);薄殼 repo 要 D20 開閘才推得動,所以清單住 ISEP 一份、兩台各自寫進自己的家目錄(inkstone/ISEP#130)。 | 📝 記錄(寫 settings) |
|
| `scripts/settings-allow-sync --quiet` | (不是閘,是腳本)對話一開始把 ISEP 的權限白名單(`docs/permissions-allow.json`:`ticket`/`mainline`/`gate-ok`/`gitea-pr-merge`/`stage`(inkstone/ISEP#137 起,雲端對 stage 寫入的正門)五個正門工具,外加 Arcrun 的 `stage-deploy-artifacts.py`)寫進**這台機器**的 `~/.claude/settings.json`。只加不減、冪等;目標檔壞掉就不碰它、講一聲。**為什麼**:leo 09-07 親手加的四條只在本機,雲端沒有 ⇒ 同樣的動作被分類器擋(09-04 run log `permission_denials=6`);薄殼 repo 要 D20 開閘才推得動,所以清單住 ISEP 一份、兩台各自寫進自己的家目錄(inkstone/ISEP#130)。 | 📝 記錄(寫 settings) |
|
||||||
| `scripts/mainline refresh` | (不是閘,是腳本)對話一開始把**現在的主線**那條 milestone 的進度與期限更新一次,好讓每回合眼前那一行講的是今天的數字。拿不到 Gitea 就原封不動——**寧可資料舊,不要把主線弄丟**。跑一次就結束,不輪詢。成員清單(hub 里程碑裡的票+沿相依邊跨 repo 收到的,`inkstone/ISEP#133`)**不在這裡重收**——真 Gitea 一走 19 秒;`set`/`adopt`/`refresh --members` 才重收,`pick`/`has`/focus guard 每次現問。**家目錄與 repo 那份都不在時**(雲端薄殼跑到這裡 `InkStoneCo/` 還沒 clone)先去 Gitea 讀 `inkstone/InkStoneCo` main 的 `system-dev/mainline.json` 一次寫進家目錄(`inkstone/ISEP#140`):匿名優先、被拒才帶 token;讀不到就退回,不報錯、逾時有上限。優先序寫在 `hooks/lib/mainline.py` 檔頭。 | 📝 記錄 |
|
| `scripts/mainline refresh` | (不是閘,是腳本)對話一開始把**現在的主線**那條 milestone 的進度與期限更新一次,好讓每回合眼前那一行講的是今天的數字。拿不到 Gitea 就原封不動——**寧可資料舊,不要把主線弄丟**。跑一次就結束,不輪詢。成員清單(hub 里程碑裡的票+沿相依邊跨 repo 收到的,`inkstone/ISEP#133`)**不在這裡重收**——真 Gitea 一走 19 秒;`set`/`adopt`/`refresh --members` 才重收,`pick`/`has`/focus guard 每次現問。**家目錄與 repo 那份都不在時**(雲端薄殼跑到這裡 `InkStoneCo/` 還沒 clone)先去 Gitea 讀 `inkstone/InkStoneCo` main 的 `system-dev/mainline.json` 一次寫進家目錄(`inkstone/ISEP#140`):匿名優先、被拒才帶 token;讀不到就退回,不報錯、逾時有上限。優先序寫在 `hooks/lib/mainline.py` 檔頭。 | 📝 記錄 |
|
||||||
| `overdue-nag-guard.sh` | 對話一開始就去 Gitea 撈**沒有人會叫的事**:逾期的 milestone、掛著等你的票(標「等了幾天」)、標著「有人在做」卻好幾天沒動的票;撈完用白話講出來,**有事就發 Telegram 給你**。沒東西可報時它會說「查過了,沒有」——**安靜跟壞掉長得一模一樣**。發不出去時不會靜默:它會先問這個 session 的 `prod-write-guard` 會不會擋(舊版把「發通知」誤認成「部署」),擋就改貼回票上並把原文印在眼前。只在開 session 時跑一次,**不輪詢、不掛排程**。 | 📝 記錄(context 注入 + Telegram) |
|
| `overdue-nag-guard.sh` | 對話一開始就去 Gitea 撈**沒有人會叫的事**:逾期的 milestone、掛著等你的票(標「等了幾天」)、標著「有人在做」卻好幾天沒動的票;撈完用白話講出來,**有事就發 Telegram 給你**。另外點名**有開著的票、工人名單上卻沒人負責的 repo**(新開 repo 後第一個開場就會叫,`inkstone/ISEP#118`)——這一類只給總管看,不發 Telegram。沒東西可報時它會說「查過了,沒有」——**安靜跟壞掉長得一模一樣**。發不出去時不會靜默:它會先問這個 session 的 `prod-write-guard` 會不會擋(舊版把「發通知」誤認成「部署」),擋就改貼回票上並把原文印在眼前。只在開 session 時跑一次,**不輪詢、不掛排程**。 | 📝 記錄(context 注入 + Telegram) |
|
||||||
| `wiki-size-guard.sh` | 對話一開始講出「哪幾個 wiki 檔已經沒有人讀得完了」(預設超過 1200 行就點名)。**讀不完的必讀檔,跟沒有那個檔的差別只在於它讓人以為有。** 另一半掛在寫檔上,見 B 組。 | 📝 記錄(context 注入) |
|
| `wiki-size-guard.sh` | 對話一開始講出「哪幾個 wiki 檔已經沒有人讀得完了」(預設超過 1200 行就點名)。**讀不完的必讀檔,跟沒有那個檔的差別只在於它讓人以為有。** 另一半掛在寫檔上,見 B 組。 | 📝 記錄(context 注入) |
|
||||||
| `skill-deploy-drift-guard.sh` | 如果「全機真正在用的 skill」跟「repo 裡版控的正本」內容對不上,就在開場講出來——避免用著一份沒人知道已經跟正本分家的舊拷貝。 | 📝 記錄 |
|
| `skill-deploy-drift-guard.sh` | 如果「全機真正在用的 skill」跟「repo 裡版控的正本」內容對不上,就在開場講出來——避免用著一份沒人知道已經跟正本分家的舊拷貝。 | 📝 記錄 |
|
||||||
|
|
||||||
|
|||||||
+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)
|
_DESC_RE = re.compile(r"^description:\s*(.+?)\s*$", re.M)
|
||||||
# 「**負責 repo**:inkstone/ISEP」——粗體記號與全形冒號都當可選,
|
# 「**負責 repo**:inkstone/ISEP」——粗體記號與全形冒號都當可選,
|
||||||
# 免得改一次排版就讓名單讀不到 repo(那會靜默失效,比擋錯更難發現)。
|
# 免得改一次排版就讓名單讀不到 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():
|
def roster_dir():
|
||||||
@@ -81,11 +92,13 @@ def load(directory=None):
|
|||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
dm = _DESC_RE.search(head)
|
dm = _DESC_RE.search(head)
|
||||||
rm = _REPO_RE.search(body)
|
repos = _parse_repos(body)
|
||||||
out.append({
|
out.append({
|
||||||
"name": m.group(1).strip(),
|
"name": m.group(1).strip(),
|
||||||
"description": (dm.group(1).strip() if dm else ""),
|
"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,
|
"path": path,
|
||||||
"body": body.strip(),
|
"body": body.strip(),
|
||||||
})
|
})
|
||||||
@@ -113,6 +126,43 @@ def find(name, directory=None):
|
|||||||
return 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):
|
def one_liner(w):
|
||||||
repo = w.get("repo") or "(跨 repo)"
|
repo = w.get("repo") or "(跨 repo)"
|
||||||
return "%-18s %-26s %s" % (w["name"], repo, w.get("description", ""))
|
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 開工)" \
|
[ -z "$CH" ] && ok "㉟ 子 session 不叫(subagent 開工不是 leo 開工)" \
|
||||||
|| no "㉟ 子 session 不叫" "實得:$CH"
|
|| 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
|
echo
|
||||||
printf '通過 %d 條,失敗 %d 條\n' "$PASS" "$FAIL"
|
printf '通過 %d 條,失敗 %d 條\n' "$PASS" "$FAIL"
|
||||||
[ "$FAIL" = 0 ]
|
[ "$FAIL" = 0 ]
|
||||||
|
|||||||
@@ -200,6 +200,84 @@ else
|
|||||||
printf ' ❌ ⑲ 真實的 subagent_type(isep:isep-hand)認不出來——就是這個造成 8/28 的死鎖\n'; FAIL=$((FAIL+1))
|
printf ' ❌ ⑲ 真實的 subagent_type(isep:isep-hand)認不出來——就是這個造成 8/28 的死鎖\n'; FAIL=$((FAIL+1))
|
||||||
fi
|
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
|
echo
|
||||||
printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N"
|
printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N"
|
||||||
[ "$FAIL" -eq 0 ] || exit 1
|
[ "$FAIL" -eq 0 ] || exit 1
|
||||||
|
|||||||
+53
-6
@@ -20,10 +20,15 @@ leo 整天上課,一天看兩眼。**不叫,就等於這件事不會發生
|
|||||||
(機器帳號 `claude-code` 在 `bots` 團隊,`/orgs/…/hooks` 回 403,裝不了)。
|
(機器帳號 `claude-code` 在 `bots` 團隊,`/orgs/…/hooks` 回 403,裝不了)。
|
||||||
**但腳本版本身是完整可用的,不是半成品。**
|
**但腳本版本身是完整可用的,不是半成品。**
|
||||||
|
|
||||||
━━ 撈三種東西 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━ 撈四種東西 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
① 逾期的 milestone 期限過了、票還沒關 ⇒ 那個版本交不出來了
|
① 逾期的 milestone 期限過了、票還沒關 ⇒ 那個版本交不出來了
|
||||||
② 掛著等 leo 的票 `Human` 或指派給 leo,**標等了幾天**
|
② 掛著等 leo 的票 `Human` 或指派給 leo,**標等了幾天**
|
||||||
③ 棒子掉在地上的票 `s/doing`/`s/review` 卻很多天沒動 ⇒ 沒有人在做
|
③ 棒子掉在地上的票 `s/doing`/`s/review` 卻很多天沒動 ⇒ 沒有人在做
|
||||||
|
④ 有票卻沒有工人的 repo 名單(`agents/`)上沒人負責、卻有開著的票
|
||||||
|
⇒ 派工一定撞 roster-guard(inkstone/ISEP#118)
|
||||||
|
**只進長版**(開場給總管看),不進 Telegram 短版——補工人是總管的事,不吵 leo。
|
||||||
|
它回答的是「新 repo 的工人誰在什麼時候加」:建 repo 之後的**第一個 session 開場**,
|
||||||
|
由這支點名;在那之前沒有任何東西會提醒。
|
||||||
|
|
||||||
每一條都用白話寫(`CLAUDE.md` 規則五):一眼看懂、代號附一句人話、
|
每一條都用白話寫(`CLAUDE.md` 規則五):一眼看懂、代號附一句人話、
|
||||||
講「這對你意味什麼」而不是系統內部狀態。
|
講「這對你意味什麼」而不是系統內部狀態。
|
||||||
@@ -133,10 +138,12 @@ def collect(fixture=None):
|
|||||||
return {"milestones": [], "issues": [], "error":
|
return {"milestones": [], "issues": [], "error":
|
||||||
"拿不到 Gitea token(GITEA_TOKEN_CLAUDE_CODE 沒設,remote 也沒帶憑證)"}
|
"拿不到 Gitea token(GITEA_TOKEN_CLAUDE_CODE 沒設,remote 也沒帶憑證)"}
|
||||||
|
|
||||||
|
# org 的 repo 清單**一律撈一次**:④「有票卻沒工人」要看整個 org,
|
||||||
|
# 而且這一通本來就要打(沒設 ISEP_NAG_REPOS 時),不多花 API。
|
||||||
|
org_rows = api("/orgs/%s/repos?limit=50" % ORG, tok) or []
|
||||||
repos = os.environ.get("ISEP_NAG_REPOS", "").split()
|
repos = os.environ.get("ISEP_NAG_REPOS", "").split()
|
||||||
if not repos:
|
if not repos:
|
||||||
rows = api("/orgs/%s/repos?limit=50" % ORG, tok) or []
|
repos = [r.get("full_name") for r in org_rows if r.get("full_name")]
|
||||||
repos = [r.get("full_name") for r in rows if r.get("full_name")]
|
|
||||||
|
|
||||||
# 🔴 平行撈,因為這支掛在 SessionStart 上——那是 leo 每次開工的關鍵路徑。
|
# 🔴 平行撈,因為這支掛在 SessionStart 上——那是 leo 每次開工的關鍵路徑。
|
||||||
# 循序版實測 16 秒(16 個 repo + 5 頁票),**開場等 16 秒沒有人會忍受它,
|
# 循序版實測 16 秒(16 個 repo + 5 頁票),**開場等 16 秒沒有人會忍受它,
|
||||||
@@ -175,7 +182,7 @@ def collect(fixture=None):
|
|||||||
issues.extend(rows)
|
issues.extend(rows)
|
||||||
if len(rows) < 50:
|
if len(rows) < 50:
|
||||||
break
|
break
|
||||||
return {"milestones": milestones, "issues": issues}
|
return {"milestones": milestones, "issues": issues, "repos": org_rows}
|
||||||
|
|
||||||
|
|
||||||
# ══ 二、找出「該叫的事」═══════════════════════════════════════════════════
|
# ══ 二、找出「該叫的事」═══════════════════════════════════════════════════
|
||||||
@@ -227,7 +234,25 @@ def findings(data, now):
|
|||||||
waiting.sort(key=lambda x: -x["quiet_days"])
|
waiting.sort(key=lambda x: -x["quiet_days"])
|
||||||
dropped.sort(key=lambda x: -x["quiet_days"])
|
dropped.sort(key=lambda x: -x["quiet_days"])
|
||||||
return {"overdue_milestones": over, "waiting_on_leo": waiting,
|
return {"overdue_milestones": over, "waiting_on_leo": waiting,
|
||||||
"dropped_batons": dropped}
|
"dropped_batons": dropped,
|
||||||
|
"repos_without_worker": no_worker(data)}
|
||||||
|
|
||||||
|
|
||||||
|
def no_worker(data):
|
||||||
|
"""④ 有票卻沒有工人的 repo(inkstone/ISEP#118)。
|
||||||
|
|
||||||
|
回 list(可能是空的);**名單讀不到或 org 清單沒撈到 ⇒ 回 None**,
|
||||||
|
報告裡講「沒查到」,不講成「每個 repo 都缺人」或「都有人」。
|
||||||
|
"""
|
||||||
|
rows = data.get("repos")
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, os.path.join(HERE, "..", "hooks", "lib"))
|
||||||
|
import roster
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return roster.uncovered(rows)
|
||||||
|
|
||||||
|
|
||||||
# ══ 三、講出來(白話)═════════════════════════════════════════════════════
|
# ══ 三、講出來(白話)═════════════════════════════════════════════════════
|
||||||
@@ -260,12 +285,13 @@ def full_report(f, now, err=None):
|
|||||||
" ⇒ **這不等於「沒有事情逾期」**,這是「我沒查到」。兩件事不要講成同一句。"]
|
" ⇒ **這不等於「沒有事情逾期」**,這是「我沒查到」。兩件事不要講成同一句。"]
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
n = sum(len(v) for v in f.values())
|
n = sum(len(f[k]) for k in ("overdue_milestones", "waiting_on_leo", "dropped_batons"))
|
||||||
if n == 0:
|
if n == 0:
|
||||||
out += ["", "✅ **查過了,沒有。** 三類都乾淨:",
|
out += ["", "✅ **查過了,沒有。** 三類都乾淨:",
|
||||||
" · 沒有逾期還開著票的 milestone",
|
" · 沒有逾期還開著票的 milestone",
|
||||||
" · 沒有掛著等你的票",
|
" · 沒有掛著等你的票",
|
||||||
" · 沒有超過 %d 天沒動的 s/doing/s/review" % STALE_DAYS]
|
" · 沒有超過 %d 天沒動的 s/doing/s/review" % STALE_DAYS]
|
||||||
|
out += worker_section(f)
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
if f["overdue_milestones"]:
|
if f["overdue_milestones"]:
|
||||||
@@ -295,9 +321,30 @@ def full_report(f, now, err=None):
|
|||||||
_clip(i["title"], 38)))
|
_clip(i["title"], 38)))
|
||||||
out += _tail(f["dropped_batons"], MAX_ROWS)
|
out += _tail(f["dropped_batons"], MAX_ROWS)
|
||||||
out.append(" ⇒ 對你意味著:這些不是在做,是沒人接。要嘛派人,要嘛改回 s/todo。")
|
out.append(" ⇒ 對你意味著:這些不是在做,是沒人接。要嘛派人,要嘛改回 s/todo。")
|
||||||
|
out += worker_section(f)
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def worker_section(f):
|
||||||
|
"""④ 只進長版(開場注入給總管看的那份),**不進 Telegram 短版**:
|
||||||
|
派工是總管的事,不是 leo 的事——拿它去吵 leo 就是把總管的活推給他。"""
|
||||||
|
rows = f.get("repos_without_worker")
|
||||||
|
if rows is None:
|
||||||
|
return ["", "👷 有票卻沒有工人的 repo:**沒查到**(org 清單或工人名單讀不到)"
|
||||||
|
"——這不等於「每個 repo 都有人」。"]
|
||||||
|
if not rows:
|
||||||
|
return ["", "👷 每個有開著票的 repo,名單上都有工人。"]
|
||||||
|
out = ["", "👷 **有票卻沒有工人的 repo(%d 個)**——派工到這裡會被 roster-guard 擋,"
|
||||||
|
"只剩開例外閘繞過去:" % len(rows)]
|
||||||
|
for r in rows[:MAX_ROWS]:
|
||||||
|
out.append(" · %s(%d 張票開著)" % (r["repo"], r["open"]))
|
||||||
|
out += _tail(rows, MAX_ROWS)
|
||||||
|
out.append(" ⇒ 對總管意味著:**先補工人再派工**——在 ISEP `agents/` 加一個 `.md`"
|
||||||
|
"(規約 docs/governance/worker-roster.md),或把它寫進既有工人的 `負責 repo`。"
|
||||||
|
"不要開 `/tmp/.roster-ok-*` 了事,那樣下一次還是會撞。")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def short_report(f, now, err=None):
|
def short_report(f, now, err=None):
|
||||||
"""Telegram 版:leo 在手機上一眼掃完、回一個詞。**寧短勿長。**"""
|
"""Telegram 版:leo 在手機上一眼掃完、回一個詞。**寧短勿長。**"""
|
||||||
if err:
|
if err:
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ def cmd_show(name):
|
|||||||
|
|
||||||
|
|
||||||
def cmd_which(repo):
|
def cmd_which(repo):
|
||||||
hits = [w for w in R.load() if w["repo"].lower() == (repo or "").strip().lower()]
|
hits = R.which(repo) # 一個工人可以管好幾個 repo(inkstone/ISEP#118)
|
||||||
if not hits:
|
if not hits:
|
||||||
print("🚫 名單上沒有負責「%s」的人。現有的是:" % repo, file=sys.stderr)
|
print("🚫 名單上沒有負責「%s」的人。現有的是:" % repo, file=sys.stderr)
|
||||||
for w in R.load():
|
for w in R.load():
|
||||||
|
|||||||
Reference in New Issue
Block a user