ship-check 送到得了任何 session:內容原樣搬過來,歸屬寫下來讓機器去比 #127

Merged
claude-code merged 4 commits from fix/isep-122-ship-check-delivery into main 2026-09-02 04:02:24 +00:00
7 changed files with 363 additions and 20 deletions
Showing only changes of commit 044ef289fe - Show all commits
+9 -2
View File
@@ -25,8 +25,8 @@
|---|---|---|
| `hooks/` | 61 支 `hooks.json` | 全部機械閘(PreToolUseStopSubagentStopSessionStartPostToolUseUserPromptSubmit 共 84 條註冊)。**一支一行的白話盤點在 `docs/hooks-inventory.md`,那裡才是這兩個數字的家** |
| `agents/` | 7 位 | **工人名單**`inkstone/ISEP#86`)——派工時指名派給誰,規約見 `docs/governance/worker-roster.md` |
| `commands/` | 7 支 | `/wiki-recall` `/ship-check` `/cp-write` … |
| `skills/` | 2 支 | |
| `commands/` | 7 支 | `/wiki-recall` `/wiki-capture` `/cp-write` `/issue-handle` … |
| `skills/` | 2 支 | `ship-check`(東西要出去之前)/`deep-recall`(把散落的枝葉還原成一棵樹)。🔴 **這兩支的內容不一定是在這裡寫的**——誰是真相源查 `docs/file-ownership.tsv` |
| `scripts/` | 49 支 | `ticket``roster``isep-nag``wiki-compress``github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) |
> 🔴 這五個數字**每次都要在自己的樹上實數**,不准沿用上一版、也不准用加減推
@@ -70,6 +70,13 @@ hook 一律用官方的 `${CLAUDE_PLUGIN_ROOT}`**不准寫死絕對路徑、
🔴 **只改這裡,然後兩邊 `/plugin update`。**
不要再改 `InkStoneCo/.claude/hooks/`——那個目錄退場中。
🔴 **但有些檔案的內容不是在這裡寫的**`inkstone/ISEP#122`):
`skills/``commands/``agents/` 底下有幾個檔案在 InkStoneCo 也有一份,
**誰是真相源、往哪個方向修,一律查 `docs/file-ownership.tsv`**,不要憑「ISEP 一定比較新」動手
——2026-09-02 實查,兩個方向都真的發生過(`ship-check` 是那邊新,`sdd-check` 是這邊新)。
改完那類檔案要把 commit/sha256 兩欄一起更新;沒更新的話,
`isep-presence-beacon.sh` 會在下一個 session 開場當著大家的面說它對不上。
## 這些閘各自在管什麼
**不用點開任何 `.sh`**——`docs/hooks-inventory.md` 一支一行白話,按「你會在什麼時候撞到它」分組。
+35 -2
View File
@@ -330,11 +330,19 @@ bash hooks/tests/unpushed-police.test.sh
- B 群(⑦—⑩,該報)任一紅 ⇒ 為了不吵而改成放行了,那是把閘關掉不是修好
- 特別看 ④:問不到遠端(離線)**不准當成「你沒推」**
### A18 — 信標會講出雲端會壞掉的那件事:14
### A18 — 信標會講出雲端會壞掉的那件事:26
```
bash hooks/tests/isep-presence-beacon.test.sh
```
**該看到**`通過 14 條,失敗 0 條`。全離線。
**該看到**`通過 26 條,失敗 0 條`。全離線。
> 📌 `inkstone/ISEP#122` 從 14 條加到 26 條:信標原本只掃 `scripts/`
> 而**真正會自動載入的東西**skillcommandagent)同樣兩邊各有一份。
> 實查:那 9 個檔案在 `0.1.0`(c263866)複製過來之後**一次都沒再同步**,
> 到 2026-09-02 已經分家兩個、而且**方向相反**——
> `skills/ship-check/SKILL.md` 是 InkStoneCo 那邊新,`commands/sdd-check.md` 是 ISEP 這邊新。
> ⇒ 所以新增的不只是「有沒有掃到」,還有「**說不說得出真相源是哪一份**」
> `docs/file-ownership.tsv`),以及**雲端沒有專案那一份可比時**改用 sha256 單邊驗。
**它在守什麼**inkstone/ISEP#90 ②③):信標原本只證明「有一份 plugin 載入了」,
不證明「載入的是哪一份」——08-27 雲端載 0.3.9、main 0.9.0,差 7 個 release
@@ -346,6 +354,31 @@ bash hooks/tests/isep-presence-beacon.test.sh
- ⑤⑦ 紅 ⇒ 舊複本遮蔽正門抓不到(`InkStoneCo/scripts/ticket` 那件)
- ⑥ 紅 ⇒ **誤攔**:同步過的複本被念,人就學會忽略它
- ⑪ 紅 ⇒ 內層迴圈變數撞名的回歸(同一個 `.claude` 下第二個殘骸會被靜靜跳過)
- ⑮⑱⑲⑳ 任一紅 ⇒ 自動載入的分身抓不到(`ship-check` 那件會再發生一次)
- ⑰ 紅 ⇒ **誤攔**:同步過的複本被念,人就學會忽略它(跟 ⑥ 同一條線)
- ㉔ 紅 ⇒ 雲端唯一還作數的那個檢查失效(薄殼沒有 InkStoneCo 可以比)
- ㉕ 紅 ⇒ **誤攔**:還沒登記 sha256 的檔案被當成「被改過」
### A29 — 閘印出來的逃生門,照著打真的過得去:10 條
> 📌 編號跳過 A27/A28:那兩格被同一張票的 PR `inkstone/ISEP#124` 佔著(還沒併)。
```
bash hooks/tests/history-first-guard.test.sh
```
**該看到**`通過 10 條,失敗 0 條`。全離線,全程用 `KBDB_STAMP_DIR` 指到 TMP
**不碰這台機器真正的 `/tmp/.kbdb-*` 戳記**(清掉別人的戳記=把閘弄成隨機的)。
**它在守什麼**`inkstone/ISEP#122`2026-09-02 實撞):`history-first-guard.sh` 印的
`🚪 KBDB 真的連不上:touch /tmp/.kbdb-down 後重送` 是**假的**——
`touch` 造出來的是空檔,而那支閘讀的是**檔案內容**當時戳,空字串被當成 0
⇒ 「距今 17 億秒」⇒ 永遠不新鮮 ⇒ **照著訊息打完,被一模一樣地擋第二次**
🔴 這跟本票 comment 6071、`inkstone/ISEP#125` 是同一句話:
**閘印出來的下一步,沒有人實際照著打過一次。**
**失敗**
- ④ 紅 ⇒ 逃生門又是假的(這是本支唯一分辨得出新舊版的那一格,見檔頭)
- ⑦ 紅 ⇒ 逃生門變成永久後門(一次 `touch` 就免疫,過期不算數這件事沒了)
- ⑧⑨⑩ 任一紅 ⇒ **誤攔**:文件/新檔/測試檔本來就不該被這支碰
### A19 — 下游做完時頂層跟著關:49 條
```
bash scripts/test-ticket-handoff-writeback.sh
+30
View File
@@ -0,0 +1,30 @@
# file-ownership.tsv — 同一個檔案兩邊都有時,**哪一份是真相源**inkstone/ISEP#122
#
# 為什麼要有這張表(2026-09-02 實查,兩個方向都真的發生過):
# skills/ship-check/SKILL.md InkStoneCo 那份前進到 651 行,ISEP 停在 595 行
# commands/sdd-check.md ISEP 那份前進到 81 行,InkStoneCo 停在 65 行
# InkStoneCo 那份還在教已經退役的「唯一 active SDD」)
# ⇒ 「ISEP 一定比較新」是錯的,「InkStoneCo 一定比較新」也是錯的。
# 兩邊都會被就地編輯,所以歸屬**不能靠人記得**,要寫下來、而且要有東西去比對。
#
# 誰在用這張表:`hooks/lib/beacon_report.py`SessionStart 信標,只報告、不擋人)
# ① 兩邊內容不同時 → 照這張表講出「真相源是哪一份、該往哪個方向修」
# ② 表上有 sha256 時 → 順便驗 ISEP 這份有沒有被就地改過(雲端沒有 InkStoneCo
# 可以比,這一格是那時唯一還能作數的檢查)
#
# 規約三條:
# 1. **只有內容會分家的檔案才需要一列。** 兩邊一模一樣時不必登記,信標也不會出聲。
# 2. 真相源是 `inkstone/InkStoneCo` ⇒ **內容要改就改在那裡**,改完把檔案原樣搬過來、
# 更新 ④⑤ 兩欄、升版。**不要在 ISEP 這邊改內容**(改了兩邊就各自演化,就是本表要解的病)。
# 3. 真相源是 `inkstone/ISEP` ⇒ 專案那份是退場中的舊複本,該被同步或刪掉;
# ③④⑤ 三欄填 `-`。
#
# 欄位(TAB 分隔,5 欄):
# ① plugin 內路徑
# ② 真相源 repo
# ③ 真相源在那個 repo 裡的路徑(真相源就是 ISEP 時填 `-`)
# ④ 這一份是從哪顆 commit 搬過來的(同上,填 `-`)
# ⑤ 搬過來的當下 sha256(同上,填 `-`)
#
skills/ship-check/SKILL.md inkstone/InkStoneCo .claude/skills/ship-check/SKILL.md bd52fa3a508cbfb29755ddc24923c6fea8c3fbf7 847967830007e804a16dec9adfc5d4120c233445649d4d5cdd34b04550fb3270
commands/sdd-check.md inkstone/ISEP - - -
1 # file-ownership.tsv — 同一個檔案兩邊都有時,**哪一份是真相源**(inkstone/ISEP#122)
2 #
3 # 為什麼要有這張表(2026-09-02 實查,兩個方向都真的發生過):
4 # skills/ship-check/SKILL.md InkStoneCo 那份前進到 651 行,ISEP 停在 595 行
5 # commands/sdd-check.md ISEP 那份前進到 81 行,InkStoneCo 停在 65 行
6 # (InkStoneCo 那份還在教已經退役的「唯一 active SDD」)
7 # ⇒ 「ISEP 一定比較新」是錯的,「InkStoneCo 一定比較新」也是錯的。
8 # 兩邊都會被就地編輯,所以歸屬**不能靠人記得**,要寫下來、而且要有東西去比對。
9 #
10 # 誰在用這張表:`hooks/lib/beacon_report.py`(SessionStart 信標,只報告、不擋人)
11 # ① 兩邊內容不同時 → 照這張表講出「真相源是哪一份、該往哪個方向修」
12 # ② 表上有 sha256 時 → 順便驗 ISEP 這份有沒有被就地改過(雲端沒有 InkStoneCo
13 # 可以比,這一格是那時唯一還能作數的檢查)
14 #
15 # 規約三條:
16 # 1. **只有內容會分家的檔案才需要一列。** 兩邊一模一樣時不必登記,信標也不會出聲。
17 # 2. 真相源是 `inkstone/InkStoneCo` ⇒ **內容要改就改在那裡**,改完把檔案原樣搬過來、
18 # 更新 ④⑤ 兩欄、升版。**不要在 ISEP 這邊改內容**(改了兩邊就各自演化,就是本表要解的病)。
19 # 3. 真相源是 `inkstone/ISEP` ⇒ 專案那份是退場中的舊複本,該被同步或刪掉;
20 # ③④⑤ 三欄填 `-`。
21 #
22 # 欄位(TAB 分隔,5 欄):
23 # ① plugin 內路徑
24 # ② 真相源 repo
25 # ③ 真相源在那個 repo 裡的路徑(真相源就是 ISEP 時填 `-`)
26 # ④ 這一份是從哪顆 commit 搬過來的(同上,填 `-`)
27 # ⑤ 搬過來的當下 sha256(同上,填 `-`)
28 #
29 skills/ship-check/SKILL.md inkstone/InkStoneCo .claude/skills/ship-check/SKILL.md bd52fa3a508cbfb29755ddc24923c6fea8c3fbf7 847967830007e804a16dec9adfc5d4120c233445649d4d5cdd34b04550fb3270
30 commands/sdd-check.md inkstone/ISEP - - -
+2 -2
View File
@@ -203,7 +203,7 @@
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
| `isep-presence-beacon.sh` | 對話一開始印一行 `🟢 ISEP vX.Y.Z 已載入(N 支閘|來源:…)`。**這行不是裝飾,是唯一能證明「這個 session 真的有閘」的東西**——它自己就住在 plugin 裡,看得到它就表示 plugin 載入了;某個 session 從頭到尾沒有這行,那個 session 是零閘狀態,先修 plugin 再做事。同一台機器可能同時有兩份 ISEP(marketplace 裝的、repo 裡 vendor 的),所以那行會講出這次是哪一份在說話。 | 📝 記錄(context 注入) |
| `isep-presence-beacon.sh` | 對話一開始印一行 `🟢 ISEP vX.Y.Z 已載入(N 支閘|來源:…)`。**這行不是裝飾,是唯一能證明「這個 session 真的有閘」的東西**——它自己就住在 plugin 裡,看得到它就表示 plugin 載入了;某個 session 從頭到尾沒有這行,那個 session 是零閘狀態,先修 plugin 再做事。同一台機器可能同時有兩份 ISEP(marketplace 裝的、repo 裡 vendor 的),所以那行會講出這次是哪一份在說話。它另外會報四件「這一份是不是還有效」:①這一份落後 ISEP main 幾版 ②專案裡有沒有內容不同的同名**腳本**②b 專案裡有沒有內容不同的同名**skillcommandagent**——這些是**自動載入**的,載到舊的那份不會有任何症狀,只會安靜地教錯的東西(`docs/file-ownership.tsv` 說得出每一組的真相源是哪一份、往哪個方向修)②c 本身的內容跟那張歸屬表對不對得上(雲端沒有專案那一份可比時,這是唯一還作數的檢查)③工作區有沒有已退役機制留下的產物。**全部只講不擋。** | 📝 記錄(context 注入) |
| `session-start-recall.sh` | 對話一開始就自動把「全局現況」(Gitea 各 repo 的票、KBDB 的藏書地圖)推到 AI 眼前,不必等它自己想到要查。 | 📝 記錄(context 注入) |
| `scripts/mainline refresh` | (不是閘,是腳本)對話一開始把**現在的主線**那條 milestone 的進度與期限更新一次,好讓每回合眼前那一行講的是今天的數字。拿不到 Gitea 就原封不動——**寧可資料舊,不要把主線弄丟**。跑一次就結束,不輪詢。 | 📝 記錄 |
| `overdue-nag-guard.sh` | 對話一開始就去 Gitea 撈**沒有人會叫的事**:逾期的 milestone、掛著等你的票(標「等了幾天」)、標著「有人在做」卻好幾天沒動的票;撈完用白話講出來,**有事就發 Telegram 給你**。沒東西可報時它會說「查過了,沒有」——**安靜跟壞掉長得一模一樣**。發不出去時不會靜默:它會先問這個 session 的 `prod-write-guard` 會不會擋(舊版把「發通知」誤認成「部署」),擋就改貼回票上並把原文印在眼前。只在開 session 時跑一次,**不輪詢、不掛排程**。 | 📝 記錄(context 注入 Telegram |
@@ -252,7 +252,7 @@
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
| `history-first-guard.sh` | AI 要改一個舊檔案之前,先把這個檔案過去被改過幾次、被誰在什麼情況下改過的紀錄攤在它眼前,逼它回答「這是不是已經修過的老問題」再動手。 | 🛑 擋 |
| `history-first-guard.sh` | AI 要改一個舊檔案之前,先把這個檔案過去被改過幾次、被誰在什麼情況下改過的紀錄攤在它眼前,逼它回答「這是不是已經修過的老問題」再動手。前面還有第 0 道:這一小時內有沒有問過 KBDB。**KBDB 連不上時 `touch /tmp/.kbdb-down` 就放行**——這條逃生門 2026-09-02 之前是壞的(`touch` 造的是空檔,而它讀的是檔案內容)**照著訊息打完還是被擋**,`inkstone/ISEP#122` 修好並補了迴歸測試。 | 🛑 擋 |
---
+141 -2
View File
@@ -1,6 +1,7 @@
# ── 以下格是 inkstone/ISEP#90 加的「這一份是不是還有效」自檢 ──────────
# ── 以下格是「這一份是不是還有效」自檢 ─────────────────────────────
# ①②③ inkstone/ISEP#90 立;②b②c inkstone/ISEP#122 補(會自動載入的東西也有分身)
# 都**只是報告,不擋任何事**(SessionStart 本來就不該擋),而且每一格拿不到答案就閉嘴。
import json, os, re, subprocess, sys, time, urllib.request
import hashlib, json, os, re, subprocess, sys, time, urllib.request
ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
PROJ = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
@@ -103,6 +104,144 @@ if sh:
"在雲端一律死在「拿不到 gitea token」。要嘛刪掉複本改叫 "
"`\"$CLAUDE_PLUGIN_ROOT\"/scripts/<名字>`,要嘛把複本同步回 ISEP。" % "".join(sh))
# ══ ②b 會**自動載入**的東西(skillcommand/agent)兩邊各有一份而內容不同 ══
#
# 🔴 為什麼上面那一格抓不到(inkstone/ISEP#1222026-09-02 實查):
# ② 只掃 `scripts/`。而真正會**自動載入**的東西住在別的目錄,同樣兩邊各有一份:
# plugin `skills/<名>/SKILL.md` ↔ 專案 `.claude/skills/<名>/SKILL.md`
# plugin `commands/<名>.md` ↔ 專案 `.claude/commands/<名>.md`
# plugin `agents/<名>.md` ↔ 專案 `.claude/agents/<名>.md`
#
# 實況:這 9 個檔案在 ISEP `0.1.0`c263866)從 InkStoneCo 複製過來**一次**
# 之後再也沒有同步過。到 2026-09-02 已經分家兩個,而且**方向相反**:
# `skills/ship-check/SKILL.md` InkStoneCo 651 行 ISEP 595 行
# `commands/sdd-check.md` ISEP 81 行 InkStoneCo 65 行
# InkStoneCo 那份還在教 ISEP#91 已退役的「唯一 active SDD」)
# ⇒ 「ISEP 一定比較新」與「InkStoneCo 一定比較新」**兩句都是錯的**。
# 兩份都會被就地編輯 ⇒ 歸屬只能寫下來(`docs/file-ownership.tsv`)並且要有東西去比。
#
# 🔴 這一格比 ② 嚴重:腳本要有人叫它才會跑,**skill/command 是自動載入的**——
# 載到舊的那份不會報錯、不會變慢、不會有任何症狀,只會安靜地教錯的東西。
# ship-check 就是這樣:舊描述在「我要發一篇部落格文章」時根本不會被觸發。)
#
# 判準與 ② 同一條:**檔名一樣而內容不同**才出聲,同步過的不吵。
LOADABLE_PAIRS = [("skills", ".claude/skills", True),
("commands", ".claude/commands", False),
("agents", ".claude/agents", False)]
def ownership():
"""讀 docs/file-ownership.tsv → {plugin 內路徑: (真相源, 真相源路徑, commit, sha256)}"""
out = {}
try:
with open(os.path.join(ROOT, "docs", "file-ownership.tsv"), encoding="utf-8") as f:
for line in f:
if not line.strip() or line.lstrip().startswith("#"):
continue
c = line.rstrip("\n").split("\t")
if len(c) >= 2 and c[0].strip():
out[c[0].strip()] = tuple((c[1:5] + ["-", "-", "-", "-"])[:4])
except Exception:
pass
return out
OWN = ownership()
def loadable_pairs():
"""產生 (plugin 內相對路徑, 專案內相對路徑)——只列 plugin 真的有的那些"""
for pdir, jdir, nested in LOADABLE_PAIRS:
src = os.path.join(ROOT, pdir)
if not os.path.isdir(src):
continue
try:
names = sorted(os.listdir(src))
except Exception:
continue
for name in names:
if nested:
if os.path.isfile(os.path.join(src, name, "SKILL.md")):
yield "%s/%s/SKILL.md" % (pdir, name), "%s/%s/SKILL.md" % (jdir, name)
elif name.endswith(".md") and os.path.isfile(os.path.join(src, name)):
yield "%s/%s" % (pdir, name), "%s/%s" % (jdir, name)
def whose(rel):
"""真相源是誰 → 一句照著做就會走到的話。表上沒有就誠實說未定,不要猜。"""
row = OWN.get(rel)
if not row:
return "歸屬未定 ⇒ 兩份都看一眼,決定之後補一列進 docs/file-ownership.tsv"
src, spath, _commit, _sha = row
if src == "inkstone/ISEP":
return "真相源=ISEP 這一份 ⇒ 專案那份是舊複本,同步過去或刪掉它"
return ("真相源=%s:%s ⇒ 內容改在那裡,改完原樣搬進 ISEP、"
"更新 docs/file-ownership.tsv 的 commitsha256、升版" % (src, spath or "?"))
def shadow_loadables():
out = []
roots = [PROJ, os.path.join(PROJ, "InkStoneCo")]
for prel, jrel in loadable_pairs():
a = os.path.join(ROOT, prel)
try:
ab = open(a, "rb").read()
except Exception:
continue
for base in roots:
b = os.path.join(base, jrel)
try:
if os.path.realpath(b) == os.path.realpath(a) or not os.path.isfile(b):
continue
if open(b, "rb").read() != ab:
out.append("%s%s%s"
% (prel, os.path.relpath(b, PROJ), whose(prel)))
except Exception:
pass
return out
ld = shadow_loadables()
if ld:
notes.append(
"🟡 **會自動載入的東西兩邊各有一份,而且內容不同**:\n - %s\n"
" 自動載入的東西載到舊的那份**不會有任何症狀**——不報錯、不變慢,"
"只會安靜地教錯的東西(`ship-check` 的舊描述在「我要發一篇部落格文章」時"
"根本不會被觸發)。歸屬表:docs/file-ownership.tsvinkstone/ISEP#122)。"
% "\n - ".join(ld))
# ══ ②c ISEP 自己這一份,跟歸屬表記的那顆對不對得上 ══════════════════════
#
# 🔴 為什麼要有這一格:②b 要「專案那一份」在磁碟上才比得出來,
# 而**雲端的 project dir 是薄殼,根本沒有那一份**(ISEP#90 記過同一件事)。
# 這一格只比「檔案 vs 表上寫的 sha256」——離線、單邊、不依賴任何別的 repo,
# 是雲端唯一還作數的那個檢查。
def off_manifest():
out = []
for rel, (src, spath, commit, sha) in OWN.items():
if not sha or sha == "-":
continue
p = os.path.join(ROOT, rel)
if not os.path.isfile(p):
continue
try:
h = hashlib.sha256(open(p, "rb").read()).hexdigest()
except Exception:
continue
if h != sha:
out.append("%s(表記 %s%s…,實際 %s…;真相源 %s:%s"
% (rel, (commit or "?")[:7], sha[:12], h[:12], src, spath or "?"))
return out
om = off_manifest()
if om:
notes.append(
"🟡 **ISEP 這一份跟歸屬表對不上**:%s。兩種可能,兩種都要動手:"
"① 它被就地改過 ⇒ 內容要改在真相源那邊,這裡只放原樣搬過來的複本;"
"② 它是同步過的新內容、只是沒更新 docs/file-ownership.tsv 的 commitsha256 ⇒ 補上那兩欄。"
% "".join(om))
# ══ ③ 工作區有沒有「已退役機制」留下的產物 ══════════════════════════════
#
# 判準是機械的、而且會自己長大:**plugin 自己的原始碼裡有沒有任何一個字提到這個目錄**。
+78
View File
@@ -25,9 +25,21 @@ mkplugin() {
cp "$HOOK" "$d/hooks/$(basename "$HOOK")"
cp "$REAL_HOOKS/lib/beacon_report.py" "$d/hooks/lib/beacon_report.py"
printf '#!/bin/sh\necho real\n' > "$d/scripts/ticket"
# inkstone/ISEP#122:會自動載入的東西(skillcommandagent)也要造得出來
mkdir -p "$d/skills/demo" "$d/commands" "$d/agents" "$d/docs"
printf 'PLUGIN 版的 skill\n' > "$d/skills/demo/SKILL.md"
printf 'PLUGIN 版的 command\n' > "$d/commands/demo.md"
printf 'PLUGIN 版的 agent\n' > "$d/agents/demo.md"
printf '%s\n' "$d"
}
# mkown <plugin root> <列...> → 寫一份 docs/file-ownership.tsv(欄位用真正的 TAB
mkown() {
d="$1"; shift; mkdir -p "$d/docs"
{ printf '# 測試用\n'; for row in "$@"; do printf '%s\n' "$row"; done; } \
| sed 's/|/\t/g' > "$d/docs/file-ownership.tsv"
}
# run <plugin root> <project dir> → 印出 systemMessageJSON 壞掉就印 __BADJSON__
run() {
CLAUDE_PLUGIN_ROOT="$1" CLAUDE_PROJECT_DIR="$2" bash "$1/hooks/$(basename "$HOOK")" 2>/dev/null \
@@ -112,6 +124,72 @@ printf '#!/bin/sh\necho "OLD"\n' > "$J6/scripts/ticket"
out=$(run "$P" "$J6")
check "⑭ 報告內容含引號 → JSON 仍然合法" "$out" "🟢 ISEP" "!__BADJSON__"
P2=$(mkplugin 1.0.0) # ⑫⑬ 會刪掉 $P 的 report,這一組要自己一份
echo "── ②b 會自動載入的東西也有分身(inkstone/ISEP#122)──────────────"
# 🔴 這一組守的是 ship-check 那件:plugin 帶著舊描述,而舊描述在「我要發部落格文章」
# 的情境根本不會被觸發 ⇒ 那個 session 永遠載不到出貨流程,而且不會有任何症狀。
JS="$TMP/proj-skill"; mkdir -p "$JS/.claude/skills/demo"
printf '專案版的 skill(比較舊)\n' > "$JS/.claude/skills/demo/SKILL.md"
out=$(run "$P2" "$JS")
check "⑮ 專案有同名 skill 而內容不同 → 要點名" "$out" \
"會自動載入的東西兩邊各有一份" "skills/demo/SKILL.md" ".claude/skills/demo/SKILL.md"
check "⑯ 歸屬表沒登記 → 誠實說未定,不要替人猜方向" "$out" "歸屬未定"
JS2="$TMP/proj-skill-same"; mkdir -p "$JS2/.claude/skills/demo"
cp "$P2/skills/demo/SKILL.md" "$JS2/.claude/skills/demo/SKILL.md"
out=$(run "$P2" "$JS2")
check "⑰ 內容一模一樣 → 不吵(誤攔比漏擋嚴重)" "$out" "!會自動載入的東西兩邊各有一份"
JC="$TMP/proj-cmd"; mkdir -p "$JC/.claude/commands"
printf '專案版的 command(比較舊)\n' > "$JC/.claude/commands/demo.md"
out=$(run "$P2" "$JC")
check "⑱ command 也要掃(sdd-check 那件)" "$out" "commands/demo.md" ".claude/commands/demo.md"
JA="$TMP/proj-agent"; mkdir -p "$JA/.claude/agents"
printf '專案版的 agent(比較舊)\n' > "$JA/.claude/agents/demo.md"
out=$(run "$P2" "$JA")
check "⑲ agent 也要掃" "$out" "agents/demo.md" ".claude/agents/demo.md"
JCL="$TMP/proj-cloud"; mkdir -p "$JCL/InkStoneCo/.claude/skills/demo"
printf '專案版的 skill(比較舊)\n' > "$JCL/InkStoneCo/.claude/skills/demo/SKILL.md"
out=$(run "$P2" "$JCL")
check "⑳ 雲端排法:真身在 \$TOP/InkStoneCo/ 底下也要抓到" "$out" \
"InkStoneCo/.claude/skills/demo/SKILL.md"
echo "── 歸屬表要講得出「往哪個方向修」──────────────────────────────"
PO=$(mkplugin 1.0.1)
mkown "$PO" "skills/demo/SKILL.md|inkstone/InkStoneCo|.claude/skills/demo/SKILL.md|abc1234|-"
out=$(run "$PO" "$JS")
check "㉑ 真相源=InkStoneCo → 說「內容改在那裡」並給得出路徑" "$out" \
"真相源=inkstone/InkStoneCo:.claude/skills/demo/SKILL.md" "內容改在那裡" "!歸屬未定"
mkown "$PO" "skills/demo/SKILL.md|inkstone/ISEP|-|-|-"
out=$(run "$PO" "$JS")
check "㉒ 真相源=ISEP → 說「專案那份是舊複本」" "$out" \
"真相源=ISEP 這一份" "舊複本" "!歸屬未定"
echo "── ②c 沒有專案那一份也要驗得出來(雲端唯一作數的檢查)────────────"
# 雲端的 project dir 是薄殼,沒有 InkStoneCo 可以比 ⇒ 只剩「檔案 vs 表上的 sha256」
SHA=$(shasum -a 256 "$PO/skills/demo/SKILL.md" | cut -d" " -f1)
mkown "$PO" "skills/demo/SKILL.md|inkstone/InkStoneCo|.claude/skills/demo/SKILL.md|abc1234|$SHA"
out=$(run "$PO" "$J")
check "㉓ sha256 對得上(而且專案那份不存在)→ 一個字都不說" "$out" \
"🟢 ISEP v1.0.1 已載入" "!跟歸屬表對不上" "!會自動載入的東西兩邊各有一份"
printf '被就地改過\n' >> "$PO/skills/demo/SKILL.md"
out=$(run "$PO" "$J")
check "㉔ 檔案被就地改過 → 就算沒有專案那份,也要抓得到" "$out" \
"跟歸屬表對不上" "skills/demo/SKILL.md" "abc1234"
mkown "$PO" "skills/demo/SKILL.md|inkstone/InkStoneCo|.claude/skills/demo/SKILL.md|abc1234|-"
out=$(run "$PO" "$J")
check "㉕ 表上 sha 欄是 \`-\` → 這一格閉嘴(還沒登記不是錯)" "$out" "!跟歸屬表對不上"
echo "── 加了這兩格也不准讓信標消失 ────────────────────────────────"
out=$(run "$P2" "$JS")
check "㉖ ②b 有報告時,那行綠色信標仍在且 JSON 合法" "$out" \
"🟢 ISEP v1.0.0 已載入" "!__BADJSON__"
echo
printf '通過 %s 條,失敗 %s 條(共 %s 條)\n' "$PASS" "$FAIL" "$N"
[ "$FAIL" = 0 ] || exit 1
+68 -12
View File
@@ -1,18 +1,21 @@
---
name: ship-check
description: |
改完任何會影響用戶的東西之後、說「做完了」之前必讀(改雲端 workerportaldaemon
workflowinstaller 都算)。也在下列時機自動載入:要打包 App、要出貨、要推 bundle
要送 MS Store、leo 問「可以測了嗎」「版本為什麼沒變」「更新了嗎」「封測者拿得到嗎」。
核心判準:**版本號是 leo 唯一的驗收介面**——portal 版本卡看雲端、daemon 檢查更新看桌面;
版本沒動=他無從判斷你做了什麼=等於沒交付,而「我在某台實例 wrangler deploy 過了」不算
收齊:兩條版本線的差別/重打 bundle(最常漏,要 grep 複驗改動真的進去)/
改 workflow 要重編預編圖/三支機械閘+把 DMG/zip 真的打開檢查/寫 changelog(用戶語言)/
D20 開閘出貨/purge jsDelivr/從 leo 會看的那兩處抓實際畫面複驗。
附「常見漏掉的」實撞表與收工前五問
**任何東西要從「內」(Gitea)送到「外」之前必讀**——不只是 arcrun。
對外包含:發一篇部落格文章到 uncle6.me、推 tag 到 GitHub 鏡像 repo、投稿 n8n 官方模板庫
出雲端零件包、出桌面 daemon。也在下列時機自動載入:改完會影響用戶的東西之後、
要打包 App/出貨/推 bundle/送 MS Store、要發文、要 pages deploy、要推 GitHub、
leo 問「可以測了嗎」「版本為什麼沒變」「更新了嗎」「封測者拿得到嗎」「發出去了嗎」
🔴 第一個動作是**分辨這是哪個出口**,因為 stage/版本/arm 每個出口都不同——
流程本體住在 system-dev/docs/3-specs/critical-paths/ship.md(唯一一份),本 skill 是它的入口。
核心判準:**收的人拿到的東西真的變了,而且他看得出變了什麼**;
對 arcrun 兩條線而言那個介面就是版本號(portal 看雲端、daemon 看桌面)
收齊:五個出口的判準/兩條版本線的差別/重打 bundle(最常漏)/改 workflow 要重編預編圖/
三支機械閘+把 DMG/zip 真的打開檢查/寫 changelogD20 開閘出貨/purge jsDelivr
從收的人會看的那個地方抓實際畫面複驗。附「常見漏掉的」實撞表與收工前五問。
---
# /ship-check — 改完東西後,讓 leo 看得到版本變了
# /ship-check — 東西要出去之前,先確定收的人真的會拿到
> **這支解什麼病**leo 2026-08-05 原話):
> 「對人來說,**我雲端看 portal 有沒有更新,本地看 daemon 有沒有更新**,
@@ -25,6 +28,44 @@ description: |
---
## 🚦 第一步:這是哪個出口?(2026-09-02 補,`inkstone/InkStoneCo#112`
> **為什麼補這段**leo 2026-09-02):「Gitea 為『內』,只要對外都是『外』,
> **任何東西出去之前都要經過出貨流程,不是只有 Arcrun, Arcrun RAG 這個計劃**」
>
> 🔴 **這支 skill 2026-09-01 失效過**:它自己與它指向的地方**只認得 daemon 那個形狀**
> 於是那晚要發部落格與 GitHub 模板時,它沒把人導向任何可用的流程——**只好即興補**。
- 🔴 **流程本體只有一份**`system-dev/docs/3-specs/critical-paths/ship.md`
- 它定義了**內/外的判準**、**六個步驟**、**五個出口各自的 stage/版本/arm/驗法**
- **動手前先讀那一卷的對應出口段**,不要憑本 skill 的印象做
- **判準一句:送出去之後,有沒有一個不是我們的人看得到/拿得到?** 有 ⇒ 走那六步
- **五個出口,各自去 `ship.md` 的哪一段**
- ① 桌面 daemon`v0.18.x`)——**本 skill 底下的內容主要是它與 ②**
- ② 雲端零件包(`1.4.x`)——同上
- ③ 部落格 uncle6.me — 🔴 **沒有 arm、沒有版本號**stage 是 `<8碼>.kbcontent.pages.dev` 快照;
`pages deploy` **必須在 repo 外的目錄跑**,否則 `.env` 的 token 會蓋掉 OAuth 並報成權限錯誤
**驗法是瀏覽器,不是 `curl`**(收件人是讀者 ⇒ 見 §5.8 與 `ship.md` 出口③「驗法」)
- ④ GitHub 鏡像 repo — 🔴 **要 arm,而且連 `git remote add` 都擋**D20);
Gitea 就是它的 stage**推 tag 之外還要發 release note**
(怎麼發、要不要再 arm ⇒ `ship.md` 出口④ 的「release note 怎麼發」段)
- 🔴 **步驟 3 的「Use this template」也要 arm,而且要單獨請一次**——它建的是
**第二個 repo**,撞 ROE「單一 repo」那條,夾不進推 tag 的窗口。
且 guard 只掛在 `Bash`**走瀏覽器按那顆按鈕不會被擋,這條靠人守**
(⇒ `ship.md` 出口④ 的「Use this template 要不要 arm」段)
- ⑤ n8n 官方投稿 — 沒有 stage,投稿端點尚未查到(`inkstone/llm-wiki-template#9`
- 🔴 **不要拿 ① 的答案去套 ③④⑤**——那正是 2026-09-01 出錯的機制
- 🔴 **「這個出口沒有 arm」不等於「這個出口不必經過 leo」**——③⑤ 的人閘是**步驟 5**
(leo 親手在 stage 上走過那一遍)。判準全文=`ship.md`
「誰批准、誰按鍵、在哪台跑」段:**leo 批准,機器執行**
- **跨多個 repo 的一批貨,「100%」看母票的相依清單,不要看 milestone 的百分比**
milestone 只數 hub 那個 repo,別的 repo 那幾張不在分母裡)
`ship.md` 的「那個數字看哪裡」段
- 🔴 **發現 `ship.md` 沒涵蓋你手上這件事 ⇒ 那一卷缺了一格,回去補它**
不要在別處另開一份流程(leo 2026-09-02 明確選了「改寫 ship.md」)
---
## ⚠️ 這支 skill 自己的失效模式(先讀這段)
leo 2026-08-05:「**你寫完一個 skill 然後每個我要提醒你,表示這個 skill 無效**」
@@ -44,8 +85,19 @@ leo 2026-08-05:「**你寫完一個 skill 然後每個我要提醒你,表示
## 什麼時候跑
**改完任何會影響用戶的東西之後**(雲端 workerportaldaemonworkflow),
在說「做完了」之前不是收工才跑。
**任何東西要送到「外面」之前**——改完會影響用戶的東西、要發一篇文章、要推 tag 到 GitHub、
要投稿、要出零件包或 daemon。在說「做完了」之前跑,不是收工才跑。
⚠️ **底下的步驟是出口①②(arcrun 兩條版本線)的細節。**
出口③④⑤ 請照上面那段回 `ship.md` 讀該出口的判準——**它們沒有版本號卡可以看**。
🔴 **但底下 §5.8「驗前端=用瀏覽器真的載一次」是全出口通則,不歸①② 所有**
2026-09-02 補:本 skill 這句劃界曾把 §5.8 圈進①②,
`ship.md` 對出口③ 給的驗法是 `curl`**兩個檔對同一個問題給了兩種答案**)。
- **一句話判準:用收的人的那個介面驗。**
- 收的人的介面是**網頁** ⇒ 瀏覽器(出口③ 讀者、出口① 的 portal 版本卡)
- 收的人的介面是 **APICLI**`curl` 才是對的(出口② 的 `/health`,程式在讀它)
- ⇒ 這不是「①② 用 curl、③ 用瀏覽器」,是**每個出口各自問自己的收件人在看什麼**
---
@@ -503,6 +555,10 @@ curl -s "https://rag.arcrun.dev/docs/start/install-windows/?cb=$RANDOM" \
> leo 原話:「**你的環境有 web,你應該用 web 驗,你已經開啓了卻沒有完成,你要把這個列入規定。**」
🔴 **適用範圍:全出口,不是只有①②**2026-09-02 補,見上面「什麼時候跑」那段的劃界)。
**收的人的介面是網頁就套這條**——出口③ 部落格的收件人是讀者,他的介面就是瀏覽器。
`ship.md` 出口③「驗法」那格寫的是同一件事,兩邊只有一種說法。
**為什麼 `curl | grep` 是假驗證**(08-08 實撞,leo 抓到而不是我發現):
`curl` 拿到的是 **HTML 原始碼**——它**不執行 JS、不載入 `config.js`、不發 API 請求**。
所以我 grep 到文案就宣稱「前端驗過」,而使用者實際打開看到的是整條紅色錯誤: