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

652 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: ship-check
description: |
**任何東西要從「內」(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` 的哪一段**
- ① 桌面 daemon`v0.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 版本卡)
- 收的人的介面是 **APICLI**`curl` 才是對的(出口② 的 `/health`,程式在讀它)
- ⇒ 這不是「①② 用 curl、③ 用瀏覽器」,是**每個出口各自問自己的收件人在看什麼**
---
## 🚚 雲端線出貨=**一個指令**,不要照下面的步驟手工做(2026-08-08 起)
> leo 2026-08-08:「**每次做一樣的事,結果會打錯實例,就是你的出貨閘是錯的。
> 寫對的應該每次都機械式的做同一件事,寫錯位置也會被它修正。**」
> 「這個出貨閘門就是廢的,它應該要像 GitHub Actions 一樣 CI/CD。」
```bash
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. 先查記憶(別重蹈覆轍)
```bash
grep -rn "版本號\|出貨\|release" system-dev/wiki/decisions-summary.md | head
```
必讀 **D39「版本號由內容算出來,不由人宣告」**
- `MAJOR.MINOR``RELEASE_LINE` 檔(人只在大改版動)
- **`PATCH` 由機器決定**:內容指紋一變 +1、沒變不動(重跑不虛增)
- **真相源只有 `manifest.release` 一處**installerlandingportal 全是讀者
> ⇒ **「改了 code 但 release 沒 bump」= bundle 沒重打包**,不是版本機制壞了。
### 1. 判斷這次改動影響哪條線
```bash
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**這步最常漏**
```bash
cd products/arcrun-rag
ARCRUN_REPO_ROOT=../../matrix/arcrun node installer/scripts/build-bundles.mjs --out <bundles repo>/
```
**複驗你的改動真的進去了**(不要只看腳本說成功):
```bash
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 的話
```bash
CYPHER_BASE=<實例 cypher URL> CYPHER_NS=<namespace> \
node installer/scripts/compile-workflows.mjs
```
⚠️ **flow 變了就不能沿用舊的預編圖**,腳本會 `exit 1` 擋住(這個閘是對的)。
不帶 `CYPHER_BASE` 就跑=直接失敗,別繞過它。
### 3. 桌面線:打包 App
```bash
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 上可交叉編譯)
```
**三支機械閘必須全過**(交貨前):
```bash
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 去?**你檢查了嗎?**」
```bash
# 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-app` 裡**v0.18.27 實測:`strings` 抓得到 `LoadDirectConfig``saveDirectConfig`direct 模式符號 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.sh``build-win.sh`:建置前把版本寫進 `wails.json` 的 `info`、
> 建完用 `trap` 還原(不污染版控)。
### 4. 寫 changelog**leo 點名要的**
`products/arcrun-rag/docs-site/src/content/docs/help/changelog.md`
格式照既有的(**寫給用戶看,不是寫 commit message**):
```markdown
## 📋 出貨對照表:**改了什麼 → 要動什麼**(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 bundle**uncle6 內部: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.」**
⇒ **KV/D1/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 閘**
```bash
# leo 親跑(AI 不得代跑)
scripts/github-arm.sh "出貨 <說明>" 30
```
開閘後:
```bash
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` 完全不管 docs**`grep docs` = 0 命中)。docs 是獨立的 `arcrun-docs` worker
沒人手動部署它就會**一直停在上一次**——leo 2026-08-08 抓到:Windows 安裝說明還寫著
「解壓縮 zip、裡面有兩個 exe」和「MSIX 要開開發人員模式」,而**我們早就只出單一 `.exe`**。
```bash
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(等於什麼都沒換)。**
**複驗要抓畫面內容,不是看部署訊息**
```bash
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 會看的那兩個地方**
```bash
# ① 雲端線: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 實撞,白部署一次)
```js
// 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_BASE``BUNDLE_BUILT`
以及 `worker.js``DEFAULT_BUNDLE_BASE`
- **為什麼會漏**:08-07 把「指向」從常數搬進設定(為了 stage/prod 用同一套機制),
**但記載該機制的文件沒跟著改** ⇒ 照舊文件做 = 改了一半 = 等於沒改
- 🔑 **可推廣**:**改了機制,就要改記載那個機制的文件**——
否則下一個人(包括你自己)照過期文件做,會得到「做完了但沒生效」
### ✅ 覆蓋 prod bundle repo 的正確動作(08-07 定,別再整包蓋)
```bash
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 |