Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 436e30b9d1 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "isep",
|
"name": "isep",
|
||||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 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、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、65 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||||
"version": "0.30.2",
|
"version": "0.30.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"inkstone",
|
"inkstone",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
| | 數量 | 是什麼 |
|
| | 數量 | 是什麼 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `hooks/` | 62 支 + `hooks.json` | 全部機械閘(PreToolUse/Stop/SubagentStop/SessionStart/PostToolUse/UserPromptSubmit 共 87 條註冊)。**一支一行的白話盤點在 `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/` | 7 位 | **工人名單**(`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` |
|
||||||
|
|||||||
+11
-2
@@ -98,12 +98,21 @@ bash scripts/test-comment-carries-task-guard.sh
|
|||||||
> 記過:訊息壞掉時閘照樣 `exit 2`,只看離開碼完全看不出來——
|
> 記過:訊息壞掉時閘照樣 `exit 2`,只看離開碼完全看不出來——
|
||||||
> 而這張票(`inkstone/ISEP#112`)就是那個形狀的極端版:**閘擋對了,但它教的解法根本不存在。**
|
> 而這張票(`inkstone/ISEP#112`)就是那個形狀的極端版:**閘擋對了,但它教的解法根本不存在。**
|
||||||
|
|
||||||
### A12 — 收工要把棒子交回來:10 條
|
### A12 — 收工要把棒子交回來:24 條
|
||||||
```
|
```
|
||||||
bash scripts/test-baton-handback-guard.sh
|
bash scripts/test-baton-handback-guard.sh
|
||||||
```
|
```
|
||||||
**該看到**:`10/10 通過`。**用 fixture 跑,不打網路、不在票池留下測試票**。
|
**該看到**:`24/24 通過`。**用 fixture 跑,不打網路、不在票池留下測試票**
|
||||||
|
(Stop 那半用 `mktemp` 造的假 transcript;出路那兩條把 `TICKET_HOST` 指到連不上的位址)。
|
||||||
**失敗**:
|
**失敗**:
|
||||||
|
- **`inkstone/ISEP#153` 驗收 1**(背景派工送出那一刻)兩條紅 ⇒ 工人剛出發就念「收工了」,
|
||||||
|
會教總管在工人動手前去改票(假交棒)——**誤攔,比漏擋嚴重**
|
||||||
|
- **驗收 2**「完工通知 completed ⇒ 報」紅 ⇒ 背景工人真的交件、票上沒交棒,卻靜靜通過;
|
||||||
|
「只點名那一則通知對到的票」紅 ⇒ 判準從 `tool-use-id` 比對滑成「transcript 裡有通知就全報」
|
||||||
|
- 「同一則通知第二次 Stop」紅 ⇒ 鬼打牆,每個回合都被擋
|
||||||
|
- 「出路印的是絕對路徑」紅 ⇒ 又印回相對的 `scripts/ticket`,總管在 InkStoneCo 貼上去會叫到舊複本(沒有 `handback`)
|
||||||
|
- ★「原樣貼上只填三格」紅 ⇒ 訊息教的那塊指令跑不完;它的對照組(拿掉 `--next`)紅就表示 ★ 沒有鑑別力
|
||||||
|
- 📌 這 14 條拿 `main` 上舊版的 hook 跑是 **7 條紅**(2026-09-13 實跑),不是換了寫法就會綠的測試
|
||||||
- 「該報」5 條任一紅 ⇒ 指派/tag/下一步缺哪一格抓不到,08-26 那次掉棒的狀態會靜靜通過
|
- 「該報」5 條任一紅 ⇒ 指派/tag/下一步缺哪一格抓不到,08-26 那次掉棒的狀態會靜靜通過
|
||||||
- 「不該報」4 條任一紅 ⇒ 每條線收工都被念一次,警報會被學會忽略
|
- 「不該報」4 條任一紅 ⇒ 每條線收工都被念一次,警報會被學會忽略
|
||||||
- 「票已關」那條紅 ⇒ 棒子已經到終點還在催,那是最典型的假警報
|
- 「票已關」那條紅 ⇒ 棒子已經到終點還在催,那是最典型的假警報
|
||||||
|
|||||||
+10
-2
@@ -7,10 +7,16 @@
|
|||||||
|
|
||||||
## 一句話結論
|
## 一句話結論
|
||||||
|
|
||||||
`hooks/` 底下有 **62 個 `.sh` 檔**,`hooks.json` 實際掛上 **87 條註冊**(同一支閘常被多種情境同時掛上);
|
`hooks/` 底下有 **62 個 `.sh` 檔**,`hooks.json` 實際掛上 **88 條註冊**(同一支閘常被多種情境同時掛上);
|
||||||
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
|
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
|
||||||
下面按「你會在什麼時候撞到它」分組,一支一行。
|
下面按「你會在什麼時候撞到它」分組,一支一行。
|
||||||
|
|
||||||
|
> 📌 **`inkstone/ISEP#153`(2026-09-13,待總管定版)+0 支、+1 條**:`baton-handback-guard.sh` 多掛一條 `Stop`。
|
||||||
|
> 背景派工時 `PostToolUse:Agent` 在工人**剛出發**就觸發,舊版當場念「收工了但棒子沒交回來」(09-13 對 ISEP#150、arcrun-rag#14 各一次,工人其實還要跑 17 分鐘)。
|
||||||
|
> 現在送出那一刻看到 launch ack 就不查;工人交件(主對話收到 `<task-notification>` completed)時由 Stop 那半用 `tool-use-id` 對回原派工單再查,同一條線只點名一次。
|
||||||
|
> **62 支、88 條、65 支腳本是在這棵樹上當場數出來的**(`ls hooks/*.sh | wc -l`=62、
|
||||||
|
> `grep -c '"command":' hooks/hooks.json`=88、`ls -p scripts | grep -v / | wc -l`=65)。
|
||||||
|
|
||||||
> 🔴 **這兩個數字上一版是錯的(2026-08-26 實際數過才發現)**:本頁原本寫「43 個檔、53 條註冊」,
|
> 🔴 **這兩個數字上一版是錯的(2026-08-26 實際數過才發現)**:本頁原本寫「43 個檔、53 條註冊」,
|
||||||
> 而當時真實是 **45 個檔、55 條註冊**——中間有兩支閘進來時沒有回頭改這裡。
|
> 而當時真實是 **45 個檔、55 條註冊**——中間有兩支閘進來時沒有回頭改這裡。
|
||||||
> 現在的寫法是實際數出來的:
|
> 現在的寫法是實際數出來的:
|
||||||
@@ -279,6 +285,7 @@
|
|||||||
| `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 唯一一支看「佇列」而不是看「這回合寫了什麼」的收工閘(`inkstone/ISEP#33`:雲端三趟 run 都是做完一件、還有票就停)。做不了的票要把理由寫回票、改欄位,pick 就不再回它。跟 leo 聊天的 session 不會被咬;同一張連續指名 3 次、讀不到佇列 3 次、或一個 session 擋滿 40 次就放行並留痕,不會鎖死。 | 🛑 擋(有上限) |
|
||||||
|
| `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`)。
|
||||||
> 這一對是自造的「待驗單」機制(一張沒有狀態、沒有持有人的 markdown),違反 D58:
|
> 這一對是自造的「待驗單」機制(一張沒有狀態、沒有持有人的 markdown),違反 D58:
|
||||||
@@ -298,7 +305,7 @@
|
|||||||
| `kbdb-asked-stamp.sh` | AI 真的查過 KBDB 之後,留一個時間戳——給前面 `history-first-guard.sh` 判斷「這輪有沒有先查過」用。 | 📝 記錄 |
|
| `kbdb-asked-stamp.sh` | AI 真的查過 KBDB 之後,留一個時間戳——給前面 `history-first-guard.sh` 判斷「這輪有沒有先查過」用。 | 📝 記錄 |
|
||||||
| `subagent-first-stamp.sh` | AI 真的派過工之後,留一個時間戳——給 `subagent-first-guard.sh` 判斷用。 | 📝 記錄 |
|
| `subagent-first-stamp.sh` | AI 真的派過工之後,留一個時間戳——給 `subagent-first-guard.sh` 判斷用。 | 📝 記錄 |
|
||||||
| `issue-status-autoflip.sh` | AI 一派工出去,就自動把對應的 Gitea 票改成「進行中(s/doing)」,不必等人手動改標籤。 | 📝 記錄(自動改票) |
|
| `issue-status-autoflip.sh` | AI 一派工出去,就自動把對應的 Gitea 票改成「進行中(s/doing)」,不必等人手動改標籤。 | 📝 記錄(自動改票) |
|
||||||
| `baton-handback-guard.sh` | 一條派工線做完了,就去看它那張票有沒有**指派給人/標籤有沒有說它卡在哪/有沒有寫下一步**,缺哪一格就當場說出來。你關心的是:不會再有票做完了卻沒人接手,躺在那裡沒人發現。 | 📝 記錄(提醒,不擋) |
|
| `baton-handback-guard.sh` | 一條派工線做完了,就去看它那張票有沒有**指派給人/標籤有沒有說它卡在哪/有沒有寫下一步**,缺哪一格就當場說出來。你關心的是:不會再有票做完了卻沒人接手,躺在那裡沒人發現。**只管前景派工**:背景派工送出的那一刻工人才剛出發,這裡不出聲,等它交件時由 F 組的 Stop 那半查(`inkstone/ISEP#153`)。 | 📝 記錄(提醒,不擋) |
|
||||||
| `kbdb-evidence-stamp.sh` | AI 每次去知識庫拿資料,就記一筆「這是**搜尋**搜到的,還是**走正規檢索路徑**拿到的」。你關心的是:它等一下要拿這筆資料下結論時,有東西可以對照它走的是哪條路。 | 📝 記錄 |
|
| `kbdb-evidence-stamp.sh` | AI 每次去知識庫拿資料,就記一筆「這是**搜尋**搜到的,還是**走正規檢索路徑**拿到的」。你關心的是:它等一下要拿這筆資料下結論時,有東西可以對照它走的是哪條路。 | 📝 記錄 |
|
||||||
| `investigate-first-stamp.sh` | 你真的**派人去查某張票**之後,按「session + 票號」留一個時間戳——給 `diagnosis-evidence-guard.sh` 判斷「這件事有沒有人查過」用。按票分而不是按 session 分:早上派人查了 A 票,不該等於解鎖了 B 票的診斷。 | 📝 記錄 |
|
| `investigate-first-stamp.sh` | 你真的**派人去查某張票**之後,按「session + 票號」留一個時間戳——給 `diagnosis-evidence-guard.sh` 判斷「這件事有沒有人查過」用。按票分而不是按 session 分:早上派人查了 A 票,不該等於解鎖了 B 票的診斷。 | 📝 記錄 |
|
||||||
## I. 你會撞到但跟「派工/收工」無關的一支(Edit·MultiEdit)
|
## I. 你會撞到但跟「派工/收工」無關的一支(Edit·MultiEdit)
|
||||||
@@ -420,6 +427,7 @@ A1 缺人話、A2 表格有幽靈、C 全頁有幽靈。
|
|||||||
| `micromanage-guard.sh` / `irreversible-dispatch-guard.sh` / `no-ticket-no-dispatch.sh` | 各 2 次 | 各自同時掛在 `Task` 與 `Agent` 兩個矩比對名稱上——這兩個名稱應該是同一種派工動作的新舊叫法,兩個都掛保證不漏接 |
|
| `micromanage-guard.sh` / `irreversible-dispatch-guard.sh` / `no-ticket-no-dispatch.sh` | 各 2 次 | 各自同時掛在 `Task` 與 `Agent` 兩個矩比對名稱上——這兩個名稱應該是同一種派工動作的新舊叫法,兩個都掛保證不漏接 |
|
||||||
| `worklist-guard.sh` / `self-drive-police.sh` / `self-drive-judge.sh` / `delivery-police.sh` / `wiki-first-police.sh` / `unpushed-police.sh` | 各 2 次 | 一次守「總管自己想收工」(Stop),一次守「subagent 交回工作」(SubagentStop)——同一套判準用在兩種角色身上 |
|
| `worklist-guard.sh` / `self-drive-police.sh` / `self-drive-judge.sh` / `delivery-police.sh` / `wiki-first-police.sh` / `unpushed-police.sh` | 各 2 次 | 一次守「總管自己想收工」(Stop),一次守「subagent 交回工作」(SubagentStop)——同一套判準用在兩種角色身上 |
|
||||||
| `search-is-not-proof-guard.sh` | 3 次 | 拿搜尋結果當證據的那個動作,可能是「貼進票裡」(Bash),也可能是「寫進派工單發包出去」(`Task`/`Agent` 兩個名稱各掛一次),三種都要接住 |
|
| `search-is-not-proof-guard.sh` | 3 次 | 拿搜尋結果當證據的那個動作,可能是「貼進票裡」(Bash),也可能是「寫進派工單發包出去」(`Task`/`Agent` 兩個名稱各掛一次),三種都要接住 |
|
||||||
|
| `baton-handback-guard.sh` | 2 次 | 同一件事的兩種收工時機:前景派工 tool 回來就是交件(PostToolUse),背景派工要等完工通知進主對話(Stop) |
|
||||||
|
|
||||||
❓ **這裡有一件我看不出來是刻意還是遺留、需要人判斷**:`micromanage-guard.sh`/`irreversible-dispatch-guard.sh`/`no-ticket-no-dispatch.sh`
|
❓ **這裡有一件我看不出來是刻意還是遺留、需要人判斷**:`micromanage-guard.sh`/`irreversible-dispatch-guard.sh`/`no-ticket-no-dispatch.sh`
|
||||||
三支都**同時**掛在 `Task` 跟 `Agent` 這兩個矩比對名稱上。如果這兩個名稱在目前版本的 Claude Code 裡指的是「同一種派工工具呼叫」,
|
三支都**同時**掛在 `Task` 跟 `Agent` 這兩個矩比對名稱上。如果這兩個名稱在目前版本的 Claude Code 裡指的是「同一種派工工具呼叫」,
|
||||||
|
|||||||
+174
-65
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 管什麼: 一條派工線收工時,檢查它的工單票有沒有把棒子交回來——指派(誰該動)+ tag(卡在哪)+ 下一步(做什麼)三格。
|
# 管什麼: 一條派工線收工時,檢查它的工單票有沒有把棒子交回來——指派(誰該動)+ tag(卡在哪)+ 下一步(做什麼)三格。
|
||||||
# 為什麼: 2026-08-26 那次交回寫在 comment 裡,票沒指派、tag 沒動 ⇒ 沒有任何欄位在說「這張票在等你」,棒子躺了 14 小時。撈一次看得到的只有欄位,不是留言。
|
# 為什麼: 2026-08-26 那次交回寫在 comment 裡,票沒指派、tag 沒動 ⇒ 沒有任何欄位在說「這張票在等你」,棒子躺了 14 小時。撈一次看得到的只有欄位,不是留言。
|
||||||
# 誤觸時怎麼關: 這條線不對應任何票 → 派工單本來就不該少了【工單】那行(no-ticket-no-dispatch.sh 管那個);真要放行在指令裡留 `baton-ok: <理由>`(本閘只提醒不阻擋,不會擋住任何動作)。
|
# 誤觸時怎麼關: 這條線不對應任何票 → 派工單本來就不該少了【工單】那行(no-ticket-no-dispatch.sh 管那個);背景工人交件後同一條線只點名一次,補完三格就不再出現。
|
||||||
#
|
#
|
||||||
# baton-handback-guard.sh — 收工=把棒子交回去(PostToolUse: Agent|Task)
|
# baton-handback-guard.sh — 收工=把棒子交回去
|
||||||
|
# 同一件事的兩種收工時機,掛兩個事件:
|
||||||
|
# PostToolUse(Agent|Task) 前景派工:tool 回來=工人交件了 ⇒ 當場查
|
||||||
|
# Stop 背景派工:主對話收到 <task-notification> completed ⇒ 回合結束時查
|
||||||
#
|
#
|
||||||
# ── leo 2026-08-27 的原話(本閘的規格)────────────────────────────────
|
# ── leo 2026-08-27 的原話(本閘的規格)────────────────────────────────
|
||||||
# 「它把事情做完後如果要檢查後續,就要把任務指定給你並改 tag,
|
# 「它把事情做完後如果要檢查後續,就要把任務指定給你並改 tag,
|
||||||
@@ -15,73 +18,165 @@
|
|||||||
# tag s/* 它卡在哪一段? 缺了它 → 撈得到票,但要重讀整串
|
# tag s/* 它卡在哪一段? 缺了它 → 撈得到票,但要重讀整串
|
||||||
# 下一步(留言) 收下的人要做什麼? 缺了它 → 撿起棒子的人得自己重建脈絡
|
# 下一步(留言) 收下的人要做什麼? 缺了它 → 撿起棒子的人得自己重建脈絡
|
||||||
#
|
#
|
||||||
# ── 為什麼掛在 PostToolUse(Agent) 而不是 SubagentStop ──────────────
|
# ── 背景派工為什麼改到 Stop 查(inkstone/ISEP#153,09-13 兩次實撞)──────────
|
||||||
# 工單票號寫在**派工單**裡(`【工單】owner/repo#N`),而派工單就是 Agent 的 tool_input。
|
# PostToolUse:Agent 對背景派工是**送出後幾秒**就觸發(parallel-lines-cap-guard.sh 檔頭有實測時間線),
|
||||||
# SubagentStop 拿到的 transcript 是主對話,票號要繞一圈才找得回來
|
# 那時 tool_response 是 launch ack:{"isAsync": true, "status": "async_launched", ...}
|
||||||
# (見 subagent-claim-worksheet.sh 檔頭那段實撞紀錄)。
|
# (同一份物件寫在 transcript 的 toolUseResult,2026-08-24 實跡)。
|
||||||
# 同一個位置 issue-status-autoflip.sh 已經在用,形狀是驗過的。
|
# 舊版在這一刻就查票 ⇒ 對 ISEP#150、arcrun-rag#14 各念一次「收工了但棒子沒交回來」,
|
||||||
|
# 而兩個工人之後各跑了 17 分鐘才交件。這會教總管在工人動手前去改票(假交棒),或學會忽略本閘。
|
||||||
#
|
#
|
||||||
# ── 只提醒,不阻擋 ────────────────────────────────────────────────
|
# 真正的交件是主 transcript 裡的 <task-notification>(<status>completed</status>),
|
||||||
# 工作已經做完了,擋在這裡沒有任何東西可以被挽救;能做的是**讓總管在當下就看到缺哪一格**。
|
# 它帶著 <tool-use-id> ——**拿這個唯一識別碼回頭對上當初那一則 Agent tool_use**,
|
||||||
# 同 unpushed-police.sh 的定位:exit 2 讓訊息進到模型眼前,不是 blocker。
|
# 從它的 prompt 取【工單】。判準是結構比對(哪一則派工),不是讀文字猜。
|
||||||
|
# 不掛 SubagentStop:parallel-lines-cap-guard.sh 檔頭實測過它在主 session 一次都沒觸發。
|
||||||
|
#
|
||||||
|
# ── 只點名一次 ────────────────────────────────────────────────────
|
||||||
|
# Stop 回 exit 2 會讓總管多走一個回合(去補三格)。查過的 tool-use-id 記在狀態檔,之後不再查
|
||||||
|
# ⇒ 補完了自然安靜,沒補也不會鬼打牆。狀態檔在 ${ISEP_BATON_STATE_DIR:-${TMPDIR:-/tmp}}。
|
||||||
|
# 前景 PostToolUse 同 unpushed-police.sh 的定位:exit 2 讓訊息進到模型眼前,不擋任何動作。
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
INPUT=$(cat)
|
INPUT=$(cat)
|
||||||
|
# 訊息裡的出路一律印絕對路徑:總管的 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是舊複本,
|
||||||
|
# `grep -c handback` → 0(2026-09-13 實查)⇒ 印相對路徑=教一條走不通的路(同 comment-carries-task-guard ⑮)。
|
||||||
|
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
|
TICKET="$PLUGIN_ROOT/scripts/ticket"
|
||||||
|
STATE_DIR="${ISEP_BATON_STATE_DIR:-${TMPDIR:-/tmp}}"
|
||||||
|
export ISEP_BATON_STATE_DIR="${STATE_DIR%/}"
|
||||||
|
|
||||||
|
# ── 找出這一次要查的票(每行一張:owner repo num)──────────────────────
|
||||||
|
SPECS=$(printf '%s' "$INPUT" | python3 -c '
|
||||||
|
import sys, json, re, os
|
||||||
|
|
||||||
|
TICKET = re.compile(r"([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#(\d+)")
|
||||||
|
|
||||||
|
def spec_of(prompt):
|
||||||
|
for line in (prompt or "").splitlines():
|
||||||
|
if "【工單】" not in line:
|
||||||
|
continue
|
||||||
|
m = TICKET.search(line)
|
||||||
|
if m:
|
||||||
|
return "%s %s %s" % m.groups()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_async_ack(resp):
|
||||||
|
# launch ack 兩種長相都認:結構化物件(isAsync/status),或只剩文字的 content 陣列
|
||||||
|
if isinstance(resp, dict) and (resp.get("isAsync") is True or resp.get("status") == "async_launched"):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
return "Async agent launched" in json.dumps(resp, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
SPEC=$(printf '%s' "$INPUT" | python3 -c '
|
|
||||||
import sys, json, re
|
|
||||||
try:
|
try:
|
||||||
d = json.load(sys.stdin)
|
d = json.load(sys.stdin)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
p = (d.get("tool_input") or {}).get("prompt") or ""
|
|
||||||
for line in p.splitlines():
|
if d.get("hook_event_name") != "Stop":
|
||||||
if "【工單】" not in line:
|
if is_async_ack(d.get("tool_response")):
|
||||||
continue
|
raise SystemExit # 工人才剛出發,還沒有「收工」可言;交件時由 Stop 那半查
|
||||||
m = re.search(r"([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#(\d+)", line)
|
s = spec_of((d.get("tool_input") or {}).get("prompt"))
|
||||||
if m:
|
if s:
|
||||||
print(f"{m.group(1)} {m.group(2)} {m.group(3)}")
|
print(s)
|
||||||
break
|
raise SystemExit
|
||||||
|
|
||||||
|
# ── Stop:有沒有背景工人交件 ─────────────────────────────────────────
|
||||||
|
tp = d.get("transcript_path") or ""
|
||||||
|
if not tp or not os.path.isfile(tp):
|
||||||
|
raise SystemExit
|
||||||
|
sid = re.sub(r"[^A-Za-z0-9_.-]", "_", d.get("session_id") or "nosid")[:64]
|
||||||
|
state = os.path.join(os.environ["ISEP_BATON_STATE_DIR"], "baton-handback-seen-%s" % sid)
|
||||||
|
try:
|
||||||
|
seen = set(open(state, encoding="utf-8").read().split())
|
||||||
|
except OSError:
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
NOTE = re.compile(r"<tool-use-id>(toolu_[A-Za-z0-9_-]+)</tool-use-id>.*?<status>([a-z_]+)</status>", re.S)
|
||||||
|
completed = [] # 保留出現順序
|
||||||
|
dispatch = {} # tool_use id → 帶【工單】的派工單
|
||||||
|
try:
|
||||||
|
with open(tp, encoding="utf-8", errors="replace") as f:
|
||||||
|
for line in f:
|
||||||
|
if "<task-notification>" in line:
|
||||||
|
# 同一則通知會以 enqueue/attachment/remove 出現多次,用 id 去重
|
||||||
|
for tuid, st in NOTE.findall(line.replace("\\n", "\n")):
|
||||||
|
if st == "completed" and tuid not in completed:
|
||||||
|
completed.append(tuid)
|
||||||
|
if "【工單】" in line and "tool_use" in line:
|
||||||
|
try:
|
||||||
|
e = json.loads(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
msg = e.get("message")
|
||||||
|
if not isinstance(msg, dict) or msg.get("role") != "assistant":
|
||||||
|
continue
|
||||||
|
for b in msg.get("content") or []:
|
||||||
|
if (isinstance(b, dict) and b.get("type") == "tool_use"
|
||||||
|
and b.get("name") in ("Agent", "Task")):
|
||||||
|
p = (b.get("input") or {}).get("prompt")
|
||||||
|
if isinstance(p, str) and "【工單】" in p:
|
||||||
|
dispatch[b.get("id")] = p
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
todo = [t for t in completed if t in dispatch and t not in seen]
|
||||||
|
if not todo:
|
||||||
|
raise SystemExit
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(state), exist_ok=True)
|
||||||
|
with open(state, "a", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(todo) + "\n")
|
||||||
|
except OSError:
|
||||||
|
pass # 記不下來頂多再念一次,不擋
|
||||||
|
out = []
|
||||||
|
for t in todo:
|
||||||
|
s = spec_of(dispatch[t])
|
||||||
|
if s and s not in out:
|
||||||
|
out.append(s)
|
||||||
|
print("\n".join(out))
|
||||||
' 2>/dev/null || true)
|
' 2>/dev/null || true)
|
||||||
|
|
||||||
[ -n "${SPEC:-}" ] || exit 0
|
[ -n "${SPECS:-}" ] || exit 0
|
||||||
# shellcheck disable=SC2086
|
|
||||||
set -- $SPEC
|
|
||||||
OWNER="$1"; REPO="$2"; NUM="$3"
|
|
||||||
|
|
||||||
# ── 測試縫(唯一目的:讓判斷邏輯測得起來,不必每跑一次測試就在票池留一張票)──
|
# ── 查一張票:缺哪一格就寫到 stderr 並回 2,齊了回 0 ─────────────────────
|
||||||
# BATON_GUARD_FIXTURE=<目錄> ⇒ 讀 issue.json / comments.json,不打網路。
|
check_one(){ # $1 owner $2 repo $3 num
|
||||||
# 正式執行時這個變數不存在,一行都不會走到。
|
local OWNER="$1" REPO="$2" NUM="$3" J C REPORT BODY NOW_A NOW_L
|
||||||
if [ -n "${BATON_GUARD_FIXTURE:-}" ]; then
|
# ── 測試縫(唯一目的:讓判斷邏輯測得起來,不必每跑一次測試就在票池留一張票)──
|
||||||
J=$(cat "$BATON_GUARD_FIXTURE/issue.json")
|
# BATON_GUARD_FIXTURE=<目錄> ⇒ 讀 issue.json / comments.json,不打網路。
|
||||||
C=$(cat "$BATON_GUARD_FIXTURE/comments.json")
|
# 正式執行時這個變數不存在,一行都不會走到。
|
||||||
else
|
if [ -n "${BATON_GUARD_FIXTURE:-}" ]; then
|
||||||
# token:掃已知的 checkout 找第一個帶憑證、指向本站的 remote(同 scripts/ticket 的做法)
|
J=$(cat "$BATON_GUARD_FIXTURE/issue.json")
|
||||||
ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
C=$(cat "$BATON_GUARD_FIXTURE/comments.json")
|
||||||
URL=""
|
else
|
||||||
for d in "$ROOT" "$ROOT/products/$REPO" "$ROOT/matrix/$REPO" "$ROOT/polaris/$REPO"; do
|
# token:掃已知的 checkout 找第一個帶憑證、指向本站的 remote(同 scripts/ticket 的做法)
|
||||||
[ -e "$d/.git" ] || continue
|
local ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" URL="" U dir TOKEN
|
||||||
U=$(git -C "$d" remote -v 2>/dev/null | grep -m1 'git\.uncle6\.me' | grep '@' | awk '{print $2}') || true
|
for dir in "$ROOT" "$ROOT/products/$REPO" "$ROOT/matrix/$REPO" "$ROOT/polaris/$REPO"; do
|
||||||
[ -n "${U:-}" ] && { URL="$U"; break; }
|
[ -e "$dir/.git" ] || continue
|
||||||
done
|
U=$(git -C "$dir" remote -v 2>/dev/null | grep -m1 'git\.uncle6\.me' | grep '@' | awk '{print $2}') || true
|
||||||
[ -n "$URL" ] || exit 0
|
[ -n "${U:-}" ] && { URL="$U"; break; }
|
||||||
TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|')
|
done
|
||||||
[ "$TOKEN" != "$URL" ] || exit 0
|
[ -n "$URL" ] || return 0
|
||||||
|
TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|')
|
||||||
|
[ "$TOKEN" != "$URL" ] || return 0
|
||||||
|
J=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \
|
||||||
|
"https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM" 2>/dev/null) || return 0
|
||||||
|
C=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \
|
||||||
|
"https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM/comments?limit=100" 2>/dev/null) || C='[]'
|
||||||
|
fi
|
||||||
|
|
||||||
J=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \
|
REPORT=$(python3 - "$J" "$C" <<'PY' 2>/dev/null
|
||||||
"https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM" 2>/dev/null) || exit 0
|
|
||||||
C=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \
|
|
||||||
"https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM/comments?limit=100" 2>/dev/null) || C='[]'
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPORT=$(python3 - "$J" "$C" <<'PY' 2>/dev/null
|
|
||||||
import json, sys
|
import json, sys
|
||||||
try:
|
try:
|
||||||
it = json.loads(sys.argv[1]); cs = json.loads(sys.argv[2])
|
it = json.loads(sys.argv[1]); cs = json.loads(sys.argv[2])
|
||||||
except Exception:
|
except Exception:
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
|
if not isinstance(it, dict) or "state" not in it:
|
||||||
|
raise SystemExit # 讀不到票(404/錯誤回應)⇒ 不瞎報
|
||||||
if it.get("state") == "closed":
|
if it.get("state") == "closed":
|
||||||
raise SystemExit # 已經關了 = 棒子到終點了,沒有下一手
|
raise SystemExit # 已經關了 = 棒子到終點了,沒有下一手
|
||||||
|
if not isinstance(cs, list):
|
||||||
|
cs = []
|
||||||
miss = []
|
miss = []
|
||||||
if not (it.get("assignees") or []):
|
if not (it.get("assignees") or []):
|
||||||
miss.append("指派:沒有人被指到這張票 ⇒ 撈 assignee 撈不到它,等於棒子躺在地上")
|
miss.append("指派:沒有人被指到這張票 ⇒ 撈 assignee 撈不到它,等於棒子躺在地上")
|
||||||
@@ -100,37 +195,51 @@ print("\n".join(" ❌ " + m for m in miss))
|
|||||||
print("__ASSIGNEES__" + ",".join(a["login"] for a in (it.get("assignees") or [])))
|
print("__ASSIGNEES__" + ",".join(a["login"] for a in (it.get("assignees") or [])))
|
||||||
print("__LABELS__" + ",".join(labs))
|
print("__LABELS__" + ",".join(labs))
|
||||||
PY
|
PY
|
||||||
) || exit 0
|
) || return 0
|
||||||
|
[ -n "${REPORT:-}" ] || return 0
|
||||||
|
|
||||||
[ -n "${REPORT:-}" ] || exit 0
|
BODY=$(printf '%s' "$REPORT" | grep -v '^__')
|
||||||
|
NOW_A=$(printf '%s' "$REPORT" | sed -n 's/^__ASSIGNEES__//p')
|
||||||
|
NOW_L=$(printf '%s' "$REPORT" | sed -n 's/^__LABELS__//p')
|
||||||
|
|
||||||
BODY=$(printf '%s' "$REPORT" | grep -v '^__')
|
cat >&2 <<EOF
|
||||||
NOW_A=$(printf '%s' "$REPORT" | sed -n 's/^__ASSIGNEES__//p')
|
|
||||||
NOW_L=$(printf '%s' "$REPORT" | sed -n 's/^__LABELS__//p')
|
|
||||||
|
|
||||||
cat >&2 <<EOF
|
|
||||||
🏃 交棒警察:$OWNER/$REPO#$NUM 這條線收工了,但**棒子沒有交回來**。
|
🏃 交棒警察:$OWNER/$REPO#$NUM 這條線收工了,但**棒子沒有交回來**。
|
||||||
|
|
||||||
現在票上:指派=[${NOW_A:-(空)}] 標籤=[${NOW_L:-(空)}]
|
現在票上:指派=[${NOW_A:-(空)}] 標籤=[${NOW_L:-(空)}]
|
||||||
|
|
||||||
$BODY
|
$BODY
|
||||||
|
|
||||||
【leo 2026-08-27】「**每個任務結束必須要指派回總管**,要說明下一步怎麼做,
|
|
||||||
直到最後交出正確 deliverable 並且驗證」
|
|
||||||
|
|
||||||
🔴 **關鍵在「指派」是欄位,不是文字。** 撈一次就看得到,不必讀 comment——
|
|
||||||
leo 自己就是用 Gitea 原生的「指派給您的」在看。
|
|
||||||
|
|
||||||
【一個動作把三格補齊】
|
【一個動作把三格補齊】
|
||||||
scripts/ticket handback $OWNER/$REPO#$NUM \\
|
"$TICKET" handback $OWNER/$REPO#$NUM \\
|
||||||
--to claude-code \\
|
--to claude-code \\
|
||||||
--next "<收下的人第一件事要做什麼,一句話>" \\
|
--next "<收下的人第一件事要做什麼,一句話>" \\
|
||||||
--label <s/review|s/stage|s/pending|s/todo> \\
|
--label <s/review|s/stage|s/pending|s/todo> \\
|
||||||
--evidence "<實測輸出或 PR 連結>"
|
--evidence "<實測輸出或 PR 連結>"
|
||||||
|
|
||||||
要 leo 親手做的 → --to Leo(會自動加 Human 標籤)
|
要 leo 親手做的 → --to Leo(會自動加 Human 標籤)
|
||||||
已經真的做完且驗過了 → scripts/ticket close $OWNER/$REPO#$NUM --deliverable <URL>
|
已經真的做完且驗過了 → "$TICKET" close $OWNER/$REPO#$NUM --deliverable <URL>
|
||||||
|
|
||||||
【撈一次看棒子在誰手上】 scripts/ticket mine
|
EOF
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
HIT=0
|
||||||
|
while read -r OWNER REPO NUM; do
|
||||||
|
[ -n "${NUM:-}" ] || continue
|
||||||
|
check_one "$OWNER" "$REPO" "$NUM" </dev/null
|
||||||
|
[ $? -eq 2 ] && HIT=1
|
||||||
|
done <<< "$SPECS"
|
||||||
|
|
||||||
|
[ "$HIT" = 1 ] || exit 0
|
||||||
|
|
||||||
|
cat >&2 <<EOF
|
||||||
|
【leo 2026-08-27】「**每個任務結束必須要指派回總管**,要說明下一步怎麼做,
|
||||||
|
直到最後交出正確 deliverable 並且驗證」
|
||||||
|
|
||||||
|
🔴 **關鍵在「指派」是欄位,不是文字。** 撈一次就看得到,不必讀 comment——
|
||||||
|
leo 自己就是用 Gitea 原生的「指派給您的」在看。
|
||||||
|
|
||||||
|
【撈一次看棒子在誰手上】 "$TICKET" mine
|
||||||
|
(背景工人交件後,同一條線只點名這一次;補完三格就不會再出現。)
|
||||||
EOF
|
EOF
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
@@ -377,6 +377,15 @@
|
|||||||
"timeout": 180
|
"timeout": 180
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/baton-handback-guard.sh",
|
||||||
|
"timeout": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubagentStop": [
|
"SubagentStop": [
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 交棒警察的測試——三格(指派/tag/下一步)各自缺漏時要被抓到
|
# 交棒警察的測試——三格(指派/tag/下一步)各自缺漏時要被抓到
|
||||||
# 判準:5 種該報、5 種不該報
|
# 判準:前景 5 種該報、5 種不該報;背景派工(inkstone/ISEP#153)送出時不報、交件時才報、只報一次;
|
||||||
|
# 訊息印的出路照著貼真的跑得完。
|
||||||
# 用 fixture 跑,不打網路、不在票池留下任何一張測試票。
|
# 用 fixture 跑,不打網路、不在票池留下任何一張測試票。
|
||||||
cd "$(dirname "$0")/.." || exit 1
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
ROOT=$(pwd)
|
||||||
H=hooks/baton-handback-guard.sh
|
H=hooks/baton-handback-guard.sh
|
||||||
PASS=0; FAIL=0
|
PASS=0; FAIL=0
|
||||||
FX=$(mktemp -d); trap 'rm -rf "$FX"' EXIT
|
FX=$(mktemp -d); ST=$(mktemp -d); trap 'rm -rf "$FX" "$ST"' EXIT
|
||||||
|
export CLAUDE_PLUGIN_ROOT="$ROOT" ISEP_BATON_STATE_DIR="$ST"
|
||||||
|
|
||||||
mkfx(){ # $1=state $2=assignees(csv) $3=labels(csv) $4=有沒有交棒留言(y/n)
|
mkfx(){ # $1=state $2=assignees(csv) $3=labels(csv) $4=有沒有交棒留言(y/n)
|
||||||
python3 - "$FX" "$1" "$2" "$3" "$4" <<'PY'
|
python3 - "$FX" "$1" "$2" "$3" "$4" <<'PY'
|
||||||
@@ -22,12 +25,15 @@ json.dump(comments, open(os.path.join(d, "comments.json"), "w"))
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
judge(){ # $1=want $2=got $3=說明
|
||||||
|
if [ "$2" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||||
|
printf 'want=%s got=%s %s\n' "$1" "$2" "$3"
|
||||||
|
}
|
||||||
|
|
||||||
run(){ # $1=want $2=說明 (fixture 已備好)
|
run(){ # $1=want $2=說明 (fixture 已備好)
|
||||||
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\n做點事"}}' \
|
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\n做點事"}}' \
|
||||||
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
||||||
got=$?
|
judge "$1" "$?" "$2"
|
||||||
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
|
||||||
printf 'want=%s got=%s %s\n' "$1" "$got" "$2"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "── 該報:三格有缺 ──"
|
echo "── 該報:三格有缺 ──"
|
||||||
@@ -46,9 +52,98 @@ mkfx closed "" "" n ; run 0 "票已關=棒子到
|
|||||||
echo "── 派工單裡沒有【工單】那行 ⇒ 不歸這支管 ──"
|
echo "── 派工單裡沒有【工單】那行 ⇒ 不歸這支管 ──"
|
||||||
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"隨便做點事"}}' \
|
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"隨便做點事"}}' \
|
||||||
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
||||||
got=$?
|
judge 0 "$?" "沒有票號的派工單"
|
||||||
if [ "$got" = 0 ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
|
||||||
printf 'want=0 got=%s 沒有票號的派工單\n' "$got"
|
# ═══ inkstone/ISEP#153:背景派工 ═══════════════════════════════════════
|
||||||
|
post(){ # $1=tool_response JSON → 離開碼
|
||||||
|
printf '{"hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\\n做點事","run_in_background":true},"tool_response":%s}' "$1" \
|
||||||
|
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "── 驗收 1:背景派工送出的那一刻不報(票上三格全缺也一樣)──"
|
||||||
|
mkfx open "" "s/doing" n
|
||||||
|
post '{"isAsync":true,"status":"async_launched","agentId":"a93f7dc98c50fc671","description":"x"}'
|
||||||
|
judge 0 "$?" "launch ack(toolUseResult 真跡的形狀:isAsync/async_launched)"
|
||||||
|
post '[{"type":"text","text":"Async agent launched successfully. (This tool result is internal metadata)\nagentId: a1"}]'
|
||||||
|
judge 0 "$?" "launch ack 只剩文字 content 陣列時也認得"
|
||||||
|
post '{"status":"completed","content":[{"type":"text","text":"工作做完了"}]}'
|
||||||
|
judge 2 "$?" "驗收 3 對照:前景派工回來(不是 launch ack)照舊要報"
|
||||||
|
|
||||||
|
# 假 transcript:派工 tool_use + launch ack +(可選)完工通知
|
||||||
|
TP="$ST/main.jsonl"
|
||||||
|
mktx(){ # $1=通知對到哪個 tool-use-id(空=還沒交件) $2=通知 status $3=派工單第一行
|
||||||
|
python3 - "$TP" "$1" "$2" "$3" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
tp, note_id, status, first = sys.argv[1:5]
|
||||||
|
L = []
|
||||||
|
def add(o): L.append(json.dumps(o, ensure_ascii=False))
|
||||||
|
for tid, line in (("toolu_AAA", first), ("toolu_BBB", "【工單】inkstone/ISEP#999")):
|
||||||
|
add({"type": "assistant", "message": {"role": "assistant", "content": [
|
||||||
|
{"type": "tool_use", "id": tid, "name": "Agent",
|
||||||
|
"input": {"prompt": line + "\n做點事", "run_in_background": True}}]}})
|
||||||
|
add({"type": "user", "message": {"role": "user", "content": [
|
||||||
|
{"type": "tool_result", "tool_use_id": tid,
|
||||||
|
"content": [{"type": "text", "text": "Async agent launched successfully."}]}]},
|
||||||
|
"toolUseResult": {"isAsync": True, "status": "async_launched"}})
|
||||||
|
if note_id:
|
||||||
|
n = ("<task-notification>\n<task-id>a1</task-id>\n<tool-use-id>%s</tool-use-id>\n"
|
||||||
|
"<status>%s</status>\n<summary>Agent finished</summary>\n<result>好了</result>\n"
|
||||||
|
"</task-notification>") % (note_id, status)
|
||||||
|
# 真跡裡同一則通知出現三次:enqueue/attachment/remove
|
||||||
|
add({"type": "queue-operation", "operation": "enqueue", "content": n})
|
||||||
|
add({"type": "attachment", "attachment": {"type": "queued_command", "prompt": n}})
|
||||||
|
add({"type": "queue-operation", "operation": "remove", "content": n})
|
||||||
|
open(tp, "w").write("\n".join(L) + "\n")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
stop(){ # $1=session id → 離開碼;stderr 存到 $ST/err
|
||||||
|
printf '{"hook_event_name":"Stop","session_id":"%s","transcript_path":"%s","stop_hook_active":false}' "$1" "$TP" \
|
||||||
|
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>"$ST/err"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "── 驗收 2:背景工人真的交件時才報 ──"
|
||||||
|
mkfx open "" "s/doing" n
|
||||||
|
mktx "" "" "【工單】inkstone/arcrun-rag#136"
|
||||||
|
stop s1; judge 0 "$?" "工人還在跑(沒有完工通知)⇒ 不報"
|
||||||
|
mktx toolu_AAA completed "【工單】inkstone/arcrun-rag#136"
|
||||||
|
stop s1; got=$?; judge 2 "$got" "完工通知 completed + 票上三格全缺 ⇒ 報"
|
||||||
|
if grep -q 'inkstone/arcrun-rag#136' "$ST/err" && ! grep -q 'ISEP#999' "$ST/err"; then got=ok; else got=bad; fi
|
||||||
|
judge ok "$got" "只點名那一則通知對到的票(tool-use-id 比對;另一條還在跑的 #999 不點)"
|
||||||
|
cp "$ST/err" "$ST/msg"
|
||||||
|
stop s1; judge 0 "$?" "同一則通知第二次 Stop ⇒ 不鬼打牆(只點名一次)"
|
||||||
|
mktx toolu_AAA killed "【工單】inkstone/arcrun-rag#136"
|
||||||
|
stop s2; judge 0 "$?" "通知不是 completed(killed)⇒ 不當成交件"
|
||||||
|
mkfx open "claude-code" "s/review" y
|
||||||
|
mktx toolu_AAA completed "【工單】inkstone/arcrun-rag#136"
|
||||||
|
stop s3; judge 0 "$?" "交件時三格已經補齊 ⇒ 安靜(出路:補完就不再出現)"
|
||||||
|
mkfx open "" "s/doing" n
|
||||||
|
mktx toolu_AAA completed "隨便做點事,沒有工單"
|
||||||
|
stop s4; judge 0 "$?" "交件的那條派工單沒有【工單】⇒ 不歸這支管"
|
||||||
|
printf '{"hook_event_name":"Stop","session_id":"s5","transcript_path":"/nonexistent"}' \
|
||||||
|
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
|
||||||
|
judge 0 "$?" "讀不到 transcript ⇒ 放行"
|
||||||
|
|
||||||
|
echo "── 出路:訊息印的那塊指令照著貼真的跑得完 ──"
|
||||||
|
BLK=$(python3 - "$ST/msg" <<'PY'
|
||||||
|
import sys, re
|
||||||
|
t = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
m = re.search(r'^\s*("[^"\n]*/scripts/ticket" handback .*?--evidence "[^"\n]*")', t, re.S | re.M)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit
|
||||||
|
b = m.group(1)
|
||||||
|
b = b.replace("<收下的人第一件事要做什麼,一句話>", "去複驗")
|
||||||
|
b = re.sub(r"<s/review|[^>]*>", "s/review", b)
|
||||||
|
b = b.replace("<實測輸出或 PR 連結>", "測試")
|
||||||
|
print(b)
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
case "$BLK" in /*|\"/*) got=abs ;; *) got="${BLK:0:30}" ;; esac
|
||||||
|
judge abs "$got" "出路印的是絕對路徑(InkStoneCo/scripts/ticket 是舊複本,沒有 handback)"
|
||||||
|
OFF="TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN_CLAUDE_CODE=fake ISEP_API_RETRIES=0"
|
||||||
|
env $OFF bash -c "$BLK" >/dev/null 2>&1
|
||||||
|
judge 1 "$?" "★ 原樣貼上只填三格 ⇒ 參數全過、走到網路才停(離開碼 1;2=被自己的用法擋下)"
|
||||||
|
env $OFF bash -c "${BLK/--next/--nope}" >/dev/null 2>&1
|
||||||
|
judge 2 "$?" "對照組:拿掉 --next ⇒ 確實跑不完(上一條才有鑑別力)"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "$PASS/$((PASS+FAIL)) 通過"
|
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||||
|
|||||||
Reference in New Issue
Block a user