Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d938f5d3a |
@@ -1252,6 +1252,33 @@ bash scripts/test-isep-doctor-live.sh # 實機:真的 CLI 與 Desktop 內
|
||||
- ⑨ 紅 ⇒ Desktop 版本用字串排序,`2.1.99` 會被當成比 `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 能跑的(雲端)
|
||||
@@ -1381,6 +1408,7 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面
|
||||
| **A39 D20 閘看 push 實際會推的 repo** | isep-hand | ✅ 26/26(2026-09-07,inkstone/ISEP#109→6629)+真路徑實跑(薄殼 cwd:`-C InkStoneCo` 放行、裸 `git push origin` 擋)+舊閘實跑 7 條紅(4 誤攔+3 漏擋) |
|
||||
| **A24 分身住 repo 裡面+收工被收(G/H 群)** | isep-hand | ✅ 110/110(2026-09-07,inkstone/ISEP#147)+舊閘實跑 20 條紅(訊息+G/H,B/D/E/F 全綠)+本線自己就是 dogfood(分身開在 `ISEP/.worktrees/ISEP-147`,主目錄 `git status` 乾淨) |
|
||||
| **A40 sweep 現場整理(七個分身的 fixture)** | isep-hand | ✅ 42/42(2026-09-07,inkstone/ISEP#147);**leo 的 Mac 現場**要他自己跑 `sweep ~/tech_projects`(先 dry-run)——這台機器碰不到 |
|
||||
| **A42 票上回碼的保險寫在出貨線讀得到的地方** | isep-hand | ✅ 18/18(2026-09-17,inkstone/ISEP#156);修之前的腳本 11 條紅 |
|
||||
| **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) |
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# gitea-arm-to-github-armed.sh 的迴歸測試——**保險寫在哪,讀的人就要在哪讀得到**。
|
||||
#
|
||||
# 🔴 它在守什麼(inkstone/ISEP#156,2026-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-guard/github-contact-guard/stage-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-armed(09-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_DIR(leo 在終端機)⇒ 跟閘一樣退回 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 ]
|
||||
@@ -21,17 +21,36 @@
|
||||
# ⇒ 拿掉那個條件,這支就變成「AI 自己批准自己發佈」——**那正是 D20 要擋的事**。
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ARMED="$ROOT/.github-armed"
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
# shellcheck source=lib/gitea-arm-common.sh
|
||||
. "$SCRIPT_DIR/lib/gitea-arm-common.sh"
|
||||
|
||||
# ── 保險寫在哪:**讀的人在哪讀,就寫在哪**(inkstone/ISEP#156,2026-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.sh/github-contact-guard.sh/stage-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 票上的回覆 ────────────────────────────
|
||||
# 不捕捉、不吞錯——check 失敗就讓它把原因印到 stderr 並中止本支。
|
||||
# 🔴 `--consume` 是必要的(2026-08-16 起 check 預設唯讀):本檔會**寫出保險**,
|
||||
# 那就是「放行」本身 ⇒ 這一次核准必須當場作廢,否則同一組碼能重複解保險。
|
||||
OUT=$("$ROOT/scripts/gitea-arm-check.sh" --consume) || {
|
||||
OUT=$("$SCRIPT_DIR/gitea-arm-check.sh" --consume) || {
|
||||
echo "" >&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
|
||||
}
|
||||
|
||||
@@ -50,6 +69,7 @@ printf '%s\n%s\n%s\n' \
|
||||
> "$ARMED"
|
||||
|
||||
echo "✅ 保險已解(來源:leo 在 Gitea 票上的核准)"
|
||||
echo " 寫在:$ARMED"
|
||||
echo " 任務:$MISSION"
|
||||
echo " 有效至:$(date -r "$EXPIRY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$EXPIRY")"
|
||||
echo " 提前上保險:rm $ARMED"
|
||||
|
||||
Reference in New Issue
Block a user