Compare commits

...

1 Commits

Author SHA1 Message Date
Leo 5d938f5d3a 票上回碼解的保險寫回專案根,出貨線讀得到(inkstone/ISEP#156)
gitea-arm-to-github-armed.sh 原本用腳本自己的上一層決定 .github-armed 寫在哪;
plugin 發佈後那是 ~/.claude/plugins/cache/inkstone/isep/<ver>/,
於是保險寫進快取、印「已解」、ship.mjs 與各閘讀不到(arcrun-rag#202 c7598)。

改寫到 gitea_arm_proj_dir(${CLAUDE_PROJECT_DIR:-$(pwd)}),與
main-and-prod-push-guard/github-contact-guard/stage-before-prod-guard 讀的位置、
以及 check 自己讀 token/待核請求的位置一致。擋下時印的 request 路徑改絕對路徑。

測試:hooks/tests/gitea-arm-to-github-armed.test.sh 18/18;修之前 11 條紅。
TESTING.md 補 A42。版本待總管定。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:58:50 +08:00
3 changed files with 179 additions and 4 deletions
+28
View File
@@ -1252,6 +1252,33 @@ bash scripts/test-isep-doctor-live.sh # 實機:真的 CLI 與 Desktop 內
- ⑨ 紅 ⇒ Desktop 版本用字串排序,`2.1.99` 會被當成比 `2.1.266` - ⑨ 紅 ⇒ Desktop 版本用字串排序,`2.1.99` 會被當成比 `2.1.266`
- 實機測試印 SKIP ⇒ 這台的版本不是 2.1.220/2.1.266,行為可能不同,那一格**沒驗過** - 實機測試印 SKIP ⇒ 這台的版本不是 2.1.220/2.1.266,行為可能不同,那一格**沒驗過**
### A42 — 票上回碼解的保險,寫在出貨線讀得到的地方:18 條
```
bash hooks/tests/gitea-arm-to-github-armed.test.sh .
```
**該看到**`通過 18 條,失敗 0 條`。**全程離線**:在 `mktemp` 裡造一個長得像
`~/.claude/plugins/cache/inkstone/isep/<ver>/` 的安裝位置與一個假的專案根,
「leo 已核准」由 stub 扮演;fail-closed 那組用**真的** `gitea-arm-check.sh`(沒有待核請求 → 本地就擋)。
**不碰真 InkStoneCo、不寫真的 `.github-armed`、不打 Gitea。**
(②a 會去讀 `InkStoneCo/products/arcrun-rag/installer/scripts/d20-guard.mjs``checkArmed`
找不到那個檔或沒有 node 時印 `⏭️ SKIP`,不算失敗,也不算驗過。)
**它在守什麼**`inkstone/ISEP#156`09-17 實撞於 `inkstone/arcrun-rag#202` c7598):
舊版用 `ROOT=$(dirname $0)/..` 決定保險寫在哪。plugin 發佈後腳本住在 plugin 快取裡
⇒ 保險寫進快取、腳本照印「✅ 保險已解」、出貨線讀 `InkStoneCo/.github-armed` 讀不到
⇒ leo 回了碼,出貨照樣卡住,總管手動搬檔才通。
現在寫在 `${CLAUDE_PROJECT_DIR:-$(pwd)}`——**跟每一個讀它的人同一個位置**。
📌 拿修之前的腳本跑是 **11 條紅**2026-09-17 實跑),不是換了寫法就會綠的測試。
**失敗**
- ① 的兩條 ★ 紅 ⇒ 保險又寫回腳本自己的位置(09-17 那個病)
- ② 任一 ★ 紅 ⇒ 檔案寫對地方了,但**讀的人不認得**(格式或位置跟出貨線/閘又分家了);
它們的「對照」紅 ⇒ ★ 那條沒有鑑別力
- ③ 紅 ⇒ 終端機(沒有 `CLAUDE_PROJECT_DIR`)時跟閘的退回規則不一致
- ④ 前兩條紅 ⇒ **沒拿到核准也寫了保險**——這是 D20 等級的洞,最嚴重
- ④ ★ 紅 ⇒ 擋下之後教的指令又印回相對路徑(站在 InkStoneCo 會叫到舊複本,`inkstone/ISEP#112` 同病)
--- ---
## B. 只有 leo 能跑的(雲端) ## B. 只有 leo 能跑的(雲端)
@@ -1381,6 +1408,7 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面
| **A39 D20 閘看 push 實際會推的 repo** | isep-hand | ✅ 26/262026-09-07inkstone/ISEP#109→6629)+真路徑實跑(薄殼 cwd:`-C InkStoneCo` 放行、裸 `git push origin` 擋)+舊閘實跑 7 條紅(4 誤攔+3 漏擋) | | **A39 D20 閘看 push 實際會推的 repo** | isep-hand | ✅ 26/262026-09-07inkstone/ISEP#109→6629)+真路徑實跑(薄殼 cwd:`-C InkStoneCo` 放行、裸 `git push origin` 擋)+舊閘實跑 7 條紅(4 誤攔+3 漏擋) |
| **A24 分身住 repo 裡面+收工被收(G/H 群)** | isep-hand | ✅ 110/1102026-09-07inkstone/ISEP#147)+舊閘實跑 20 條紅(訊息+G/H,B/D/E/F 全綠)+本線自己就是 dogfood(分身開在 `ISEP/.worktrees/ISEP-147`,主目錄 `git status` 乾淨) | | **A24 分身住 repo 裡面+收工被收(G/H 群)** | isep-hand | ✅ 110/1102026-09-07inkstone/ISEP#147)+舊閘實跑 20 條紅(訊息+G/H,B/D/E/F 全綠)+本線自己就是 dogfood(分身開在 `ISEP/.worktrees/ISEP-147`,主目錄 `git status` 乾淨) |
| **A40 sweep 現場整理(七個分身的 fixture)** | isep-hand | ✅ 42/422026-09-07inkstone/ISEP#147);**leo 的 Mac 現場**要他自己跑 `sweep ~/tech_projects`(先 dry-run)——這台機器碰不到 | | **A40 sweep 現場整理(七個分身的 fixture)** | isep-hand | ✅ 42/422026-09-07inkstone/ISEP#147);**leo 的 Mac 現場**要他自己跑 `sweep ~/tech_projects`(先 dry-run)——這台機器碰不到 |
| **A42 票上回碼的保險寫在出貨線讀得到的地方** | isep-hand | ✅ 18/182026-09-17inkstone/ISEP#156);修之前的腳本 11 條紅 |
| **A26 雲端憑證清單這台拿得到** | isep-hand | ✅ 11/112026-09-01inkstone/ISEP#115→5526554155425577)+兩條「該紅」方向實跑 | | **A26 雲端憑證清單這台拿得到** | isep-hand | ✅ 11/112026-09-01inkstone/ISEP#115→5526554155425577)+兩條「該紅」方向實跑 |
| **A16 放行的門真的打得開** | 總管 | ✅ 17/172026-08-28inkstone/ISEP#90 | | **A16 放行的門真的打得開** | 總管 | ✅ 17/172026-08-28inkstone/ISEP#90 |
| **A17 未推警察不誤攔雲端分支** | 總管 | ✅ 10/102026-08-28inkstone/ISEP#90 | | **A17 未推警察不誤攔雲端分支** | 總管 | ✅ 10/102026-08-28inkstone/ISEP#90 |
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# gitea-arm-to-github-armed.sh 的迴歸測試——**保險寫在哪,讀的人就要在哪讀得到**。
#
# 🔴 它在守什麼(inkstone/ISEP#1562026-09-17 實撞,inkstone/arcrun-rag#202):
# ISEP 以 plugin 形式裝在 `~/.claude/plugins/cache/inkstone/isep/<ver>/`
# 舊版用 `ROOT=$(dirname $0)/..` 決定 `.github-armed` 寫在哪
# ⇒ 保險被寫進 **plugin 快取資料夾**;而出貨線(ship.mjs → d20-guard.mjs
# 與每一支讀它的閘(main-and-prod-push-guardgithub-contact-guardstage-before-prod-guard
# 讀的都是**專案根**`${CLAUDE_PROJECT_DIR:-$(pwd)}`
# ⇒ leo 在票上回了碼、腳本印「✅ 保險已解」,出貨照樣卡住。
# **比單純失敗更糟:它對 leo 說謊。**
#
# 判準是「要求某個東西在場」:保險檔必須出現在讀的人會讀的那個位置,
# 並且**真的被那些讀的人認得**(不是只驗檔案存在)。
#
# 全程離線:`gitea-arm-check.sh` 的核准本身(Leo 真的回覆)必須打真的 Gitea,
# 那部分由 gitea-arm-check.test.sh 與 inkstone/InkStoneCo#34 的人工實測負責。
# 這裡把「核准成功」換成 stub,**只驗核准之後保險落在哪、被不被認得**;
# fail-closed 那條則用**真的** check(無待核請求 → 本地就擋,不碰網路)。
#
# 用法:bash hooks/tests/gitea-arm-to-github-armed.test.sh .
set -u
REPO="$(cd "${1:?用法: $0 <ISEP repo 根目錄>}" && pwd)"
SRC="$REPO/scripts/gitea-arm-to-github-armed.sh"
[ -f "$SRC" ] || { echo "❌ 找不到 $SRC(參數要傳 ISEP repo 根目錄)"; exit 1; }
T=$(mktemp -d)
trap '[ -n "${KEEP:-}" ] && echo "保留 $T" || rm -rf "$T"' EXIT
# ── 造一個長得像 plugin 快取的安裝位置,與一個長得像 InkStoneCo 的專案根 ──
PLUG="$T/.claude/plugins/cache/inkstone/isep/9.9.9"
PROJ="$T/InkStoneCo"
mkdir -p "$PLUG/scripts/lib" "$PROJ" "$T/elsewhere"
cp "$SRC" "$PLUG/scripts/"
cp "$REPO/scripts/lib/gitea-arm-common.sh" "$PLUG/scripts/lib/"
cp "$REPO/scripts/gitea-arm-request.sh" "$PLUG/scripts/" 2>/dev/null
# stub:扮演「leo 已在票上核准」——輸出格式照 gitea-arm-check.sh 成功時的 `ARMED: <mission>`
cat > "$PLUG/scripts/gitea-arm-check.sh" <<'STUB'
#!/bin/sh
[ "${1:-}" = "--consume" ] || { echo "stub: 應該帶 --consume" >&2; exit 1; }
echo "ARMED: 測試出貨 ISEP#156"
STUB
chmod +x "$PLUG/scripts/"*.sh
PASS=0; FAIL=0
ok(){ echo "$1"; PASS=$((PASS+1)); }
ng(){ echo "$1"; FAIL=$((FAIL+1)); }
chk(){ if eval "$2"; then ok "$1"; else ng "$1"; fi; }
echo "── ① 從 plugin 路徑執行,CLAUDE_PROJECT_DIR 指向專案根(總管 session 的實際形狀)──"
( cd "$T/elsewhere" && CLAUDE_PROJECT_DIR="$PROJ" sh "$PLUG/scripts/gitea-arm-to-github-armed.sh" ) >"$T/out1" 2>&1
rc=$?
chk "離開碼 0" '[ $rc -eq 0 ]'
chk "★ 專案根出現 .github-armed" '[ -f "$PROJ/.github-armed" ]'
chk "★ plugin 快取資料夾裡沒有 .github-armed09-17 實撞的位置)" '[ ! -e "$PLUG/.github-armed" ]'
chk "cwd 也沒有被寫(不是靠碰巧站對位置)" '[ ! -e "$T/elsewhere/.github-armed" ]'
chk "印出的「提前上保險」路徑就是真正寫下的那份" 'grep -qF "rm $PROJ/.github-armed" "$T/out1"'
chk "第 1 行是未來的到期 epoch" '[ "$(sed -n 1p "$PROJ/.github-armed")" -gt "$(date +%s)" ]'
chk "第 2 行帶任務說明" '[ "$(sed -n 2p "$PROJ/.github-armed")" = "測試出貨 ISEP#156" ]'
echo "── ② 讀的人真的認得這份保險(不是只驗檔案存在)──"
# ②a 出貨線:ship.mjs 的 prod --confirm preflight 呼叫的就是 d20-guard.mjs 的 checkArmed(INKSTONE_ROOT)
D20=""
for c in "${INKSTONE_ROOT:-}" "$REPO/../InkStoneCo" "$REPO/../../InkStoneCo" "$HOME/Documents/tech_projects/InkStoneCo"; do
[ -n "$c" ] && [ -f "$c/products/arcrun-rag/installer/scripts/d20-guard.mjs" ] \
&& { D20="$(cd "$c" && pwd)/products/arcrun-rag/installer/scripts/d20-guard.mjs"; break; }
done
if [ -n "$D20" ] && command -v node >/dev/null 2>&1; then
node --input-type=module -e "
const { checkArmed } = await import(process.argv[1]);
const r = checkArmed(process.argv[2]);
console.log('checkArmed OK mission=' + r.mission);
" "$D20" "$PROJ" >"$T/out2" 2>&1
rc=$?
chk "★ ship.mjs 的 checkArmed(專案根) 認得($(head -1 "$T/out2")" '[ $rc -eq 0 ]'
node --input-type=module -e "
const { checkArmed } = await import(process.argv[1]);
checkArmed(process.argv[2]);
" "$D20" "$PLUG" >/dev/null 2>&1
rc=$?
chk "對照:checkArmed(plugin 快取) 讀不到 ⇒ 上一條有鑑別力" '[ $rc -ne 0 ]'
else
echo " ⏭️ SKIP 找不到 arcrun-rag 的 d20-guard.mjs 或 node(不算失敗,也不算驗過)"
fi
# ②b 閘:同一個位置(`${CLAUDE_PROJECT_DIR:-$(pwd)}/.github-armed`
mk(){ python3 -c "import json,sys;print(json.dumps({'tool_name':'Bash','tool_input':{'command':sys.argv[1]},'cwd':sys.argv[2]}))" "$1" "$T/elsewhere"; }
gate(){ # gate <hook> <cmd> <CLAUDE_PROJECT_DIR> → 回 hook 離開碼
mk "$2" | ( cd "$T/elsewhere" && env -u CLAUDE_CODE_CHILD_SESSION CLAUDE_PROJECT_DIR="$3" bash "$1" ) >/dev/null 2>&1
}
# 閘會寫 github-contact-log/狀態目錄——一律寫進暫存的專案根,不碰真的 InkStoneCo
mkdir -p "$PROJ/system-dev/docs/3-specs/autonomy-dispatch" "$T/empty/system-dev/docs/3-specs/autonomy-dispatch"
WR="npx wrangler deploy"
GH="git push https://github.com/example/example.git main"
gate "$REPO/hooks/main-and-prod-push-guard.sh" "$WR" "$PROJ"; rc=$?
chk "★ main-and-prod-push-guard:保險在專案根 ⇒ wrangler deploy 放行" '[ $rc -ne 2 ]'
gate "$REPO/hooks/main-and-prod-push-guard.sh" "$WR" "$T/empty"; rc=$?
chk "對照:沒有保險 ⇒ 同一條被擋" '[ $rc -eq 2 ]'
gate "$REPO/hooks/github-contact-guard.sh" "$GH" "$PROJ"; rc=$?
chk "★ github-contact-guard:保險在專案根 ⇒ push github 放行" '[ $rc -ne 2 ]'
gate "$REPO/hooks/github-contact-guard.sh" "$GH" "$T/empty"; rc=$?
chk "對照:沒有保險 ⇒ 同一條被擋" '[ $rc -eq 2 ]'
echo "── ③ 沒設 CLAUDE_PROJECT_DIRleo 在終端機)⇒ 跟閘一樣退回 cwd ──"
rm -f "$PROJ/.github-armed"
( cd "$PROJ" && env -u CLAUDE_PROJECT_DIR sh "$PLUG/scripts/gitea-arm-to-github-armed.sh" ) >"$T/out3" 2>&1
rc=$?
chk "離開碼 0" '[ $rc -eq 0 ]'
chk "寫在 cwd(專案根),不在 plugin 快取" '[ -f "$PROJ/.github-armed" ] && [ ! -e "$PLUG/.github-armed" ]'
echo "── ④ fail-closed:沒拿到核准就一個檔都不寫(用真的 check,不碰網路)──"
rm -f "$PROJ/.github-armed" "$PLUG/.github-armed"
cp "$REPO/scripts/gitea-arm-check.sh" "$PLUG/scripts/gitea-arm-check.sh"
printf 'GITEA_TOKEN_CLAUDE_CODE=test-token-not-real\n' > "$PROJ/.env"
( cd "$T/elsewhere" && CLAUDE_PROJECT_DIR="$PROJ" sh "$PLUG/scripts/gitea-arm-to-github-armed.sh" ) >"$T/out4" 2>&1
rc=$?
chk "沒有待核請求 ⇒ 離開碼非 0" '[ $rc -ne 0 ]'
chk "專案根與 plugin 快取都沒有 .github-armed" '[ ! -e "$PROJ/.github-armed" ] && [ ! -e "$PLUG/.github-armed" ]'
# 出路要走得通:訊息印的那支 request 腳本必須是真的存在的絕對路徑
# (相對的 `scripts/…` 會對貼上那個人的 cwd 解析,站在 InkStoneCo 會叫到舊複本——ISEP#112 同一個病)
# 取「本檔自己印的」那一行(⛔ 之後);中文冒號不是空白,所以用「⛔ 之後第一個以 / 開頭的 token」
REQ=$(sed -n '/⛔/,$p' "$T/out4" | grep -oE '[^[:space:]]*gitea-arm-request\.sh' | grep '^/' | head -1)
chk "★ 出路印的是存在的絕對路徑(印出:${REQ:-}" '[ -n "$REQ" ] && [ -f "$REQ" ]'
echo
echo "通過 $PASS 條,失敗 $FAIL"
[ "$FAIL" -eq 0 ]
+24 -4
View File
@@ -21,17 +21,36 @@
# ⇒ 拿掉那個條件,這支就變成「AI 自己批准自己發佈」——**那正是 D20 要擋的事**。 # ⇒ 拿掉那個條件,這支就變成「AI 自己批准自己發佈」——**那正是 D20 要擋的事**。
set -eu set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ARMED="$ROOT/.github-armed" # shellcheck source=lib/gitea-arm-common.sh
. "$SCRIPT_DIR/lib/gitea-arm-common.sh"
# ── 保險寫在哪:**讀的人在哪讀,就寫在哪**inkstone/ISEP#1562026-09-17 實撞)──────
#
# 🔴 舊版寫 `ROOT=$(dirname $0)/..`(腳本自己的上一層)。ISEP 還住在 InkStoneCo 裡時兩者剛好
# 同一個位置;**改成 plugin 發佈後,腳本住在 `~/.claude/plugins/cache/inkstone/isep/<ver>/`**
# ⇒ 保險被寫進 plugin 快取資料夾,印「✅ 保險已解」,而出貨線與每一道閘都讀不到
# ⇒ leo 回了碼、出貨照樣卡住,總管手動把檔案搬過去才通(inkstone/arcrun-rag#202 c7598)。
# **一句「已解」卻沒解,比單純失敗更糟——它讓 leo 以為自己的動作生效了。**
#
# ⇒ 改用 `gitea_arm_proj_dir`(=`${CLAUDE_PROJECT_DIR:-$(pwd)}`),理由是**跟讀的人對齊**:
# · main-and-prod-push-guard.shgithub-contact-guard.shstage-before-prod-guard.sh
# 全部讀 `${CLAUDE_PROJECT_DIR:-$(pwd)}/.github-armed`
# · ship.mjs 讀 `InkStoneCo/.github-armed`(總管 session 的 CLAUDE_PROJECT_DIR 就是它)
# · 本檔自己的核准來源(`gitea-arm-check.sh` 讀的 token 與待核請求)本來就住在這裡
# ⇒ 專案根猜錯時 check 會先因為讀不到 `.env`/待核請求而 fail-closed**不會寫到錯的地方**
# 不要再改回「腳本的位置」:腳本住哪是安裝方式決定的,保險住哪是讀的人決定的,兩者無關。
ARMED="$(gitea_arm_proj_dir)/.github-armed"
# ── 唯一的授權來源:leo 在 Gitea 票上的回覆 ──────────────────────────── # ── 唯一的授權來源:leo 在 Gitea 票上的回覆 ────────────────────────────
# 不捕捉、不吞錯——check 失敗就讓它把原因印到 stderr 並中止本支。 # 不捕捉、不吞錯——check 失敗就讓它把原因印到 stderr 並中止本支。
# 🔴 `--consume` 是必要的(2026-08-16 起 check 預設唯讀):本檔會**寫出保險**, # 🔴 `--consume` 是必要的(2026-08-16 起 check 預設唯讀):本檔會**寫出保險**,
# 那就是「放行」本身 ⇒ 這一次核准必須當場作廢,否則同一組碼能重複解保險。 # 那就是「放行」本身 ⇒ 這一次核准必須當場作廢,否則同一組碼能重複解保險。
OUT=$("$ROOT/scripts/gitea-arm-check.sh" --consume) || { OUT=$("$SCRIPT_DIR/gitea-arm-check.sh" --consume) || {
echo "" >&2 echo "" >&2
echo "⛔ 沒有取得 leo 的核准 ⇒ 不寫保險(fail-closed" >&2 echo "⛔ 沒有取得 leo 的核准 ⇒ 不寫保險(fail-closed" >&2
echo " 先跑:scripts/gitea-arm-request.sh <票號> \"<要做什麼>\",把代碼給 leo 回在那張票上。" >&2 # 印絕對路徑:相對的 `scripts/…` 會對貼上那個人的 cwd 解析,站在 InkStoneCo 會叫到舊複本(ISEP#112 同病)
echo " 先跑:$SCRIPT_DIR/gitea-arm-request.sh <票號> \"<要做什麼>\",把代碼給 leo 回在那張票上。" >&2
exit 1 exit 1
} }
@@ -50,6 +69,7 @@ printf '%s\n%s\n%s\n' \
> "$ARMED" > "$ARMED"
echo "✅ 保險已解(來源:leo 在 Gitea 票上的核准)" echo "✅ 保險已解(來源:leo 在 Gitea 票上的核准)"
echo " 寫在:$ARMED"
echo " 任務:$MISSION" echo " 任務:$MISSION"
echo " 有效至:$(date -r "$EXPIRY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$EXPIRY")" echo " 有效至:$(date -r "$EXPIRY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$EXPIRY")"
echo " 提前上保險:rm $ARMED" echo " 提前上保險:rm $ARMED"