Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d18b6c8e5 | |||
| f13eed7ca2 | |||
| 95c6fee199 | |||
| 49a7145e70 | |||
| bcf7c05a7b | |||
| e9492e5d7c | |||
| 5b0267112f |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "isep",
|
||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:61 支機械閘(85 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、60 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||
"version": "0.25.0",
|
||||
"version": "0.27.0",
|
||||
"keywords": [
|
||||
"inkstone",
|
||||
"guardrails",
|
||||
|
||||
+41
-2
@@ -624,11 +624,24 @@ bash hooks/tests/parallel-lines-cap-guard.test.sh
|
||||
`PostToolUse:Agent` 在**送出後 5 秒**就觸發(不是收工);
|
||||
subagent **不是獨立行程**(`ps` 只有一個 claude-code 行程,所以數行程數不到線)。
|
||||
|
||||
### A24 — 一條線要有自己的工作目錄:59 條
|
||||
### A24 — 一條線要有自己的工作目錄:77 條
|
||||
```
|
||||
bash hooks/tests/line-needs-own-worktree.test.sh
|
||||
```
|
||||
**該看到**:`59/59 通過`。離線,每個案例自己開**兩顆**真的 git repo +一份真的 linked worktree。
|
||||
**該看到**:`77/77 通過`。離線,每個案例自己開**兩顆**真的 git repo +一份真的 linked worktree。
|
||||
|
||||
**E 群在守什麼**(comment 6587,2026-09-07 補):**閘印出來的那條出路要真的走得通。**
|
||||
閘訊息教的是 `WORKTREE_OK=1 git -C … checkout …`,舊版讀的卻是 hook 自己的環境變數——PreToolUse hook
|
||||
跟指令不是同一個行程,指令前綴的賦值到不了它 ⇒ 總管三次照貼三次被擋。E 群的測資是**閘自己印出來的那一行**
|
||||
((61) 先擋一次、(62) 抽出那行、(63) 原樣餵回去要放行);(64)–(67) 釘住判準是**位置不是字**:
|
||||
`echo WORKTREE_OK=1; git checkout`、前綴掛在別的指令上、值不是 1、寫在 commit 訊息裡,四種都照擋。
|
||||
修之前拿新測試跑舊閘:(60)(63)(68) 三條紅,其餘全綠——紅的那三條就是真的改到的。
|
||||
|
||||
**F 群**是 `inkstone/ISEP#125` 票上原文的驗收表 A–F((70)–(75),期望值照票:A–E 要 0、**F 要 2**)。
|
||||
D/E 是「用 heredoc 寫一份內含 `cd <目錄> && git checkout X` 示範的文件」被當成在切分支(09-02 兩次實地命中),
|
||||
現在先用 `lib/strip_heredoc.py` 剝掉 body 再解析(跟 D20 閘同一支 helper)。(76) 是寫成測試的已知邊界:
|
||||
tokenize 層把換行當空白吞掉,`…EOF⏎git checkout main` 從第一版起就看不到(漏擋不是誤攔,與 (53) 同層,另報);
|
||||
(77) 是它的對照組(換成分號就擋)。
|
||||
|
||||
**D 群在守什麼**(comment 5398,2026-08-29 補):不只要擋對,還要**說得出是哪一個 repo**。
|
||||
舊版一律拿 payload 的 `cwd` 當答案,於是 `cd <別的 repo> && git checkout` 擋是擋對了,
|
||||
@@ -646,6 +659,10 @@ D 群同時釘住反面:認不出來(`cd $VAR`、`cd -`、cd 到不是 repo
|
||||
擋掉它們等於線動不了工
|
||||
- ① 紅 ⇒ 連總管在自己的目錄裡切分支都被擋。那不是嚴格,那是把共用目錄的主人趕出去
|
||||
- ㉞㉟ 紅 ⇒ `prune` 做錯了。㉞是沒清掉說謊的登記;**㉟是把還在的 worktree 也弄掉了,那是災難**
|
||||
- (63) 紅 ⇒ 閘印的出路又走不通了——被擋的人不會停下來修閘,他會去找繞過去的方法(6587 當天就是)
|
||||
- (64)–(67) 任一紅 ⇒ 出路退化成關鍵字:印個字就能過,閘等於沒有
|
||||
- (75) 紅 ⇒ **閘廢了**(#125 原話):剝 heredoc 剝過頭,真的切也放行
|
||||
- (76) 紅 ⇒ tokenize 那層被改了,回頭跑 (53) 與推 main 的 10+19 條
|
||||
|
||||
### A25 — `sdd-guard` 退役了,而且退乾淨了:8 條
|
||||
```
|
||||
@@ -911,6 +928,26 @@ bash hooks/tests/isep-presence-beacon.test.sh # ㉗–㉝ 那七條
|
||||
**真實跑(2026-09-07)**:信標不帶 fixture 打真 Gitea(匿名)→ 安靜(v0.24.0 tag+release 都在),
|
||||
留下 `.isep-chain-ok-v0.24.0` 快取;`release-check` 在本分支 → `v0.24.0 ✅✅✅ ←`、三個裁定不補的版本標 ⏭、exit 0。
|
||||
|
||||
### A39 — D20 閘判「push 到哪」看的是指令實際會推的那個 repo:26 條
|
||||
```
|
||||
bash scripts/test-github-contact-guard.sh
|
||||
```
|
||||
**該看到**:`26/26 通過`。離線;後 12 條自己開兩顆 git repo(`shell` 的 origin=github.com、
|
||||
`body` 的 origin 是 Gitea 的形狀),payload 帶 `cwd`,跟 hook 真的會收到的一樣。
|
||||
|
||||
**它在守什麼**(`inkstone/ISEP#109` → comment 6629,2026-09-07 實撞):雲端薄殼的 cwd 是 GitHub 那份,
|
||||
總管站在那裡 `git -C <InkStoneCo> push origin x`(InkStoneCo 的 origin=Gitea),舊版拿 **hook 收到的 cwd**
|
||||
解 `origin` ⇒ 判成「指向 GitHub」擋下,`-C` 看都沒看;arcrun-hand 同日在 `Arcrun#176` comment 6614 撞到同一支。
|
||||
現在 remote 名在**這條指令實際會推的那個 repo** 裡解(沿用 `hooks/lib/push_target_dir.py`:cd 鏈、`-C`、
|
||||
子殼不外洩,推 main 那道閘 08-23 就為同一個病接的它);判準仍是 remote 解出來的 URL 主機,**不是「有 -C 就放行」**。
|
||||
修之前拿新測試跑舊閘:**7 條紅**——4 條是誤攔(推 Gitea 被擋),**3 條是漏擋**(站在 Gitea 那份 `-C` 到薄殼推 GitHub,舊版放行)。
|
||||
|
||||
**失敗**:
|
||||
- 「不該擋」6 條任一紅 ⇒ **誤攔**:雲端推 Gitea 分支又得改用完整網址繞路,久了整道閘沒人當真
|
||||
- 「該擋」5 條任一紅 ⇒ D20 有洞:`-C`/`cd` 指到 GitHub 那份的 push 沒被攔
|
||||
- `(cd body && true); git push` 那條紅 ⇒ 子殼的 cd 外洩了(08-11 穿透的形狀)
|
||||
- 前 14 條紅 ⇒ 舊有判準(heredoc/引號裡的散文/讀取放行)被改壞
|
||||
|
||||
### A37 — 雲端第一則 prompt 的主線就是現在這條,不是兩週前的舊線:31 條
|
||||
```
|
||||
bash hooks/tests/mainline-gitea-fallback.test.sh
|
||||
@@ -1214,6 +1251,8 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面
|
||||
| **A12 留言身份欄(兩道門)** | 總管 | ✅ 11/11(2026-08-27) |
|
||||
| **A23 同時跑的線有上限** | isep-guard | ✅ 33/33(2026-08-29,inkstone/ISEP#109) |
|
||||
| **A24 一條線要有自己的工作目錄** | isep-guard | ✅ 35/35(2026-08-29,inkstone/ISEP#109→5391) |
|
||||
| **A24 出路那一行原樣餵回去走得通+#125 A–F** | isep-hand | ✅ 77/77(2026-09-07,inkstone/ISEP#109→6587、#125)+真路徑實跑(薄殼 cwd、`-C ISEP`:擋→印的那行→放行)+舊閘實跑 3 條紅 |
|
||||
| **A39 D20 閘看 push 實際會推的 repo** | isep-hand | ✅ 26/26(2026-09-07,inkstone/ISEP#109→6629)+真路徑實跑(薄殼 cwd:`-C InkStoneCo` 放行、裸 `git push origin` 擋)+舊閘實跑 7 條紅(4 誤攔+3 漏擋) |
|
||||
| **A26 雲端憑證清單這台拿得到** | isep-hand | ✅ 11/11(2026-09-01,inkstone/ISEP#115→5526/5541/5542/5577)+兩條「該紅」方向實跑 |
|
||||
| **A16 放行的門真的打得開** | 總管 | ✅ 17/17(2026-08-28,inkstone/ISEP#90) |
|
||||
| **A17 未推警察不誤攔雲端分支** | 總管 | ✅ 10/10(2026-08-28,inkstone/ISEP#90) |
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
> **61 支、85 條、60 支腳本是在這棵樹上當場數出來的**(`ls hooks/*.sh | wc -l`=61、
|
||||
> `grep -c '"command":' hooks/hooks.json`=85、`ls -p scripts | grep -v / | wc -l`=60)。
|
||||
|
||||
> 📌 **`inkstone/ISEP#109` → comment 6629(2026-09-07)+0 支、+0 條**:兩支閘改判準不加閘——
|
||||
> `line-needs-own-worktree.sh` 的出路改認指令字串裡的 `WORKTREE_OK=1` 前綴(舊版讀 hook 自己的環境,
|
||||
> 出路永遠走不通);`github-contact-guard.sh` 判 push 目標改在「這條指令實際會推的那個 repo」裡解 remote
|
||||
> (舊版在 hook 的 cwd 解,薄殼站 GitHub、推 Gitea 會誤擋,反向會漏)。
|
||||
> 數字是改完當場數的:`ls hooks/*.sh | wc -l`=61、`grep -c '"command":' hooks/hooks.json`=85。
|
||||
|
||||
> 📌 **`inkstone/ISEP#130`(2026-09-07)+0 支、+1 條**:SessionStart 多掛一條
|
||||
> `scripts/settings-allow-sync --quiet`(不是閘,是腳本:把 ISEP 的權限白名單寫進這台的
|
||||
> `~/.claude/settings.json`,雲端才不會每個正門工具都被分類器擋)。
|
||||
@@ -129,7 +135,7 @@
|
||||
|
||||
| 閘名 | 對你意味著什麼 | 動作 |
|
||||
|---|---|---|
|
||||
| `github-contact-guard.sh` | AI 想碰 GitHub(`git push`、`gh api` 等)就先擋下——兩個帳號曾經因為機器人式高頻寫入被永久 flag 過。**只有讀取(clone/fetch)自由,寫入一律要你在終端機跑 `github-arm.sh` 解鎖**。 | 🛑 擋 |
|
||||
| `github-contact-guard.sh` | AI 想碰 GitHub(`git push`、`gh api` 等)就先擋下——兩個帳號曾經因為機器人式高頻寫入被永久 flag 過。**只有讀取(clone/fetch)自由,寫入一律要你在終端機跑 `github-arm.sh` 解鎖**。判「這次 push 到哪」看的是**這條指令實際會推的那個 repo**(含 `cd …&&`、`git -C …`、cwd 在子目錄),不是 hook 站的 cwd——雲端薄殼站在 GitHub 那份、推的是 Gitea 那份時不會誤擋,反過來也不會漏(`inkstone/ISEP#109`→6629)。 | 🛑 擋 |
|
||||
| `kbdb-api-wall-guard.sh` | AI 想繞過 KBDB 的 API、直接對資料庫下 SQL 或建新表就擋下——KBDB 的鐵律是「只能用 API、永遠只有三張表」。 | 🛑 擋 |
|
||||
| `stage-before-prod-guard.sh` | AI 想動「出貨鏈」相關指令(含 `arcrun-rag-bundles`/`github-arm`/`publish-github` 字樣)卻沒先在 stage 環境驗過就擋下。 | 🛑 擋 |
|
||||
| `main-and-prod-push-guard.sh` | 兩層確認:① subagent 想推 Gitea `main` 分支 → 一定要交回總管,總管確認過才放行;② 任何 `wrangler deploy` 打正式環境 → 一定要你親手解鎖。 | 🛑 擋 |
|
||||
@@ -143,7 +149,7 @@
|
||||
| `reply-identity-guard.sh` | 直接打 Gitea API 貼留言到票上時,內文第一行沒有 `【身份】`(`總管`/`subagent`/`leo`)就擋下——票上多條線並行時,看不出哪一則是誰寫的。純讀取、走 `scripts/ticket` 正門、指令裡加 `reply-identity-ok` 都放行。 | 🛑 擋 |
|
||||
| `comment-carries-task-guard.sh` | AI 要在票裡留一則「等某某上線才驗得了」這種**還沒做完的事**,卻沒有把它開成一張子票時,擋一次。留言沒人會回頭讀,票的 open/closed 撈一次就看得到。**擋下來時印的是一整塊可以直接貼著跑的指令**(腳本用絕對路徑、母票已經填好、內文草稿附在同一塊裡),那塊字是跟 `scripts/ticket usage subtask` 現要的,不是閘自己抄的一份(`inkstone/ISEP#112`)。 | 🛑 擋(同一輪只擋一次) |
|
||||
| `search-is-not-proof-guard.sh` | AI 用知識庫的**搜尋**找到一筆東西,然後把那筆東西的原始編號直接貼進票裡或派工單裡,當成「某個功能壞了」的證據——而它從沒用正規的檢索路徑去查過那筆——擋一次。你關心的是:不會再有人拿錯的方法量出一個錯的結論,然後退回一張其實已經做完的票。 | 🛑 擋(同一輪只擋一次) |
|
||||
| `line-needs-own-worktree.sh` | 一條派工線想在**大家共用的那份工作目錄**裡切分支(`git checkout`/`git switch`)就擋下,叫它先開一份自己的 worktree。你關心的是:不會再有人交回工作時把某個 repo 留在自己的分支上,而下一個人在同一個目錄跑出貨、讀到的是別人留下的 HEAD。**總管在自己的目錄裡切分支不管**(那是他的目錄);已經在自己 worktree 裡的切換全部放行;還原檔案(`git checkout -- 檔`)不管。擋下來的同時會順手把「指向空氣」的 worktree 登記清掉。 | 🛑 擋 |
|
||||
| `line-needs-own-worktree.sh` | 一條派工線想在**大家共用的那份工作目錄**裡切分支(`git checkout`/`git switch`)就擋下,叫它先開一份自己的 worktree。你關心的是:不會再有人交回工作時把某個 repo 留在自己的分支上,而下一個人在同一個目錄跑出貨、讀到的是別人留下的 HEAD。**總管在自己的目錄裡切分支不管**(那是他的目錄);已經在自己 worktree 裡的切換全部放行;還原檔案(`git checkout -- 檔`)不管。擋下來的同時會順手把「指向空氣」的 worktree 登記清掉。出路是閘訊息印的那一行 `WORKTREE_OK=1 git -C … checkout …`——認的是**指令字串裡**的這個前綴,原樣貼回去就過(`inkstone/ISEP#109`→6587:舊版讀 hook 自己的環境變數,那行在 Claude Code 底下永遠走不通)。 | 🛑 擋 |
|
||||
| `diagnosis-evidence-guard.sh` | 你想把一段有份量的東西寫上票,而這個 session **沒派人去查過那張票**、那段話裡也**沒有任何走得過去的出處**(檔案:行號/commit/comment 號/票號/網址/貼出來的實測輸出)就擋一次——未經調查的診斷寫在票上會長得像事實,工人會照著它去驗證而不是去查。派過人查、或把出處帶上,都放行;一句短回覆不管。 | 🛑 擋 |
|
||||
## B. AI 想寫檔案/改程式碼的當下(PreToolUse / Write·Edit·MultiEdit)
|
||||
|
||||
|
||||
@@ -87,12 +87,29 @@ try: print(json.load(sys.stdin).get('cwd','') or '')
|
||||
except Exception: print('')
|
||||
" 2>/dev/null)
|
||||
[ -n "$CWD" ] || CWD=$(pwd)
|
||||
# 🔴 2026-09-07(inkstone/ISEP#109 → comment 6629):remote 名要在**這條指令實際會推的
|
||||
# 那個 repo** 裡解,不是在 hook 收到的 cwd 裡解。雲端薄殼的 cwd 是 GitHub 那份
|
||||
# (origin=github.com),總管站在那裡 `git -C <InkStoneCo> push origin x`
|
||||
# (InkStoneCo 的 origin=Gitea)——舊版拿 cwd 解 origin ⇒ 判成「指向 GitHub」擋下,
|
||||
# `-C` 看都沒看;反過來,站在 Gitea 那份 `git -C <薄殼> push origin x` 則**漏擋**。
|
||||
# ⇒ 沿用 lib/push_target_dir.py(推 main 那道閘 08-23 就是為同一個病接的它):
|
||||
# 純 tokenize 解出 cd 鏈/-C,解得出來就 cd 進去(唯讀)問 git;解不出來
|
||||
# (`cd $VAR`、目錄不存在)才退回 cwd——退回的方向是「照舊」,不是放寬。
|
||||
# 判準沒變:仍是 remote 解出來的 URL 主機(唯一識別碼),不是「有沒有 -C」。
|
||||
PUSHDIR="$CWD"
|
||||
_ptd="$(dirname "$0")/lib/push_target_dir.py"
|
||||
if [ -f "$_ptd" ]; then
|
||||
_expr=$(printf '%s' "$CMD" | python3 "$_ptd" 2>/dev/null || printf '')
|
||||
if [ -n "$_expr" ]; then
|
||||
PUSHDIR=$(cd "$CWD" 2>/dev/null && cd "$_expr" 2>/dev/null && pwd) || PUSHDIR="$CWD"
|
||||
fi
|
||||
fi
|
||||
# 取 push 後面第一個非 flag 的字當 remote 名;沒有就是 origin
|
||||
RNAME=$(printf '%s' "$CMD" | sed -nE 's/.*git[[:space:]]+([^|;&]*[[:space:]])?push[[:space:]]+(-[^[:space:]]+[[:space:]]+)*([A-Za-z0-9._//-]+).*/\3/p' | head -1)
|
||||
[ -n "$RNAME" ] || RNAME="origin"
|
||||
RURL=$(git -C "$CWD" remote get-url "$RNAME" 2>/dev/null)
|
||||
RURL=$(git -C "$PUSHDIR" remote get-url "$RNAME" 2>/dev/null)
|
||||
if printf '%s' "$RURL" | grep -qi 'github\.com'; then
|
||||
HIT="git push → github(remote「$RNAME」指向 GitHub)"
|
||||
HIT="git push → github($PUSHDIR 的 remote「$RNAME」指向 GitHub:$(printf '%s' "$RURL" | sed 's|//[^@]*@|//<憑證省略>@|g'))"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -30,13 +30,24 @@
|
||||
其他 目錄運算式,可能是相對的;呼叫端自己 `cd` 進去問 git(唯讀)。
|
||||
"." =「就是呼叫端自己的 cwd」(指令裡沒有 cd 也沒有 -C)。
|
||||
|
||||
用法:printf '%s' "$CMD" | python3 checkout_target_dir.py
|
||||
出路的判準也住在這裡(inkstone/ISEP#109 → comment 6587,2026-09-07)
|
||||
閘訊息教的出路是 `WORKTREE_OK=1 git -C … checkout …`。舊版讀的是 **hook 自己的
|
||||
環境變數**,而 PreToolUse hook 跑在指令之前、跟指令不是同一個行程——指令字串裡的
|
||||
`WORKTREE_OK=1` 是給 git 的前綴賦值,hook 的行程裡根本沒有它 ⇒ 總管三次照貼三次被擋。
|
||||
⇒ 改成認**指令字串裡的字面前綴**,而且判準是位置不是字:那個賦值必須是
|
||||
「會移動 HEAD 的那條 git 指令」自己的前綴(`echo WORKTREE_OK=1; git checkout` 不算、
|
||||
`WORKTREE_OK=1 echo hi && git checkout` 不算、寫在 commit 訊息裡不算)。
|
||||
留痕就是那個前綴本身——它在指令歷史上。
|
||||
|
||||
用法:printf '%s' "$CMD" | python3 checkout_target_dir.py → 目錄運算式(見上)
|
||||
printf '%s' "$CMD" | python3 checkout_target_dir.py --escape WORKTREE_OK=1
|
||||
→ 那條 checkout 帶著這個前綴就印 1,否則空
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from push_target_dir import _events, _join # noqa: E402
|
||||
from push_target_dir import _ENV_ASSIGN, _events, _join # noqa: E402
|
||||
|
||||
VERBS = ("checkout", "switch")
|
||||
UNKNOWN = "?"
|
||||
@@ -52,14 +63,19 @@ def _step(base, path):
|
||||
|
||||
|
||||
def _moves_head(tokens):
|
||||
"""tokens 是整串 `git …`。回傳 (會不會動 HEAD, -C 指的路徑 or None)。
|
||||
"""tokens 是整串 `[NAME=value …] git …`。
|
||||
回傳 (會不會動 HEAD, -C 指的路徑 or None, 前綴賦值清單)。
|
||||
|
||||
判準只有「這個動作動不動 HEAD」,不看任何措辭:
|
||||
git checkout -- <檔>/git checkout HEAD -- <檔> ⇒ 還原檔案,不動
|
||||
git checkout -p ⇒ 挑 hunk,不動
|
||||
git checkout(沒有任何目標) ⇒ git 自己會報錯
|
||||
"""
|
||||
tok = list(tokens[1:]) # 去掉 "git"
|
||||
tok = list(tokens)
|
||||
env = []
|
||||
while tok and _ENV_ASSIGN.match(tok[0]):
|
||||
env.append(tok.pop(0)) # `WORKTREE_OK=1 git …` 的前綴——出路就認這個
|
||||
tok = tok[1:] # 去掉 "git"
|
||||
c_path = None
|
||||
while tok and tok[0].startswith("-"):
|
||||
if tok[0] == "-C" and len(tok) > 1:
|
||||
@@ -72,24 +88,25 @@ def _moves_head(tokens):
|
||||
tok.pop(0)
|
||||
continue
|
||||
if tok[0].startswith("--git-dir"):
|
||||
return (False, None) # 自己指 git-dir 的花式用法,不猜
|
||||
return (False, None, env) # 自己指 git-dir 的花式用法,不猜
|
||||
tok.pop(0)
|
||||
if not tok or tok[0] not in VERBS:
|
||||
return (False, None)
|
||||
return (False, None, env)
|
||||
|
||||
rest = tok[1:]
|
||||
if "--" in rest or "-p" in rest or "--patch" in rest:
|
||||
return (False, None)
|
||||
return (False, None, env)
|
||||
if not [a for a in rest if not a.startswith("-")] and \
|
||||
not any(a in ("-b", "-B", "-c", "-C") for a in rest):
|
||||
return (False, None)
|
||||
return (True, c_path)
|
||||
return (False, None, env)
|
||||
return (True, c_path, env)
|
||||
|
||||
|
||||
def find_checkout_target(cmd):
|
||||
def find_checkout(cmd):
|
||||
"""回 (目錄運算式, 那條 checkout 的前綴賦值清單)。目錄運算式的三種值見檔頭。"""
|
||||
events = _events(cmd, VERBS, strip_env=True)
|
||||
if not events:
|
||||
return "" # 引號壞掉之類——沿用「不擋在不確定上」
|
||||
return ("", []) # 引號壞掉之類——沿用「不擋在不確定上」
|
||||
|
||||
stack = [None] # 每層括號各自的 cwd;None = 同呼叫端的 cwd
|
||||
for kind, val in events:
|
||||
@@ -101,7 +118,7 @@ def find_checkout_target(cmd):
|
||||
elif kind == "cd":
|
||||
stack[-1] = _step(stack[-1], val)
|
||||
elif kind == "verb":
|
||||
moves, c_path = _moves_head(val)
|
||||
moves, c_path, env = _moves_head(val)
|
||||
if not moves:
|
||||
continue
|
||||
where = stack[-1]
|
||||
@@ -110,13 +127,29 @@ def find_checkout_target(cmd):
|
||||
# (而且 -C 若是相對路徑,是接在 cd 之後算,不是接在 cwd)
|
||||
where = _step(where, c_path)
|
||||
if where == UNKNOWN:
|
||||
return UNKNOWN
|
||||
return where or "."
|
||||
return ""
|
||||
return (UNKNOWN, env)
|
||||
return (where or ".", env)
|
||||
return ("", [])
|
||||
|
||||
|
||||
def find_checkout_target(cmd):
|
||||
return find_checkout(cmd)[0]
|
||||
|
||||
|
||||
def has_escape(cmd, assignment):
|
||||
"""會移動 HEAD 的那條 git 指令,自己的前綴裡有沒有這個字面賦值(例 `WORKTREE_OK=1`)。
|
||||
只看**那條指令的前綴**:印在別處、掛在別的指令上、寫在引號裡都不算。"""
|
||||
_expr, env = find_checkout(cmd)
|
||||
return assignment in env
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.write(find_checkout_target(sys.stdin.read()))
|
||||
argv = sys.argv[1:]
|
||||
cmd = sys.stdin.read()
|
||||
if len(argv) == 2 and argv[0] == "--escape":
|
||||
sys.stdout.write("1" if has_escape(cmd, argv[1]) else "")
|
||||
return
|
||||
sys.stdout.write(find_checkout_target(cmd))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -91,21 +91,31 @@ def _classify(tokens, verbs=("push",), strip_env=False):
|
||||
stays deliberately loose here (the verb may sit anywhere after `git`) --
|
||||
callers re-walk the returned token list to confirm command position, so
|
||||
a false positive at this layer costs nothing."""
|
||||
body = tokens
|
||||
if strip_env:
|
||||
# `FOO=1 git checkout …` -- an env prefix doesn't change which command
|
||||
# runs. Opt-in so find_push_target's behaviour stays byte-identical;
|
||||
# checkout_target_dir.py turns it on (its predecessor stripped these,
|
||||
# and dropping that would have been a silent regression -- caught by
|
||||
# the worktree gate's own case ㉒).
|
||||
#
|
||||
# 2026-09-07 (inkstone/ISEP#109 -> comment 6587): the prefix is skipped
|
||||
# for *classifying* the statement, but the "verb" event hands back the
|
||||
# full token list, prefix included. The worktree gate's escape hatch is
|
||||
# exactly that prefix (`WORKTREE_OK=1 git checkout …`), and a PreToolUse
|
||||
# hook can only see it in the command string -- it never reaches the
|
||||
# hook's own environment. Dropping it here made the printed way out
|
||||
# unwalkable. Callers that don't care simply skip leading NAME=value
|
||||
# tokens themselves (checkout_target_dir._moves_head does).
|
||||
i = 0
|
||||
while i < len(tokens) and _ENV_ASSIGN.match(tokens[i]):
|
||||
i += 1
|
||||
tokens = tokens[i:]
|
||||
if not tokens:
|
||||
body = tokens[i:]
|
||||
if not body:
|
||||
return ("other", None)
|
||||
if tokens[0] == "cd" and len(tokens) > 1:
|
||||
return ("cd", tokens[1])
|
||||
if tokens[0] == "git" and any(v in tokens[1:] for v in verbs):
|
||||
if body[0] == "cd" and len(body) > 1:
|
||||
return ("cd", body[1])
|
||||
if body[0] == "git" and any(v in body[1:] for v in verbs):
|
||||
return ("verb", tokens)
|
||||
return ("other", None)
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 管什麼: 一條派工線要在「共用的工作目錄」裡切分支時擋下來,叫它去開一份自己的 worktree。
|
||||
# 為什麼: 2026-08-29 inkstone/ISEP#109 comment 5391——每個 repo 只有一份工作目錄,所有線共用。#163 切到自己的分支、交回時沒切回來,總管後來在同一個目錄跑 ship.mjs,讀到的是別人留下的 HEAD。票號說得出「這是哪個任務」,沒有任何東西說得出「這個目錄現在是誰留下的狀態」。
|
||||
# 誤觸時怎麼關: `WORKTREE_OK=1 git checkout …`(留痕,跟 NOT_MY_BRANCH_OK 同形狀)。
|
||||
# 誤觸時怎麼關: `WORKTREE_OK=1 git checkout …`——認的是**指令字串裡**這個字面前綴(留痕就在指令歷史上)。
|
||||
# 🔴 2026-09-07(inkstone/ISEP#109 → comment 6587):舊版讀的是 hook 自己的環境變數 `$WORKTREE_OK`,
|
||||
# 而 PreToolUse hook 跟指令不是同一個行程——指令前綴的賦值到不了它 ⇒ 閘印出來的出路在 Claude Code
|
||||
# 底下永遠走不通(總管三次照貼三次被擋)。現在兩種都認:環境變數(本機 shell export 過)與字面前綴
|
||||
# (閘訊息教的那一行)。判準是位置不是字:前綴必須掛在會移動 HEAD 的那條 git 指令上,見
|
||||
# lib/checkout_target_dir.py 檔頭;測試 E 群把閘印出來的那一行原樣餵回去驗它走得通。
|
||||
#
|
||||
# line-needs-own-worktree.sh — 共用工作目錄裡不准切分支(PreToolUse: Bash)
|
||||
#
|
||||
@@ -74,6 +79,13 @@ sys.stdout.write((ti.get("command") or "") if isinstance(ti, dict) else "")
|
||||
' 2>/dev/null) || exit 0
|
||||
[ -n "${CMD:-}" ] || exit 0
|
||||
|
||||
# heredoc 的 body 是資料不是指令(inkstone/ISEP#125 缺陷 D/E,2026-09-02 兩次實地命中):
|
||||
# 用 heredoc 寫一份「內含 `cd <目錄> && git checkout X` 示範」的說明文件,整條指令沒有任何
|
||||
# git 操作,卻被當成在切分支擋下——被擋的是「寫文件」這個動作。跟 github-contact-guard 一樣
|
||||
# 先把 body 剝掉再解析;剝不動就用原字串(fail-open 的是「這支 helper 有沒有跑」,不是判準)。
|
||||
CMD=$(printf '%s' "$CMD" | python3 "$(dirname "$0")/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||||
[ -n "${CMD:-}" ] || exit 0
|
||||
|
||||
CWD=$(printf '%s' "$PAYLOAD" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
@@ -96,6 +108,11 @@ EXPR=$(printf '%s' "$CMD" | python3 "$RESOLVER" 2>/dev/null) || exit 0
|
||||
[ -n "${EXPR:-}" ] || exit 0 # 沒有會移動 HEAD 的 checkout/switch
|
||||
[ "$EXPR" = "?" ] && exit 0 # 目錄解不出來(cd $VAR / cd -)⇒ 不猜,見該檔檔頭
|
||||
|
||||
# 出路:那條 checkout 自己帶著 `WORKTREE_OK=1` 前綴 ⇒ 放行(comment 6587)。
|
||||
# 看的是指令字串裡的位置,不是 hook 的環境——hook 的環境裡永遠不會有它。
|
||||
ESC=$(printf '%s' "$CMD" | python3 "$RESOLVER" --escape WORKTREE_OK=1 2>/dev/null) || ESC=""
|
||||
[ "${ESC:-}" = "1" ] && exit 0
|
||||
|
||||
TARGET=$(cd "$CWD" 2>/dev/null && cd "$EXPR" 2>/dev/null && pwd) || exit 0
|
||||
[ -n "${TARGET:-}" ] || exit 0
|
||||
|
||||
|
||||
@@ -213,6 +213,78 @@ else
|
||||
printf ' ❌ (59) lib/checkout_target_dir.py 不見了\n'; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "── E 群:出路要真的走得通(inkstone/ISEP#109 → comment 6587,2026-09-07 實撞)──"
|
||||
# 為什麼這群非有不可:閘訊息教的出路是 `WORKTREE_OK=1 git -C … checkout …`,
|
||||
# 而舊版讀的是 **hook 自己的環境變數**。PreToolUse hook 在指令執行之前跑,
|
||||
# 指令字串裡的 `WORKTREE_OK=1` 是給 git 的前綴賦值,hook 的行程裡根本沒有它
|
||||
# ⇒ 那條出路在 Claude Code 這個 harness 下永遠走不通(總管當天三次照貼三次被擋)。
|
||||
# 🔴 這群的測資是「閘自己印出來的那一行」,原樣餵回去——驗的是出路跑不跑得動,
|
||||
# 不是閘擋不擋(mistakes.md「閘擋對了,但它印出來的那條出路跑不動」)。
|
||||
# 判準是**位置**不是字:那個賦值必須是「會移動 HEAD 的那條 git 指令」自己的前綴。
|
||||
|
||||
t 0 "(60) 指令字串裡帶 WORKTREE_OK=1 前綴、hook 的環境裡**沒有**它 ⇒ 放行" \
|
||||
"WORKTREE_OK=1 git checkout -b feat/esc" "$REPO"
|
||||
t 2 "(61) 先擋一次,拿它印出來的出路當下一條的測資" "git checkout main" "$REPO"
|
||||
ESC_LINE=$(printf '%s' "$LAST_ERR" | grep -F 'WORKTREE_OK=1 git -C' | head -1 | sed 's/^[[:space:]]*//; s/<分支>/main/')
|
||||
N=$((N+1))
|
||||
if [ -n "$ESC_LINE" ]; then printf ' ✅ (62) 抽得出那一行:%s\n' "$ESC_LINE"; PASS=$((PASS+1))
|
||||
else printf ' ❌ (62) 訊息裡抽不出 WORKTREE_OK=1 git -C … 那一行\n'; FAIL=$((FAIL+1)); ESC_LINE="WORKTREE_OK=1 git -C $REPO checkout main"; fi
|
||||
t 0 "(63) 閘印出來的那一行**原樣餵回去**(只填分支、人站在別處)⇒ 放行——6587 那天走不通的就是這條" \
|
||||
"$ESC_LINE" "$PLAIN"
|
||||
t 2 "(64) 只是印了那個字:echo WORKTREE_OK=1; git checkout main ⇒ 照擋(看位置不看字)" \
|
||||
"echo WORKTREE_OK=1; git checkout main"
|
||||
t 2 "(65) 前綴掛在別的指令上:WORKTREE_OK=1 echo hi && git checkout main ⇒ 照擋" \
|
||||
"WORKTREE_OK=1 echo hi && git checkout main"
|
||||
t 2 "(66) 值不是 1:WORKTREE_OK=0 git checkout main ⇒ 照擋" \
|
||||
"WORKTREE_OK=0 git checkout main"
|
||||
t 2 "(67) 寫在 commit 訊息裡再切 ⇒ 照擋(引號裡的字不是前綴)" \
|
||||
"git commit -qm 'WORKTREE_OK=1 git checkout main' ; git checkout main"
|
||||
t 0 "(68) cd 進去再掛前綴:cd <repo> && WORKTREE_OK=1 git checkout main ⇒ 放行" \
|
||||
"cd $REPO && WORKTREE_OK=1 git checkout main" "$PLAIN"
|
||||
t 0 "(69) 在自己的 worktree 裡也掛了前綴 ⇒ 本來就放行,前綴不礙事" \
|
||||
"WORKTREE_OK=1 git checkout -b feat/x2" "$OWN"
|
||||
|
||||
echo
|
||||
echo "── F 群:inkstone/ISEP#125 驗收表 A–F(票上原文的六條,期望值照票)──"
|
||||
# A 逃生門原文照打→0 B 環境變數裡真的有→0 C 文件裡只寫 'git checkout X'→0
|
||||
# D 文件裡寫 'cd <目錄> && git checkout X'→0 E 同 D 角括號拿掉→0 F 真的要切→2(本體,退化成 0=閘廢了)
|
||||
t 0 "(70) #125-A 逃生門原文照打:WORKTREE_OK=1 git -C <TOP> checkout main ⇒ 0" \
|
||||
"WORKTREE_OK=1 git -C $REPO checkout main" "$PLAIN"
|
||||
t 0 "(71) #125-B 同一句、WORKTREE_OK 真的在 hook 的環境裡 ⇒ 0(本來就是,不要弄壞)" \
|
||||
"git -C $REPO checkout main" "$PLAIN" "CLAUDE_CODE_CHILD_SESSION=1 WORKTREE_OK=1"
|
||||
DOC_C="cat > $ROOT/doc-c.md <<'EOF'
|
||||
把 repo 複製一份,然後 git checkout X
|
||||
EOF
|
||||
echo written"
|
||||
t 0 "(72) #125-C 文件裡只寫 'git checkout X' ⇒ 0" "$DOC_C"
|
||||
DOC_D="cat > $ROOT/doc-d.md <<'EOF'
|
||||
示範:cd <目錄> && git checkout X
|
||||
EOF
|
||||
echo written"
|
||||
t 0 "(73) #125-D 文件裡寫 'cd <目錄> && git checkout X' ⇒ 0(寫文件不是執行;09-02 兩次實地命中)" "$DOC_D"
|
||||
DOC_E="cat > $ROOT/doc-e.md <<'EOF'
|
||||
示範:cd $REPO && git checkout feat/x
|
||||
EOF
|
||||
echo written"
|
||||
t 0 "(74) #125-E 同 D、角括號拿掉、目錄是真的共用目錄 ⇒ 0" "$DOC_E"
|
||||
t 2 "(75) #125-F 真的要切(對照組)⇒ 2 🔴 本體,退化成 0=閘廢了" "cd $REPO && git checkout feat/x" "$PLAIN"
|
||||
# 🔴 已知邊界,寫成測試而不是註解(跟 (53) 同一層、同一個病):tokenize 層把**換行**當空白吞掉,
|
||||
# 多行指令被併成一句 ⇒ `echo hi⏎git checkout main` 從第一版起就看不到(舊閘實測 exit=0,
|
||||
# 同一句改成 `echo hi; git checkout main` 就擋)。剝 heredoc 沒有讓它變好也沒有變壞——
|
||||
# body 剝掉之後留下的仍是「換行分隔的兩句」。要修得動那層,會連帶改到推 main 那道閘的偵測範圍
|
||||
# ⇒ 不在本票裡動(另報)。這條紅了=那層被改了,回頭看 (53) 與推 main 的 10+19 條。
|
||||
t 0 "(76) 已知邊界:heredoc 之後**換行**再真的切 ⇒ 這道閘看不到(漏擋,不是誤攔;(53) 同層)" \
|
||||
"cat > $ROOT/doc-f.md <<'EOF'
|
||||
只是文件
|
||||
EOF
|
||||
git checkout main"
|
||||
t 2 "(77) 同一句換成分號:cat <<'EOF' … EOF; git checkout main ⇒ 照擋(剝掉的只有 body)" \
|
||||
"cat > $ROOT/doc-g.md <<'EOF'
|
||||
只是文件
|
||||
EOF
|
||||
; git checkout main"
|
||||
|
||||
clean
|
||||
echo
|
||||
echo "══ $PASS/$N 通過${FAIL:+,$FAIL 個失敗} ══"
|
||||
|
||||
@@ -34,6 +34,43 @@ run 0 'python3 -c "requests.post(url, json={\"body\": \"quoting: git push origin
|
||||
run 0 'grep -n "gh api" hooks/github-contact-guard.sh'
|
||||
run 0 'grep -rn "git push" installer/scripts/line-source-repo.mjs'
|
||||
|
||||
echo "── 判「push 到哪」要看這條指令實際會推的那個 repo,不是 hook 站的 cwd(inkstone/ISEP#109 → comment 6629,2026-09-07 實撞)──"
|
||||
# 為什麼這群非有不可:雲端薄殼的 cwd 是 GitHub 那份(origin=github.com),而總管在
|
||||
# `InkStoneCo/`(origin=Gitea)推分支。舊版拿 **hook 收到的 cwd** 去解 `origin`,
|
||||
# 於是 `git -C <InkStoneCo> push origin x` 被判成「remote origin 指向 GitHub」擋下
|
||||
# ——連 `-C` 都沒看。arcrun-hand 同日在 Arcrun#176 comment 6614 撞到同一支。
|
||||
# 判準仍是唯一識別碼(remote 解出來的 URL 主機),不是名字;**不是「有 -C 就放行」**——
|
||||
# 下面「該擋」那組就是 -C/cd 指到 GitHub 那份時照擋的證據。
|
||||
W=$(mktemp -d "${TMPDIR:-/tmp}/isep-ghc.XXXXXX"); trap 'rm -rf "$W"' EXIT
|
||||
W=$(cd "$W" && pwd -P)
|
||||
mk(){ git init -q -b main "$1" && git -C "$1" remote add origin "$2"; }
|
||||
mk "$W/shell" https://github.com/example/shell.git # 薄殼:origin=GitHub
|
||||
mk "$W/body" https://git.example.invalid/inkstone/body.git # 真身:origin=Gitea 的形狀(主機不是 github.com)
|
||||
mkdir -p "$W/body/sub"
|
||||
runc(){ # $1=want $2=cwd $3=cmd —— 跟 run 一樣,只是 payload 帶 cwd(hook 真的會收到它)
|
||||
LAST_ERR=$(printf '%s' "{\"tool_name\":\"Bash\",\"cwd\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2"),\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$3")}}" \
|
||||
| bash "$H" 2>&1 >/dev/null)
|
||||
got=$?
|
||||
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||
printf 'want=%s got=%s cwd=%s %.60s\n' "$1" "$got" "${2#$W/}" "$3"
|
||||
}
|
||||
has(){ if printf '%s' "$LAST_ERR" | grep -qF -- "$1"; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi; printf '%s\n' "$2"; }
|
||||
|
||||
echo " · 該擋:這條指令實際推的那個 repo 的 origin 是 github.com"
|
||||
runc 2 "$W/shell" 'git push origin x'
|
||||
runc 2 "$W/body" "git -C $W/shell push origin x"
|
||||
has "$W/shell" " 訊息說得出是哪個目錄的 remote 指向 GitHub(人才知道自己站錯地方還是推錯地方)"
|
||||
runc 2 "$W/body" "cd $W/shell && git push origin x"
|
||||
runc 2 "$W/shell" "(cd $W/body && true); git push origin x"
|
||||
runc 2 "$W/shell" 'cd $SOMEWHERE && git push origin x'
|
||||
echo " · 不該擋:這條指令實際推的那個 repo 的 origin 不是 github.com(★=6629 那天被擋的形狀)"
|
||||
runc 0 "$W/shell" "git -C $W/body push origin x"
|
||||
runc 0 "$W/shell" "git -C $W/body push -u origin feat/x"
|
||||
runc 0 "$W/shell" "cd $W/body && git push origin x"
|
||||
runc 0 "$W/body/sub" 'git push origin x'
|
||||
runc 0 "$W/shell" "cd $W && git -C body push origin x"
|
||||
runc 0 "$W/body" "(cd $W/shell && true); git push origin x"
|
||||
|
||||
echo
|
||||
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
||||
@@ -373,3 +373,41 @@ leo 當場:「**這些為什麼不寫到票裡?**」
|
||||
正確做法: 匿名優先、匿名被拒(401/403/404)且環境拿得到 token 才帶 token 再問一次(`fetch_gitea()`);
|
||||
假伺服器要有 private 模式(匿名 404、帶 token 200)照真 Gitea 回;真跑兩條都跑(帶 token 讀到、拔 token 退回)。
|
||||
收到「X 讀得到/X 不存在」這種宣稱先打一次再動手——那是一個 curl 的成本。
|
||||
|
||||
## ⚠️ MISTAKE: 閘看的是 hook 自己的環境/cwd,不是指令要動的那個 repo
|
||||
|
||||
票: `inkstone/ISEP#109`(comment 6587/6629);`inkstone/ISEP#125` 是缺陷 A 與 D 的原票(09-02 開)
|
||||
日期: 2026-09-07
|
||||
|
||||
症狀: 同一天兩支閘同一個病。① `line-needs-own-worktree.sh` 印的出路
|
||||
`WORKTREE_OK=1 git -C … checkout …` 照貼三次三次被擋。
|
||||
② `github-contact-guard.sh` 把 `git -C <InkStoneCo> push origin x` 判成「remote origin 指向 GitHub」擋下
|
||||
(總管站在薄殼 cwd;InkStoneCo 的 origin 明明是 Gitea),連 `-C` 都沒看;
|
||||
同日 arcrun-hand 在 `Arcrun#176` comment 6614 撞到同一支。當天只能改用完整 Gitea 網址推,
|
||||
而那種形狀分類器又時過時不過。
|
||||
|
||||
實查: ① hook 讀的是 `$WORKTREE_OK` 這個**環境變數**。PreToolUse hook 跟指令不是同一個行程,
|
||||
指令字串裡的 `WORKTREE_OK=1` 是給 git 的前綴賦值,hook 的行程裡永遠沒有它
|
||||
⇒ 那條出路在 Claude Code 底下**結構上**走不通,不是偶爾。(`NOT_MY_BRANCH_OK` 同形狀,見下)
|
||||
② hook 拿 **payload 的 cwd** 去 `git remote get-url origin`,而 `-C`/`cd … &&` 改的是
|
||||
指令自己的目錄。`main-and-prod-push-guard.sh` 08-23 就為同一個病接了
|
||||
`lib/push_target_dir.py`(`inkstone/ISEP#30` comment 3949)——**同一族的另一支沒跟上**,
|
||||
跟 `leo21c-write-guard.sh` 那條坑一模一樣(修一份不會帶動另外兩份)。
|
||||
修之前拿新測試跑舊閘:① 3 條紅;② **7 條紅——4 條誤攔之外還有 3 條漏擋**
|
||||
(站在 Gitea 那份 `git -C <薄殼> push origin x` 舊版放行)。cwd 不只讓它誤擋,也讓 D20 有洞。
|
||||
|
||||
原因: 兩支都在回答「這條指令會動到哪個 repo」,卻拿「hook 自己站在哪」當答案。
|
||||
本機一份目錄時兩者恰好相等,所以測試全綠;雲端薄殼+真身兩份目錄一分開就露餡。
|
||||
而「出路走不走得通」那一格從來沒被測過——測的只有「擋不擋」。
|
||||
|
||||
正確做法:
|
||||
- 出路要是 hook **讀得到**的形狀:認指令字串裡的字面前綴(判準是位置——前綴必須掛在會移動 HEAD
|
||||
的那條 git 指令上;印個字、掛在別的指令上、寫在引號裡都不算),或 `gate-ok` 同款的戳記檔。
|
||||
- 「這條指令在哪個 repo 執行」一律走 `hooks/lib/push_target_dir.py`/`checkout_target_dir.py`
|
||||
解 cd 鏈與 `-C`,解不出來才退回 cwd(退回的方向是照舊,不是放寬)。改判準時 `grep -l` 找同一族
|
||||
(`main-and-prod-push-guard`/`github-contact-guard`/`line-needs-own-worktree`/`not-my-branch-guard`)一次看齊。
|
||||
- 測試要有一條「把閘印出來的那一行原樣餵回去」(A24 (61)–(63)),還要有 payload 帶 `cwd`、
|
||||
cwd 與目標 repo **不同**的案例(A39 後 12 條)——本機一份目錄的測試驗不出這個病。
|
||||
|
||||
📌 沒在本票修的同形狀:`not-my-branch-guard.sh` 的 `NOT_MY_BRANCH_OK=1 git commit …` 同樣只讀環境變數
|
||||
(第 28 行),沒實測、沒動——它認的動作是 commit,不是 push_target_dir 那條線的事,另報。
|
||||
|
||||
Reference in New Issue
Block a user