Compare commits

...

6 Commits

Author SHA1 Message Date
Leo 2cd3af2764 開票的側門也封上:Gitea API 直接開票要有搜尋戳記
leo 2026-08-20 問「如何防止」。

規範早就有(leo 2026-08-16「開票前先去搜尋要開在哪裡」),閘也早就有——
但它長在 scripts/ticket 這支工具裡,而總管當天走 API 這條側門,
一口氣開了 12 張與舊票重疊的票,事後全部只能關掉指回舊票。
⇒ 同一個動作有兩條路,只封了一條。

本支封的是動作:Bash 指令只要打到 issues 集合端點且帶 POST,就要有新鮮的搜尋戳記
(與 scripts/ticket 共用 /tmp/.ticket-where-ok,30 分鐘)。

判準刻意簡單:端點 + 一個裸字 POST。原本寫成逐個比對 -X POST/"POST"/req("POST",
實測第二種就因為跳脫引號逃掉——那正是 leo 08-17 說的「自然語言的變體無限,
blacklist 追不完」。端點已經確定是開票那條,純讀取不會出現 POST,一個字就夠。

實測 13 條:4 種該擋全擋、8 種不該擋(GET/留言/改標籤/關票/scripts\/ticket/
文字提到/grep 自己/撈清單)全放行、有戳記時放行。
2026-08-20 16:43:49 +08:00
claude-code 2fb18e9f48 Merge pull request '治理 M4.7:降 scope 必須留痕在里程碑描述上' (#25) from leaf/24-descope into main
治理 M4.7
2026-08-20 08:33:28 +00:00
Leo aec7f3a980 治理 M4.7:把票移出里程碑必須寫進里程碑描述(closes #24 的規範面)
leo 2026-08-20 抓到:v0.2.0 顯示 100%,但那是把 #5 移出去之後的 100%,
畫面上看不出降 scope 發生過。

與 §3.4(審核完沒關票 ⇒ 數字偏低)是同一個病的兩面——
畫面上的數字不等於實際狀態,而 leo 只看得到畫面。

禁的不是降 scope(卡人閘時降 scope 是 M4.3 要的),禁的是降得無聲無息。
痕跡要留在他會經過的地方=里程碑描述,不是票裡、不是對話裡。
2026-08-20 16:33:08 +08:00
claude-code 34e7c45eb4 Merge pull request 'release: v0.2.0' (#23) from release/v0.2.0 into main
release v0.2.0 前置
2026-08-20 06:12:38 +00:00
Leo 500b95d80d release: v0.2.0(版本號、author、描述與實際內容對齊)
plugin.json / marketplace.json 的 version 與 author 同步;描述改成實際清點的數字
(42 支閘 52 條註冊、7 command、2 skill、25 腳本、治理規範、標籤真相源)。
claude plugin validate 由「1 warning」變成完全通過。

milestone inkstone/ISEP v0.2.0 已 5/5 關閉;#5 因卡在只有 leo 能做的
Cloud environment 設定(#21),照 M4.3 降 scope 移出本版。
2026-08-20 14:12:34 +08:00
claude-code 7b96f238b6 Merge pull request '雲端載入契約:機制與文件(refs #5,尚未啟用)' (#22) from leaf/5-cloud into main
審核通過:雲端載入機制(refs #5,成果待 #21)
2026-08-20 05:42:35 +00:00
5 changed files with 117 additions and 6 deletions
+8 -3
View File
@@ -2,12 +2,17 @@
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "inkstone", "name": "inkstone",
"description": "InkStoneCo 自用的 Claude Code 環境", "description": "InkStoneCo 自用的 Claude Code 環境",
"owner": { "name": "Leo" }, "owner": {
"name": "Leo"
},
"plugins": [ "plugins": [
{ {
"name": "isep", "name": "isep",
"description": "InkStone Environment Plugin —— 機械閘/command/skill/腳本的唯一真相源", "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:42 支機械閘(52 條註冊)、7 支 slash command、2 支 skill、25 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"author": { "name": "Leo" }, "author": {
"name": "Leo",
"url": "https://uncle6.me"
},
"category": "productivity", "category": "productivity",
"source": "./" "source": "./"
} }
+13 -3
View File
@@ -1,6 +1,16 @@
{ {
"name": "isep", "name": "isep",
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:42 支機械閘、7 支 slash command、2 支 skill、24 支腳本。本機與雲端裝同一份。", "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:42 支機械閘52 條註冊)、7 支 slash command、2 支 skill、25 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"version": "0.0.0", "version": "0.2.0",
"keywords": ["inkstone", "guardrails", "hooks", "gitea", "arcrun"] "keywords": [
"inkstone",
"guardrails",
"hooks",
"gitea",
"arcrun"
],
"author": {
"name": "Leo",
"url": "https://uncle6.me"
}
} }
+11
View File
@@ -121,6 +121,17 @@ s/triage ──驗傷通過──▶ s/backlog ──排進 milestone──▶ s
🔴 自動打 tag 會製造假交付——tag 永遠只在「驗過了」之後發生。 🔴 自動打 tag 會製造假交付——tag 永遠只在「驗過了」之後發生。
- **M4.4** Milestone 關閉 release tag,一對一。「已交付」唯一合法形式是 **tag 存在且裝得起來**;打 tag 前置 open issues 0(§8 E12)。 - **M4.4** Milestone 關閉 release tag,一對一。「已交付」唯一合法形式是 **tag 存在且裝得起來**;打 tag 前置 open issues 0(§8 E12)。
- **M4.5** Description 只寫版本目標一句 tracking 連結。討論回 tracking issue。 - **M4.5** Description 只寫版本目標一句 tracking 連結。討論回 tracking issue。
- **M4.7(降 scope 必須留痕)** 🔴 **把票移出里程碑,必須同時寫進該里程碑的 description。**
格式:原本幾張、移走哪幾張、為什麼、移去哪裡。
- **禁的不是降 scope**——卡人閘時降 scope 就是 M4.3 要的。禁的是**降得無聲無息**。
- 為什麼寫在 description 不是寫在票裡:**leo 看的是百分比那個畫面**,
他不會為了確認 100% 是不是真的而去逐張點票。**痕跡要留在他會經過的地方。**
- 來由(2026-08-20 實犯):`v0.2.0` 本來 6 張,`#5` 卡人閘被移出 ⇒ 分母 6 變 5 ⇒ 顯示 100%。
leo 當場問「為什麼還有很多 issues 開放中」。
🔴 這與 §3.4(審核完沒關票 ⇒ 數字偏低)是**同一個病的兩面**:
畫面上的數字不等於實際狀態,而 leo 只看得到畫面。
- 機械閘:`inkstone/ISEP#24`
- **M4.6** 🔴 **release note 寫在 Gitea Releases 裡,不寫在 README。**leo 2026-08-20:「release 不是寫在 readme,要放在 release 裡」) - **M4.6** 🔴 **release note 寫在 Gitea Releases 裡,不寫在 README。**leo 2026-08-20:「release 不是寫在 readme,要放在 release 裡」)
README 不得自行宣稱版本號——那會產生第二份會漂移的版本真相。 README 不得自行宣稱版本號——那會產生第二份會漂移的版本真相。
+4
View File
@@ -36,6 +36,10 @@
{ {
"type": "command", "type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/release-tag-guard.sh" "command": "${CLAUDE_PLUGIN_ROOT}/hooks/release-tag-guard.sh"
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/ticket-api-bypass-guard.sh"
} }
] ]
}, },
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# ticket-api-bypass-guard.sh — 開票的「側門」也要經過同一道搜尋閘
#
# 來由(leo 2026-08-20 當場問「如何防止」):
# `scripts/ticket new` 早就強制「開票前先搜」(/tmp/.ticket-where-ok 戳記,30 分鐘失效)。
# 但總管當天開了 12 張與舊票重疊的新票——因為他**沒用那支工具,直接打 Gitea API**。
# ⇒ 規範有、閘也有,但閘長在「工具」上,而那個動作有兩條路,只封了一條。
# leo:「你在讓事情複雜化」/「每張票開以前都要搜尋現有票,你為什麼會開了不搜?」
#
# 這支封的是**動作**:任何 Bash 指令只要在對 Gitea 的 issues 端點做寫入,
# 就要有一個新鮮的搜尋戳記。它不強迫你用 scripts/ticket,只強迫你搜過。
# (同 InkStoneCo#36:「守 prod 的閘,包一層腳本就繞過去了——它看的是指令長相」。
# 本支同樣只看得到指令文字,這是 PreToolUse 這層的天花板;
# 所以判準取「端點 + 寫入動詞」兩個都命中才擋,讓純讀取一律放行。)
#
# 放行(刻意,這些都不是「開票」):
# - 只讀不寫(GET):撈清單、看票、對帳
# - 對既有票的留言/改標籤/關票(/issues/<N>/... 這種帶票號的子路徑)
# - scripts/ticket 自己(它有自己的閘,重複擋只會互相打架)
# - 指令裡出現 ticket-api-ok(逃生口,會留在指令歷史上)
set -uo pipefail
INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | python3 -c "
import sys,json
try: print(json.load(sys.stdin).get('tool_input',{}).get('command',''))
except Exception: print('')
" 2>/dev/null)
[ -n "$CMD" ] || exit 0
# 逃生口(留痕)
case "$CMD" in *ticket-api-ok*) exit 0 ;; esac
# scripts/ticket 有自己的閘
case "$CMD" in *scripts/ticket*|*"ticket where"*|*"ticket new"*|*"ticket say"*) exit 0 ;; esac
# ① 有沒有打到 Gitea 的 issues 端點(不帶票號的那個=建立新票的路徑)
printf '%s' "$CMD" | grep -qE 'repos/[^ "'"'"']*/issues([?"'"'"'`,)\\[:space:]]|$)' || exit 0
# ② 指令裡有沒有 POST 這個詞(純 GET 一律放行)
# 刻意只認一個裸字:跳脫引號、heredoc、python、curl、各種包裝的寫法無限多,
# 逐個補 pattern 追不完(leo 2026-08-17:「自然語言的變體是無限的,blacklist 永遠追不完」)。
# ①已經確定這是「開票那條端點」,純讀取的指令不會出現 POST ⇒ 一個字就夠,而且沒有跳脫的破口。
printf '%s' "$CMD" | grep -qw 'POST' || exit 0
# ③ 要有新鮮的搜尋戳記(與 scripts/ticket 共用同一個,30 分鐘)
STAMP=/tmp/.ticket-where-ok
NOW=$(date +%s)
FRESH=no
if [ -f "$STAMP" ]; then
AT=$(python3 -c "import json;print(int(json.load(open('$STAMP'))['at']))" 2>/dev/null || echo 0)
[ $((NOW - AT)) -le 1800 ] && FRESH=yes
fi
[ "$FRESH" = "yes" ] && exit 0
cat >&2 <<'MSG'
🚫 你正在用 Gitea API 直接開新票,而這一輪沒有搜尋紀錄
leo 2026-08-16:「**寫開票前先去搜尋要開在哪裡,不然你永遠會亂開新票**」
leo 2026-08-20:「**每張票開以前都要搜尋現有票,你為什麼會開了不搜?這個規範不是早就有 hook 了?**」
規範有,閘也有——但那道閘長在 `scripts/ticket` 這支工具裡,
而你走的是 API 這條側門。**本閘就是把那道門也封上。**
實錯(2026-08-20 同日):總管用 API 開了 12 張票,事後盤點**每一張都跟舊票重疊**,
全部只能關掉指回舊票。leo:「**你在讓事情複雜化**」。
── 怎麼過(擇一)───────────────────────────────
1. 先搜(預設,戳記 30 分鐘有效,之後 API 也放行):
scripts/ticket where <關鍵字...>
🔴 搜到了就**貼進那張票**,不要開新的:
scripts/ticket say <owner/repo#N> -F <內文檔>
2. 真的是新的一條線 → 直接用那支工具開,它會幫你把該檢查的檢查完:
scripts/ticket new <repo> -F <內文檔> --title <標題>
3. 這次確實不是在開新票(例如批次改標籤/關票/留言)
→ 指令裡加 `ticket-api-ok` 說明理由,留痕放行。
放行的情況(本閘不管):純讀取(GET)、對既有票 /issues/<N>/ 的留言與標籤、scripts/ticket 自己。
MSG
exit 2