Files
ISEP/skills/ship-check/SKILL.md
T
Leo 044ef289fe ship-check 送到得了任何 session:內容原樣搬過來,歸屬寫下來讓機器去比(inkstone/ISEP#122 → comment 6096)
## 這一份是原樣搬過來的,一個字都沒改

skills/ship-check/SKILL.md ← inkstone/InkStoneCo `bd52fa3`(PR #114,**還沒併**)
  搬完 md5 f565e4df1b18cc8aaf057f5fa64a9e15,跟來源逐位元組相同(cmp 通過)
  595 → 651 行;描述裡多了部落格/GitHub 鏡像/n8n/pages deploy 這些觸發詞
  ——舊描述那幾個詞**一個都沒有**,所以「我要發一篇部落格文章」那個情境
    根本觸發不到它,而那正是 leo 這次要解的問題。

## 為什麼不是「改成指針」也不是「ISEP 不再自帶」

雲端的 project dir 是**薄殼根**,InkStoneCo 只是它底下的一個目錄
(docs/governance/cloud-wiring.md 記著這件事)
⇒ `InkStoneCo/.claude/skills/` 在雲端**不會被載入**,只有 plugin 這一份會
⇒ ISEP 不帶全文 = 雲端拿不到 = 又變回「薄殼是真身的子集」,
   而那正是這個 repo 成立時要殺掉的病(README 開頭)。
所以 ISEP 必須帶全文,但它是**搬運工不是作者**:內容改在真相源,這裡只放複本。

## 歸屬不能靠人記得——實查證明兩個方向都會發生

skills/ 與 commands/ 那 9 個檔案在 0.1.0(c263866)從 InkStoneCo 複製過來一次,
之後**再也沒有同步過**(git log 只有那一顆)。到今天已經分家兩個,方向相反:

  skills/ship-check/SKILL.md   InkStoneCo 651 行 / ISEP 595 行   ← 那邊新
  commands/sdd-check.md        ISEP 81 行 / InkStoneCo 65 行     ← 這邊新
                               (InkStoneCo 那份還在教 ISEP#91 已退役的「唯一 active SDD」)

⇒「ISEP 一定比較新」與「InkStoneCo 一定比較新」兩句都是錯的。
  這是票上第 4 題「為什麼會有兩份」的答案:不是誰忘了同步,
  是**兩份都會被就地編輯**,而沒有任何東西會喊一聲。

## 所以機制是「寫下來 + 讓機器去比」,而且不新開一支閘

- docs/file-ownership.tsv —— 哪一份是真相源、取自哪顆 commit、當時的 sha256
- 比對長在**既有的信標**上(isep-presence-beacon.sh → hooks/lib/beacon_report.py):
  它的 ② 已經在做「同名而內容不同」這件事,只是**只掃 scripts/**。
  這次把 skills/commands/agents 一起納進去(②b),
  再加一格 ②c 用 sha256 單邊驗——**雲端沒有 InkStoneCo 可以比,那是唯一還作數的檢查**。
  🔴 刻意不開新閘:ISEP 最常見的錯是重造一支平行的閘
  (docs/governance/dispatch-and-reply-format.md §1.6 記著同一課)。

判準是「檔名一樣**而內容不同**」,同步過的不吵——誤攔比漏擋嚴重。
全部只講不擋(SessionStart 本來就不該擋人)。

## 實跑(這棵樹,真的 InkStoneCo)

  🟡 會自動載入的東西兩邊各有一份,而且內容不同:
    - skills/ship-check/SKILL.md ↔ .claude/skills/ship-check/SKILL.md
      (真相源=inkstone/InkStoneCo:… ⇒ 內容改在那裡,改完原樣搬進 ISEP、更新 commit/sha256、升版)
    - commands/sdd-check.md ↔ .claude/commands/sdd-check.md
      (真相源=ISEP 這一份 ⇒ 專案那份是舊複本,同步過去或刪掉它)

兩個方向各講對了自己的出路。

測試:hooks/tests/isep-presence-beacon.test.sh 14 → 26 條,全綠、全離線。
README/docs/hooks-inventory.md/docs/TESTING.md(A18 改 26 條、新增 A29)都跟著改了。
六個數字在這棵樹上實數:61 支閘/84 條註冊/7 位工人/7 支命令/2 支 skill/49 支腳本
——這次沒有增減,但仍然是數出來的,不是沿用上一版。

(本 commit 也帶著上一顆「逃生門」那件事的兩列文件:hooks-inventory 第 255 列與 TESTING A29。)

🔴 待總管定版:改了會被載入的東西就要升版,否則 plugin update 是 no-op。
🔴 InkStoneCo 那半不是我做的(不同 repo):那邊的 .claude/skills/ 與 .claude/commands/
   還留著兩份舊複本,該同步或刪掉;在那之前信標會每次開場點名它們。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:12:22 +08:00

38 KiB
Raw Blame History

name, description
name description
ship-check **任何東西要從「內」(Gitea)送到「外」之前必讀**——不只是 arcrun。 對外包含:發一篇部落格文章到 uncle6.me、推 tag 到 GitHub 鏡像 repo、投稿 n8n 官方模板庫、 出雲端零件包、出桌面 daemon。也在下列時機自動載入:改完會影響用戶的東西之後、 要打包 App/出貨/推 bundle/送 MS Store、要發文、要 pages deploy、要推 GitHub、 leo 問「可以測了嗎」「版本為什麼沒變」「更新了嗎」「封測者拿得到嗎」「發出去了嗎」。 🔴 第一個動作是**分辨這是哪個出口**,因為 stage/版本/arm 每個出口都不同—— 流程本體住在 system-dev/docs/3-specs/critical-paths/ship.md(唯一一份),本 skill 是它的入口。 核心判準:**收的人拿到的東西真的變了,而且他看得出變了什麼**; 對 arcrun 兩條線而言那個介面就是版本號(portal 看雲端、daemon 看桌面)。 收齊:五個出口的判準/兩條版本線的差別/重打 bundle(最常漏)/改 workflow 要重編預編圖/ 三支機械閘+把 DMG/zip 真的打開檢查/寫 changelogD20 開閘出貨/purge jsDelivr/ 從收的人會看的那個地方抓實際畫面複驗。附「常見漏掉的」實撞表與收工前五問。

/ship-check — 東西要出去之前,先確定收的人真的會拿到

這支解什麼病leo 2026-08-05 原話): 「對人來說,我雲端看 portal 有沒有更新,本地看 daemon 有沒有更新, 這個更新機制都寫好了,然後你說你改了這麼多居然版本一樣,還要去找怎麼做, 這個太危險了。」

🔴 核心判準:版本號是 leo 唯一的驗收介面。 版本沒動 = 他無從判斷你做了什麼 = 你等於沒交付。 「我在某台實例 wrangler deploy 過了」不算——那只改了那一台。


🚦 第一步:這是哪個出口?(2026-09-02 補,inkstone/InkStoneCo#112

為什麼補這段leo 2026-09-02):「Gitea 為『內』,只要對外都是『外』, 任何東西出去之前都要經過出貨流程,不是只有 Arcrun, Arcrun RAG 這個計劃

🔴 這支 skill 2026-09-01 失效過:它自己與它指向的地方只認得 daemon 那個形狀, 於是那晚要發部落格與 GitHub 模板時,它沒把人導向任何可用的流程——只好即興補

  • 🔴 流程本體只有一份system-dev/docs/3-specs/critical-paths/ship.md
    • 它定義了內/外的判準六個步驟五個出口各自的 stage/版本/arm/驗法
    • 動手前先讀那一卷的對應出口段,不要憑本 skill 的印象做
  • 判準一句:送出去之後,有沒有一個不是我們的人看得到/拿得到? 有 ⇒ 走那六步
  • 五個出口,各自去 ship.md 的哪一段
    • ① 桌面 daemonv0.18.x)——本 skill 底下的內容主要是它與 ②
    • ② 雲端零件包(1.4.x)——同上
    • ③ 部落格 uncle6.me — 🔴 沒有 arm、沒有版本號stage 是 <8碼>.kbcontent.pages.dev 快照; pages deploy 必須在 repo 外的目錄跑,否則 .env 的 token 會蓋掉 OAuth 並報成權限錯誤 |驗法是瀏覽器,不是 curl(收件人是讀者 ⇒ 見 §5.8 與 ship.md 出口③「驗法」)
    • ④ GitHub 鏡像 repo — 🔴 要 arm,而且連 git remote add 都擋D20); Gitea 就是它的 stage推 tag 之外還要發 release note (怎麼發、要不要再 arm ⇒ ship.md 出口④ 的「release note 怎麼發」段)
      • 🔴 步驟 3 的「Use this template」也要 arm,而且要單獨請一次——它建的是 第二個 repo,撞 ROE「單一 repo」那條,夾不進推 tag 的窗口。 且 guard 只掛在 Bash走瀏覽器按那顆按鈕不會被擋,這條靠人守 (⇒ ship.md 出口④ 的「Use this template 要不要 arm」段)
    • ⑤ n8n 官方投稿 — 沒有 stage,投稿端點尚未查到(inkstone/llm-wiki-template#9
  • 🔴 不要拿 ① 的答案去套 ③④⑤——那正是 2026-09-01 出錯的機制
  • 🔴 「這個出口沒有 arm」不等於「這個出口不必經過 leo」——③⑤ 的人閘是步驟 5 (leo 親手在 stage 上走過那一遍)。判準全文=ship.md 的 「誰批准、誰按鍵、在哪台跑」段:leo 批准,機器執行
  • 跨多個 repo 的一批貨,「100%」看母票的相依清單,不要看 milestone 的百分比 milestone 只數 hub 那個 repo,別的 repo 那幾張不在分母裡) ⇒ ship.md 的「那個數字看哪裡」段
  • 🔴 發現 ship.md 沒涵蓋你手上這件事 ⇒ 那一卷缺了一格,回去補它, 不要在別處另開一份流程(leo 2026-09-02 明確選了「改寫 ship.md」)

⚠️ 這支 skill 自己的失效模式(先讀這段)

leo 2026-08-05:「你寫完一個 skill 然後每個我要提醒你,表示這個 skill 無效

根因:我寫這支時是憑印象列步驟,沒有真的走一遍使用者的路 ⇒ 於是「DMG 打開長什麼樣」「Info.plist 版本對不對」這些只有真的打開才看得到的東西全漏了, 每一條都要 leo 問「你檢查了嗎」才補。

因此本 skill 的鐵律

  1. 每一條檢查都要有可貼的實測輸出——寫不出指令的條目就是還沒想清楚,不要列。
  2. 凡是「使用者會看到的東西」,一律真的打開來看(掛載 DMG、解開 zip、抓網頁內容), 不是確認檔案存在、不是看腳本說成功。
  3. 被 leo 問出來的缺口,當場補進這支 skill——否則下次還是靠他記得。 (本檔的 3.5、2.9 兩段都是這樣補進來的,日期都記著。)

什麼時候跑

任何東西要送到「外面」之前——改完會影響用戶的東西、要發一篇文章、要推 tag 到 GitHub、 要投稿、要出零件包或 daemon。在說「做完了」之前跑,不是收工才跑。

⚠️ 底下的步驟是出口①②(arcrun 兩條版本線)的細節。 出口③④⑤ 請照上面那段回 ship.md 讀該出口的判準——它們沒有版本號卡可以看

🔴 但底下 §5.8「驗前端=用瀏覽器真的載一次」是全出口通則,不歸①② 所有 2026-09-02 補:本 skill 這句劃界曾把 §5.8 圈進①②, 而 ship.md 對出口③ 給的驗法是 curl兩個檔對同一個問題給了兩種答案)。

  • 一句話判準:用收的人的那個介面驗。
    • 收的人的介面是網頁 ⇒ 瀏覽器(出口③ 讀者、出口① 的 portal 版本卡)
    • 收的人的介面是 APICLIcurl 才是對的(出口② 的 /health,程式在讀它)
  • ⇒ 這不是「①② 用 curl、③ 用瀏覽器」,是每個出口各自問自己的收件人在看什麼

🚚 雲端線出貨=一個指令,不要照下面的步驟手工做(2026-08-08 起)

leo 2026-08-08:「每次做一樣的事,結果會打錯實例,就是你的出貨閘是錯的。 寫對的應該每次都機械式的做同一件事,寫錯位置也會被它修正。」 「這個出貨閘門就是廢的,它應該要像 GitHub Actions 一樣 CI/CD。」

cd products/arcrun-rag
node installer/scripts/ship.mjs --list                        # 有哪些目標
node installer/scripts/ship.mjs --target stage                # 預演:建+算版本+報線上差距
node installer/scripts/ship.mjs --target stage --confirm      # 真的走完 stage
node installer/scripts/ship.mjs --target prod  --confirm      # prod(先 stage,且需 leo 開閘)

九個步驟固定不變preflight → build → version → commit → push → pin → deploy → purge → verify 每一步只有「執行/跳過(附機械理由)」兩種結果,斷了後面一步都不跑

它已經替你做掉的事(所以下面 §2〜§6 的手工步驟不要再照著做一遍):

以前靠人記得 現在
記得重打 bundle build 步驟每次都重打(含 build-ui-bundle.mjs——它跟 build-bundles.mjs兩支,只跑一支就是 portal 改動送不出去)
記得換釘子、且兩處都要換 pin 步驟同時寫 wrangler.toml 的 vars 與 worker.js 常數
記得 purge jsDelivr purge 步驟(prod 才需要),驗到收斂為止
記得複驗線上 verify 步驟真的把 daemon 下載下來算 sha256
手打 --bundles <路徑>(會打錯) 🔴 已移除。目標只能 --target,座標全來自 installer/ship.targets.json

🔴 目標打錯打不進去:本機 clone 的 origin 與登錄簿不符 ⇒ 當場擋; CLOUDFLARE_ACCOUNT_ID 一律由登錄簿覆蓋,不吃環境裡飄來的值; prod 沒有 leo 的 .github-armed ⇒ preflight 就斷。

📌 stage 驗證章由管線自己蓋--target stage --confirm 成功才寫 /tmp/.stage-verified)。 不要用手 touch——手蓋的章證明不了任何事,那正是這道閘想擋的東西。

⚠️ 桌面線(打包 App/DMG/exe)還沒併進管線,仍照下面 §3、§3.5 手工做, 做完把產物放進 bundles repo 的 daemon/,再跑 ship.mjs(它會驗版本與 sha 對不對得上)。


兩條版本線(先分清楚在講哪一條)

版本號 leo 從哪看 出貨鏈
雲端 manifest.release(如 1.4.11 portal 設定頁的版本卡 重打 bundle → 推 bundles repo → release 自動 bump → 換安裝器釘子 → 部署安裝器
桌面 manifest.daemon.version(如 v0.18.4 daemon 的「檢查更新」 打包 App → 放進 bundles daemon/ → 改 manifest.daemon → purge jsDelivr

⚠️ 兩條各自獨立。改 kbdb 不會讓 daemon 版本動,反之亦然。


步驟

0. 先查記憶(別重蹈覆轍)

grep -rn "版本號\|出貨\|release" system-dev/wiki/decisions-summary.md | head

必讀 D39「版本號由內容算出來,不由人宣告」

  • MAJOR.MINORRELEASE_LINE 檔(人只在大改版動)
  • PATCH 由機器決定:內容指紋一變 +1、沒變不動(重跑不虛增)
  • 真相源只有 manifest.release 一處installerlandingportal 全是讀者

「改了 code 但 release 沒 bump」= bundle 沒重打包,不是版本機制壞了。

1. 判斷這次改動影響哪條線

git -C matrix/arcrun log --oneline -5      # 雲端 workercypher/kbdb/portal…)
git -C products/arcrun-rag log --oneline -5  # daemon/安裝器/workflow
  • 動到 matrix/arcrun 的 worker 或 console-ui/public/portal/雲端線
  • 動到 collector/(含 cmd/arcrun-app/桌面線
  • 動到 workflows/*.yaml要重編 workflows.json(見步驟 2.5

2. 雲端線:重打 bundle這步最常漏

cd products/arcrun-rag
ARCRUN_REPO_ROOT=../../matrix/arcrun node installer/scripts/build-bundles.mjs --out <bundles repo>/

複驗你的改動真的進去了(不要只看腳本說成功):

grep -c "<你改的關鍵字>" <bundles repo>/tier2/<worker>/index.js
# 例:改 embed 模型 → grep -c "bge-m3"  應 > 0,且舊模型應為 0

🔴 實撞(2026-08-05):我改了 kbdb 的 embed 模型並 wrangler deploy 到 youlin沒重打 bundle ⇒ bundles repo 裡仍是 bge-base-en新用戶安裝/既有用戶重裝都拿到舊的,而 leo 看到的 release 仍是 1.4.11。

2.9 bundle 沒重打之前,要主動叫 leo 別按「立即更新」

這是最危險的狀態,比「沒出貨」更糟:

  • 你直接 wrangler deploy 到某台實例 ⇒ 那台是新的
  • 但 bundle 還是舊的 ⇒ leo 按 portal 的「立即更新」=從舊 bundle 重裝
  • 他的實例會被你剛修好的東西「降級」回舊版

實例(2026-08-05,leo 說「我要去更新實例驗證」時攔下): kbdb 已直推 bge-m3(1024 維)+已建 1024 維 Vectorize index 但 bundle 裡仍是 bge-base-en(768) ⇒ 一按更新,kbdb 退回舊模型, 與 1024 維 index 對不上 ⇒ 中文語意搜尋直接壞掉

📌 判準:只要你「直推過實例」但「還沒重打 bundle」, 主動說一句「先別按立即更新,會裝回舊的」——不要等 leo 自己踩到。 兩者狀態不一致的期間,降級風險是你造成的,說清楚是你的責任

🔁 2.7 動到安裝/更新/認證的話:stage 要走三段,不准只測 update

leo 2026-08-14 立,全文見頂層 decisions-summary.md D84 之二

leo 原話:「以後你的 stage 測試除了測 update 還要用 uninstaller 刪除後再模擬第一次安裝。

① 測 update(既有 stage 實例)
② 用 uninstaller 拆掉
③ 從乾淨狀態模擬第一次安裝

🔴 只做 ① 不算測過。 stage 實例是長期存在的,它身上有所有舊 binding 與舊 secret ⇒ 它天生就是「既有實例」,天生看不到全新用戶會撞的坑。

這條的代價是實際發生過的2026-08-14 一天三個坑,全是只驗 update 會漏掉的):

  • 安裝器從沒種過 CF_SECRETS_API_TOKEN全新用戶建不出第一個帳號Arcrun#119
  • 中心 KV 說「你裝過了」而帳號上什麼都沒有 ⇒ 重裝死結Arcrun#120
  • 撞牆訊息叫用戶去跑一件做不到的事(Arcrun#121

每一個都在既有實例上測不出來,因為既有實例會沿用舊資源把缺口蓋住。

⚠️ uninstaller 還沒做出來之前,這條走不完 ⇒ 出貨時要明講「③ 沒驗」 不准因為「工具還沒有」就跳過不提。

2.5 改過 workflow 的話

CYPHER_BASE=<實例 cypher URL> CYPHER_NS=<namespace> \
  node installer/scripts/compile-workflows.mjs

⚠️ flow 變了就不能沿用舊的預編圖,腳本會 exit 1 擋住(這個閘是對的)。 不帶 CYPHER_BASE 就跑=直接失敗,別繞過它。

3. 桌面線:打包 App

cd products/arcrun-rag/collector/cmd/arcrun-app
VERSION=vX.Y.Z bash build-mac.sh && bash build-dmg.sh   # Mac
VERSION=vX.Y.Z bash build-win.sh                        # WindowsMac 上可交叉編譯)

三支機械閘必須全過(交貨前):

for s in check-cis.sh check-render.sh check-tray.sh; do bash "$s" || echo "❌ $s"; done

實跑驗證產物(不是看檔案存在):Mac 用 open 真的跑一次; 或用同綑的 collector 跑 direct --once 確認你的修復在裡面。

3.5 🔴 把 DMG/zip 真的打開,用「使用者第一次看到的樣子」檢查

leo 2026-08-05 連問三次才問出來的東西——列步驟不算驗,要真的開起來看。 「我問你 Mac 打包是不是一個 folder 打開可以把 dmg 拖到 application 去?你檢查了嗎?

# Mac:掛載 DMG,看使用者會看到什麼
hdiutil attach dist/Arcrun-vX.Y.Z.dmg -nobrowse -quiet -mountpoint /tmp/dmgchk
ls /tmp/dmgchk/                       # 應**剛好兩項**Arcrun.app  Applications 捷徑
ls /tmp/dmgchk/Arcrun.app/Contents/MacOS/   # ⚠️ 2026-08-14 訂正:現在是**單一二進位**,只有 arcrun-app
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \
  /tmp/dmgchk/Arcrun.app/Contents/Info.plist   # ← 必須是本次版本,**不是 1.0.0**
/usr/libexec/PlistBuddy -c "Print :LSUIElement" \
  /tmp/dmgchk/Arcrun.app/Contents/Info.plist   # 應為 true(不佔 Dock
codesign -v /tmp/dmgchk/Arcrun.app            # 無輸出=簽章有效
hdiutil detach /tmp/dmgchk -quiet

# Windows:解開 zip 看內容
unzip -l dist/ArcrunRAG-win-unsigned-vX.Y.Z.zip   # 應有 Arcrun.exe  arcrun-collector.exe

逐項判準

檢查 為什麼 漏掉的後果
DMG 內剛好兩項 這就是「拖進 Applications」的標準畫面 沒有捷徑 ⇒ 使用者在下載資料夾直接開 ⇒ 更新會蓋錯位置t184
arcrun-collector 同綑 已過期(2026-08-14 訂正) 以前 daemon 是獨立二進位;現在 collector 內建在 arcrun-appv0.18.27 實測:strings 抓得到 LoadDirectConfigsaveDirectConfigdirect 模式符號 18 處) 🔴 照舊版檢查會發出「你的 App 缺件」的假警報——總管 08-14 差點對 leo 發出來。要驗「同步能力在不在」改用 strings 抓 direct 模式符號
CFBundleShortVersionString Finder/「關於」顯示的版本 永遠顯示 1.0.0 ⇒ 用戶無從判斷自己是不是新版
LSUIElement=true 常駐小工具不該佔 Dock 行為與設計不符
codesign -v 改過 bundle 內容必重簽 launchd 拒開

🔴 實撞(2026-08-05):CFBundleShortVersionString 一直是 1.0.0—— wails.json 沒有 info.productVersion,而 wails build 沒有 CLI 旗標可指定。 解法已寫進 build-mac.shbuild-win.sh:建置前把版本寫進 wails.jsoninfo、 建完用 trap 還原(不污染版控)。

4. 寫 changelogleo 點名要的

products/arcrun-rag/docs-site/src/content/docs/help/changelog.md

格式照既有的(寫給用戶看,不是寫 commit message):


## 📋 出貨對照表:**改了什麼 → 要動什麼**(leo 2026-08-08:「寫清楚不要再改錯」)

> 這張表是**查的**,不是用判斷的。08-08 一整天的錯全是「憑印象決定要重打什麼」。
> **每次出貨逐列對,有碰到就一定要做。**

| 你改了什麼 | 要重打/重部署 | 版本線 | 要一起改的文件 | 最後落在哪 |
|---|---|---|---|---|
| **daemon Go 程式** | 三平台打包(mac/win/msix | **daemon 版本線** | `changelog.md` **必寫** | 用戶自己的電腦 |
| **portalconsole-ui 前端** | `build-ui-bundle.mjs` | **雲端 release** | 有畫面變化就要更 docs | 每個實例的 `arcrun-rag-ui` |
| **cypher / kbdb / registry / mcp** | `build-bundles.mjs` | **雲端 release** | — | 每個實例的對應 worker |
| **安裝器 `worker.js`** | 部署安裝器(帶 `--config` | 安裝器自己 | — | `install.arcrun.dev` |
| **landing** | 部署 landing | — | — | `rag.arcrun.dev` |
| **說明文件** | `npm run build`**`dist``deploy/docs`** → 部署 | — | — | `rag.arcrun.dev/docs` |
| **錯誤分類/新的失敗原因** | — | — | 🔴 **FAQ 一定要同步** | docs 站 |

🔴 **兩個最常漏的(都有 08-08 實據)**
- `build-bundles.mjs` **不會重建 `arcrun-rag-ui`**(它在「沿用非本腳本產」清單裡)
  ⇒ 只跑一支=**靜默出半套**。portal 文案傳不出去整整一天就是這個。
- docs 的 `dist → deploy/docs` **沒有腳本**,漏了它 `wrangler deploy` 會「成功」但只上傳 0.35 KiB。

### 版本修改內容(`changelog.md`)怎麼寫

`changelog.md` 是**版本號的單一真相源**,也是使用者在「版本與更新」畫面看到的內容。

- **一行一句,講「你會看到什麼不一樣」**,不是 commit 訊息翻譯
- 🔴 **要短。細節去 docs 讀**leo 08-08 看到 v0.18.24 的更新說明是一整面文字牆:
  「**不要這麼長的散文,簡短講改了什麼,細節去 docs 讀**」)
- **不要帶 markdown 粗體**——那個欄位是純文字,`**` 會原樣顯示給使用者看
- 沒有對應版本的段落就**不准打包**(`changelog-section.sh --check` 會擋)

### FAQ 什麼時候一定要動

- **新增/改變任何「使用者會看到的失敗原因分類」** ⇒ FAQ 必須同步
  (分類收斂成有限幾類,畫面只出分類與份數,**細節全在 FAQ**——見 t214
- **改了安裝或更新的做法** ⇒ 對應的說明頁必須同步
  (08-08 實據:Windows 說明還寫「解壓縮 zip、裡面兩個 exe」,而我們早就只出單一 `.exe`

📌 **判準:使用者會因為這次改動而問出新問題嗎?會 → FAQ 要有答案。**

### 什麼地方 → 修哪個實例(別再搞混)

> 🔴 **AI 的工作迴圈只有兩件事**leo 2026-08-08 原話):
> 「**實際上你是在 uncle6 修改 bundle,然後你自己在 youlin stage 安裝新版 bundle。**」
> ⇒ **改 staging bundleuncle6)→ 裝進 youlin 的 stage 環境 → 自己測**。這一圈可以無限重複。
> **「uncle6 不碰」指的是 prod 那一側,不是 staging bundle**——別把自己的工作區也擋掉。

| 這個東西 | 住在誰的帳號 | AI 能不能動 |
|---|---|---|
| **staging bundle repo** | uncle6Gitea | ✅ **這是 AI 的工作區**,隨便改隨便推 |
| **youlin 的 stage 環境** | youlin | ✅ **AI 的測試場**,隨便裝、壞了自己修 |
| **prod bundle repo** | uncle6GitHub | 🔴 要 leo arm,且只准「**原封提升**」不准重打 |
| **prod 的安裝器/landingdocs 部署** | uncle6 | 🔴 部署=出貨動作,要 leo 開閘 |
| **youlin 的 prod 環境** | youlin | 👤 leo 用來驗「用戶裝 prod 拿到什麼」 |
| **geek6688(出貨機/第一個顧客)** | geek6688 | 🔴 **不准拿它當測試場**;但**發佈後總管要手動把它更新到最新**——見 `ops-facts.md` §出貨線。**出貨機比貨舊是錯的**(08-12 實撞:停在 1.4.29、落後 12 版) |
| `rag-demo.arcrun.dev` | uncle6 | 🪦 **已退場**,任何文件/腳本不得再導向 |

📌 **一句話界線****staging 側全是我的、prod 側全是 leo 的閘。**

### 🖥️ 三台機器各自幹嘛(leo 2026-08-12 定義,08-14 再次口頭確認)

> **youlin =測試機|geek6688 =出貨機|uncle6 =總部機**

- **youlin =測試場**:**它就是拿來拆的**。裝壞了、要清空重來,**不必問、不必猶豫**
  ——把它當成珍貴資產而不敢動,等於讓「只有全新用戶會撞的坑」永遠測不到(D84 之二)。
  唯一要守的是**時序**:確認沒有別條線正踩在上面。
- **geek6688 =出貨機/第一個顧客**:**不當測試場**,但**發佈後要更新到最新**(總管手動)。
- **uncle6 =總部機**bundle 的家。staging 側是總管的工作區,**prod 側是 leo 的閘**。

📌 **總管 2026-08-14 實錯**leo 交辦拆 youlin 時,總管去問「有沒有資料是重裝也長不回來的」
——**而這個答案 `ops-facts.md:29` 早就寫著了**。leo 當場點破:「**stage 不就是用來測的?**」
⇒ **對測試機的謹慎不是美德,是把該做的驗證擋在門外。**

## 🗺️ 出貨流程(leo 2026-08-08 深夜定死,不得再自創路徑)

> leo 原話:「**uncle6 的 bundle 要裝在 youlin 的 stage 環境,沒問題後才推 prod 改裝在
> youlin 的 prod 環境,所以 uncle6 的 bundle 要 2 套,youlin 也要兩套,
> 就像把同樣程式碼打到不同分支一樣。**」
> 「**CF 可以這樣建立分離的 stage,不是在 prod 環境測 stage。**」

### 矩陣(兩軸,四格,缺一格就會像 08-08 那樣炸)

            產物來源(uncle6)            安裝目標(youlin

stage → staging bundle repo → youlin 的 stage 環境 prod → prod bundle repo → youlin 的 prod 環境

**同一份程式碼打到兩條線,像 git 的兩個分支——彼此不共用任何會互相污染的東西。**

### 六步,順序不准跳

① 改 code → commit + push gitea ② 打 bundle → 版本號由內容指紋算出;內容變了版本一定變 ⚠️ 要跑兩支:build-bundles.mjs build-ui-bundle.mjs (前者不會重建 arcrun-rag-ui,漏跑=靜默出半套) ③ 推 staging bundle → Gitea(不碰 GitHub,無 D20 閘) ④ 裝進 youlin 的 stage 環境 → AI 自己測:登入、首頁、匯出診斷檔(瀏覽器實載) ⑤ 👤 leo 去 youlin 的 stage 環境,看修復版是不是真的修好了 → 他說可以,跑 scripts/stage-ok.sh ⑥ 👤 leo 跑 scripts/github-arm.sh → 把 staging bundle「原封提升」成 prod bundleuncle6 內部:staging repo → prod repo👤 leo 在 youlin 的 prod 環境裝一次新版 prod 內容 → 驗的是「用戶從 prod 安裝器裝,會拿到什麼」這條路本身 ⑧ 之後任何人從 prod 安裝器裝就拿到新版;leo 想在哪抽查都行(geek6688 或任何實例)


🔴 **youlin 為什麼一定要兩套環境**leo 2026-08-08:「**然後我可以在 youlin 測新版的
prod 內容安裝**」):
- **stage 環境** → 驗「修復到底修好了沒」(發佈前的閘,第 ⑤ 步)
- **prod 環境** → 驗「用戶裝 prod 會拿到什麼」(發佈後的第一手驗證,第 ⑦ 步)
⇒ 兩件事驗的東西不同,**不能共用同一套環境**——
  共用就會變成 2026-08-08 那樣:為了測而動到正在用的那套,把要驗的東西弄壞。

🔴 **⑥ 是「提升」不是「重打」**leo 2026-08-08 原話:
「**arm 推的是 uncle6 把 stage 的 bundle 推到 prod 的 bundle**」)
⇒ **prod 的產物必須與 stage 上被驗過的那一份逐位元相同**。
   一旦重跑一次 build 才推 prod**stage 驗過的東西就不是 prod 上的東西**,
   ⑤ 那一關的意義當場歸零。
⇒ 驗收方式:提升後比對兩邊的**指紋/sha**,不同就是做錯了。
   這正是 leo 的比喻「**就像把同樣程式碼打到不同分支一樣**」——搬,不是再做一次。

🔴 **stage 永遠在 youlin,這是固定的**leo 2026-08-08 原話:
「**5 是我去 youlin stage 環境看你的修復版是否真的修復,就可以發佈,發佈以後,
我可以在任意地方試 prod,但 stage 一定在 youlin。**」)
⇒ **核實點固定在 stage**,不是「找另一台乾淨的機器來驗」。
   prod 發佈後在哪裡試都行,那是**發佈後的抽查**,不是發佈前的閘。

**兩把鑰匙都在 leo 手上(⑤⑥),AI 造不出來,這是刻意的。**

### CF 官方機制(查證出處,不是憑記憶)

`https://developers.cloudflare.com/workers/wrangler/environments/`

- 用 `[env.NAME]` 宣告環境;部署 `npx wrangler deploy --env NAME`
- **Worker 名字自動變成 `<top-level-name>-<env>`**(例:`arcrun-cypher-executor-stage`
- 🔴 官方原文:**「Non-inheritable keys are configurable at the top-level, but cannot be
  inherited by environments and must be specified for each environment.」**
  ⇒ **KVD1/vars 一律不繼承,每個環境必須各自宣告**——這正是環境真正分離的地方,
  也是「漏宣告就會共用到 prod 資源」的風險點。

### 🔴 為什麼要把流程寫死(08-08 一整天的代價)

那天出貨是**用手拼的**:手動 cp 產物 → 手動改 manifest → 手動 sed 換兩處釘子 → 手動 deploy。
後果四連發,全部有實據:
- 改了 portal 文案,**bundle 版本沒動** ⇒ 改動永遠送不出去
- 版本沒動 ⇒ 安裝器判「同版整批跳過」⇒ **重裝也修不好**(leo 白重裝一次)
- 兩台都宣稱 `1.4.22` **程式碼卻不同**(一台有診斷端點、一台 404)
- 為了測 stage 而手動部署,**繞過安裝器注入** ⇒ leo 的 portal 畫面壞、登入斷
  ⇒ **他晚上要驗收時,發現要驗的東西被驗收流程本身弄壞了**

⇒ leo:「**每次做一樣的事,結果會打錯實例⋯⋯寫對的應該每次都機械式的做同一件事,
寫錯位置也會被它修正。**」**這條流程就是那個「機械式」的定義,不准再自創。**

## 🚦 第 0 步:**先上 stage,不准直達 prod**leo 2026-08-08 立,封測期起)

> leo:「現在因為**開始封測**,不直接打到 prod,而是先打到昨天建的 stage⋯⋯
> 因為**推 prod 就發佈了**,雖然現在人不多,但**要謹慎**。」

🔴 **這一段是補進來的,因為本 skill 原本從頭到尾寫的是 prod**
github-arm → GitHub `arcrun-rag-bundles` → jsDelivr/raw 驗證),
**沒有任何一步是「先上 stage 驗過再上 prod」**;而 D20 那道閘擋的是「寫 GitHub」,
不是「未經 stage 就發佈」⇒ 光「知道有 stage」對行為零作用。

順序(**不准跳**):

1. 打 **staging** bundle → 推 Gitea `arcrun-rag-bundles-staging`
   (不碰 GitHub**無 D20 閘**,不需要 leo
2. 用 staging 安裝器實裝到測試實例
   `https://arcrun-rag-installer-staging.uncle6-me.workers.dev`
3. 走一次**封測者真的會走的那條路**,貼實測輸出(不是 HTTP 200)
4. 過了才做下面的 prod 步驟

⚠️ **身分**leo 2026-07-25 令「測試一律用 youlin,別拿 leo21c 當探針(會製造假信號)」
⇒ 部署前先 `acr whoami`。

⚠️ **stage 與 prod 內容範圍不一樣**daemon 安裝檔、README、core 顆數)
⇒ 見下面「別再整包蓋」那段,**不要拿 staging manifest 整包覆蓋 prod**。

📌 stage 判準(leo 08-07):stage **不需要 custom domain**`*.workers.dev` 就好——
「僅預設網域不准標 ✅」的目的是「用戶拿不到=沒交付」,而 stage 的用戶就是 leo 與總管。
不掛 custom domain 反而是優點(沒人會誤入、不被索引、不會被當正式網址傳出去)。

🔧 機械閘:`.claude/hooks/stage-before-prod-guard.sh`
6 小時內沒 stage 驗證紀錄 ⇒ 擋掉 prod bundle repogithub-armpublish-github)。

📖 stage 救過一次的實例:見頂層 `system-dev/wiki/status.md`
「同一次測試照出一個會炸掉所有用戶的回歸(stage 第一次真的救了我們)」。

## vX.Y.ZYYYY-MM-DD

**建議更新**——一句話說「這版解決你什麼問題」。

- 🔴 **最重要那項**:用戶語言描述症狀與結果
- 其他項…

判準:用戶讀得懂「這對我有什麼差別」。 不要寫「修 t195 的 401」,要寫「修好『一個檔失敗就卡住整個資料夾』」。

5. 出貨(需 leo 開 D20 閘

# leo 親跑(AI 不得代跑)
scripts/github-arm.sh "出貨 <說明>" 30

開閘後:

cd products/arcrun-rag
node installer/scripts/ship.mjs --bundles <bundles repo>            # 先 dry-run 看待辦
node installer/scripts/ship.mjs --bundles <bundles repo> --confirm  # 真出貨

ship.mjs 會依序做:推 bundle → 換安裝器釘子 → 部署安裝器/landing → purge jsDelivr

⚠️ purge 一次可能不夠,要驗到收斂(腳本自己會重試,別提前宣稱完成)。

5.5 說明文件站(2026-08-08 補:它不在任何腳本裡,最容易整批過期

🔴 ship.mjs 完全不管 docsgrep docs = 0 命中)。docs 是獨立的 arcrun-docs worker 沒人手動部署它就會一直停在上一次——leo 2026-08-08 抓到:Windows 安裝說明還寫著 「解壓縮 zip、裡面有兩個 exe」和「MSIX 要開開發人員模式」,而我們早就只出單一 .exe

cd docs-site
npm run build                       # → dist/
rm -rf deploy && mkdir -p deploy/docs && cp -R dist/. deploy/docs/   # ⚠️ 見下
npx wrangler deploy

⚠️ dist → deploy/docs 這一步沒有腳本,只活在人的記憶裡08-08 實撞): wrangler.toml[assets] directory = "./deploy",而 Astro 建到 dist漏了它,wrangler deploy 會「成功」但只上傳 0.35 KiB(等於什麼都沒換)。

複驗要抓畫面內容,不是看部署訊息

curl -s "https://rag.arcrun.dev/docs/start/install-windows/?cb=$RANDOM" \
  | grep -oE "<這次該出現的字串>"

📌 判準:凡是改動會讓說明文件過期的出貨,docs 就是出貨的一部分。 版本、安裝方式、畫面長相變了 ⇒ 一起改、一起部署、一起複驗。

5.8 🔴 驗前端=用瀏覽器真的載一次curl | grep 不算(leo 2026-08-08 立)

leo 原話:「你的環境有 web,你應該用 web 驗,你已經開啓了卻沒有完成,你要把這個列入規定。

🔴 適用範圍:全出口,不是只有①②2026-09-02 補,見上面「什麼時候跑」那段的劃界)。 收的人的介面是網頁就套這條——出口③ 部落格的收件人是讀者,他的介面就是瀏覽器。 ship.md 出口③「驗法」那格寫的是同一件事,兩邊只有一種說法。

為什麼 curl | grep 是假驗證(08-08 實撞,leo 抓到而不是我發現): curl 拿到的是 HTML 原始碼——它不執行 JS、不載入 config.js、不發 API 請求。 所以我 grep 到文案就宣稱「前端驗過」,而使用者實際打開看到的是整條紅色錯誤: 「設定檔沒載入(config.js),這個頁面連不到你的服務」。那個畫面我一次都沒看到。

判準升級 HTTP 200 不算驗過grep 到字串也不算驗過只有「瀏覽器載入後看起來能用」才算

mcp__Claude_Browser__preview_start  {url: "<用戶會走的網址>"}
mcp__Claude_Browser__computer       {action: "screenshot"}   ← 看畫面,不是看原始碼
mcp__Claude_Browser__read_console_messages {onlyErrors: true} ← JS 有沒有炸

要看的是:有沒有錯誤橫幅/該有的資料是不是還停在「載入中…」「查詢中…」/console 有沒有紅字。

⚠️ 順帶一個會騙人的坑(同日實撞)curl?cb=$RANDOM 繞不掉 Cloudflare 邊緣快取—— /config.js 一直回舊的空值,直到加 -H "Cache-Control: no-cache" 才看到真值。 ⇒ 用 curl 查線上狀態時,沒加 no-cache 就可能是在驗快取,不是在驗線上

6. 複驗:從 leo 會看的那兩個地方

# ① 雲端線:portal 版本卡讀的是安裝器 /api/latest
curl -s https://install.arcrun.dev/api/latest
#    → "release" 應等於 manifest.release

# ② 桌面線:daemon「檢查更新」讀的是 jsDelivr**不是 raw**
curl -s "https://cdn.jsdelivr.net/gh/youlinhsieh/arcrun-rag-bundles@main/manifest.json" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['daemon']['version'])"

# ③ 用戶下載的固定檔名真的抓得到、且 sha 與 manifest 相符
curl -sI "https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac.dmg"

🔴 raw 是新的不代表 jsDelivr 是新的——daemon 讀 jsDelivr 2026-08-05 實撞:push 完 raw 立刻新版、jsDelivr 仍吐 v0.15.7purge 後第 2 次才收斂。


收工前自問(答不出來就是還沒做完)

  1. leo 打開 portal,版本卡會顯示新號碼嗎? 不會 → 雲端線沒出貨完
  2. leo 按 daemon「檢查更新」,會看到新版嗎? 不會 → 桌面線沒出貨完
  3. 新用戶現在安裝,拿到的是我改的那份嗎? 不確定 → 回步驟 2 複驗 bundle 內容
  4. changelog 有這一版嗎? 沒有 → 用戶不知道你改了什麼
  5. 以上任一是「否」→ 不准說「完成」,要說「已改,未送達」

常見漏掉的(實撞記錄)

漏掉的 後果 日期
沒重打 bundle release 不 bump、新用戶拿舊版 08-05
沒 purge jsDelivr 用戶按「檢查更新」像沒反應 08-02
沒改 manifest.daemon 「檢查更新」回「manifest 沒有 daemon 版本欄位」全體失效 08-02
只推 bundle 沒換釘子 安裝器仍裝舊 bundle 08-01
🔴 釘子只改了常數,沒改 [vars] 部署成功但釘子沒換——api/latest 與 install 頁全是舊版,白部署一次 08-07
rsync --delete 覆蓋 prod bundle repo 砍掉 prod 才有的 daemon/20 個安裝檔)與 README.md,封測者下載不到 daemon 08-07(推之前 git status 抓到)
用 staging 的 manifest 整包蓋 prod core 從 5 顆變 24 顆——prod 只裝 5 顆是懶載設計,會讓每個新用戶多裝 19 顆 worker 08-07(同一個坑 bbb433e 有前科)
git worktree 打 bundle 0/4 bundled——worktree 沒有 .component-builds/ 的預編 wasmgitignore 產物) 08-07

🔴 換釘子:真身是 [vars],不是常數(08-07 實撞,白部署一次)

// installer/oauth-prototype/worker.js:74
bundleBase(env) = env.BUNDLE_BASE ? env.BUNDLE_BASE : DEFAULT_BUNDLE_BASE

wrangler.toml[vars] BUNDLE_BASE 優先於 worker.js 的常數。

  • 兩處都要改wrangler.toml[vars] BUNDLE_BASEBUNDLE_BUILT、 以及 worker.jsDEFAULT_BUNDLE_BASE
  • 為什麼會漏:08-07 把「指向」從常數搬進設定(為了 stage/prod 用同一套機制), 但記載該機制的文件沒跟著改 ⇒ 照舊文件做 = 改了一半 = 等於沒改
  • 🔑 可推廣改了機制,就要改記載那個機制的文件—— 否則下一個人(包括你自己)照過期文件做,會得到「做完了但沒生效」

覆蓋 prod bundle repo 的正確動作(08-07 定,別再整包蓋)

git reset --hard HEAD && git clean -fd        # 先回到 prod 原狀
cp -R <staging>/<worker>/. <worker>/          # 逐顆覆蓋,不用 rsync --delete
# manifest:只換 prod core 清單裡「原本就有」的那幾顆 + release/built/source
git add -A
git diff --cached --diff-filter=D --name-only   # ← 護欄:刪除檔案數必須是 0

判準:staging 與 prod 的內容範圍不一樣(daemon 安裝檔、README、core 顆數),不能整包蓋。 | changelog 沒寫 | 用戶不知道能不能/要不要更新 | 08-05(停在 v0.15.7v0.18.x 全空) | | portal 下載連結沒跟著換 | DMG 打好了但按鈕還給 zip | 08-05 |