ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份

leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
              「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
                你自己可以 dogfooding」

搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。

51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。

尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 11:41:46 +08:00
commit c2638668e3
85 changed files with 11879 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
#!/bin/bash
# gitea-arm-request.sh — 開一個「等 leo 在 Gitea 上回覆」的請求
#
# 用法:scripts/gitea-arm-request.sh <票號> "任務描述" [有效分鐘,預設30,上限60]
#
# 這支**machine 自己就能跑**(不像 scripts/github-arm.sh 要求終端機互動)——
# 因為「請求」本身不是安全邊界,**leo 用 Leo 帳號回覆才是**。
# 這支只負責:生一組一次性代碼、貼上呼叫端指定的那張 Gitea 票、把代碼與人話訊息印出來
# (讓呼叫者拿去用 notify_leoTelegram 轉告 leo;本支不碰 Telegram)。
#
# 🪦 2026-08-16 起票號不再寫死(原本固定貼 inkstone/InkStoneCo#34「頻道票」)——
# leo:「我不要把所有的票都放在一個 issues,不然就難 track 歷史記錄」。
# **解哪張票的保險,就把請求貼在那張票上**:要出貨 X 就傳 X 那張票的號碼,
# 不要再統一貼 #34(#34 保留 open 當歷史,見 issues/34#issuecomment-2804)。
#
# 之後由 scripts/gitea-arm-check.sh 核對 leo 有沒有在同一張票回覆同一組代碼。
set -euo pipefail
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
# shellcheck source=lib/gitea-arm-common.sh
. "$SCRIPT_DIR/lib/gitea-arm-common.sh"
ISSUE="${1:-}"
MISSION="${2:-}"
MINUTES="${3:-30}"
if [ -z "$ISSUE" ] || [ -z "$MISSION" ]; then
echo "用法:scripts/gitea-arm-request.sh <票號> \"任務描述\" [分鐘,預設30,上限60]" >&2
echo "例:scripts/gitea-arm-request.sh 41 \"出 prodxxx\"" >&2
exit 1
fi
# 票號可寫 `N`(預設 InkStoneCo)或 `repo#N`(同 org 的別的 repo,例:arcrun-rag#115
case "$ISSUE" in
*"#"*)
_R="${ISSUE%%#*}"; ISSUE="${ISSUE##*#}"
gitea_arm_set_repo "$_R" || exit 1;;
esac
if ! gitea_arm_valid_issue "$ISSUE"; then
echo "❌ 票號要是純數字,或 repo#N(收到:$ISSUE)——解哪張票的保險就填那張票" >&2
exit 1
fi
case "$MINUTES" in
''|*[!0-9]*)
echo "❌ 第三個參數要純數字分鐘(收到:$MINUTES" >&2
exit 1;;
esac
[ "$MINUTES" -le 60 ] || MINUTES=60
[ "$MINUTES" -ge 1 ] || MINUTES=1
TOKEN=$(gitea_arm_token) || { echo "❌ 讀不到 GITEA_TOKEN_CLAUDE_CODE(頂層 .env" >&2; exit 1; }
NONCE="ARM-$(python3 -c 'import secrets; print(secrets.token_hex(4))')"
NOW=$(date +%s)
EXPIRES=$((NOW + MINUTES * 60))
EXPIRES_HUMAN=$(date -r "$EXPIRES" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -d "@$EXPIRES" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$EXPIRES")
STATE_DIR=$(gitea_arm_state_dir)
PENDING_FILE="$STATE_DIR/pending/${NONCE}.json"
# ── 貼留言到呼叫端指定的那張票 ─────────────────────────────────────────
COMMENT_BODY=$(MISSION="$MISSION" NONCE="$NONCE" EXPIRES_HUMAN="$EXPIRES_HUMAN" MINUTES="$MINUTES" python3 -c '
import os
mission = os.environ["MISSION"]
nonce = os.environ["NONCE"]
expires_human = os.environ["EXPIRES_HUMAN"]
minutes = os.environ["MINUTES"]
print(f"""🔐 ARM 請求
任務:{mission}
有效時限:{minutes} 分鐘內({expires_human} 前)
📱 回這則、貼上下面這串就解鎖這一次(用完就失效,別的請求不能借用):
{nonce}
""")
')
REQUEST_JSON=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')
RESP=$(curl -s -w '\n%{http_code}' -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$REQUEST_JSON" \
"$GITEA_ARM_API/repos/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE/comments")
HTTP_CODE=$(printf '%s' "$RESP" | tail -1)
RESP_BODY=$(printf '%s' "$RESP" | sed '$d')
if [ "$HTTP_CODE" != "201" ]; then
echo "❌ 貼留言失敗(HTTP $HTTP_CODE),沒有建立請求——票號 #$ISSUE 存在嗎?" >&2
echo "$RESP_BODY" >&2
exit 1
fi
COMMENT_ID=$(printf '%s' "$RESP_BODY" | jq -r '.id // empty')
CREATED_AT=$(printf '%s' "$RESP_BODY" | jq -r '.created_at // empty')
if [ -z "$COMMENT_ID" ] || [ -z "$CREATED_AT" ]; then
echo "❌ Gitea 回應解不出留言 id/時間,視為失敗(fail-closed,沒寫本地狀態)" >&2
exit 1
fi
jq -n \
--arg nonce "$NONCE" \
--arg mission "$MISSION" \
--arg issue "$ISSUE" \
--arg repo "$GITEA_ARM_REPO" \
--argjson requested_at "$NOW" \
--argjson expires_at "$EXPIRES" \
--arg request_comment_id "$COMMENT_ID" \
--arg request_created_at "$CREATED_AT" \
'{nonce:$nonce, mission:$mission, issue:$issue, repo:$repo, requested_at:$requested_at, expires_at:$expires_at,
request_comment_id:$request_comment_id, request_created_at:$request_created_at}' \
> "$PENDING_FILE"
# ── 貼完請求就指派給 leo 掛 Humanleo 2026-08-16:「沒有指派,沒有加 Human」)──
#
# 為什麼要自動:總管同一天才立下「指派給 leo 的每一張都要真的在等他」,
# 貼完 ARM 就忘了做 ⇒ leo 的「指派給您的」清單漏掉那張真正在等他的票。
# ⇒ 這件事不該靠人記得:**貼出 ARM 請求 = 這張票此刻在等 leo**,兩者是同一件事。
_LBL=$(curl -s -H "Authorization: token $TOKEN" \
"$GITEA_ARM_API/repos/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/labels?limit=60" \
| jq -r '.[] | select(.name=="Human") | .id' 2>/dev/null | head -1)
_CUR=$(curl -s -H "Authorization: token $TOKEN" \
"$GITEA_ARM_API/repos/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE" \
| jq -r '[.labels[].id] | @csv' 2>/dev/null | tr -d '"')
if [ -n "$_LBL" ]; then
_IDS=$(printf '%s,%s' "${_CUR:-}" "$_LBL" | tr ',' '\n' | grep -E '^[0-9]+$' | sort -u | paste -sd, -)
curl -s -o /dev/null -X PUT -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"labels\":[${_IDS}]}" \
"$GITEA_ARM_API/repos/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE/labels"
fi
curl -s -o /dev/null -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"assignees\":[\"$GITEA_ARM_APPROVER_LOGIN\"]}" \
"$GITEA_ARM_API/repos/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE"
echo " (已指派給 $GITEA_ARM_APPROVER_LOGIN 並掛上 Human——他的「指派給您的」看得到這張)"
echo "✅ 已在 Gitea 貼出請求:https://git.uncle6.me/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE"
echo ""
echo "── 轉告 leo(照這樣發,一句狀況+一個可回的詞)──────────────────"
echo "[總管] 需要你解一道鎖:$MISSION"
echo "回這則 Gitea 留言貼「$NONCE」就好($MINUTES 分鐘內有效):https://git.uncle6.me/$GITEA_ARM_OWNER/$GITEA_ARM_REPO/issues/$ISSUE"
echo "──────────────────────────────────────────────────────────────"
echo ""
echo "nonce=$NONCE"
echo "issue=$ISSUE"
echo "expires_at=$EXPIRES ($EXPIRES_HUMAN)"
echo "本地請求檔:$PENDING_FILE"