Spec 一致性 CI 把關用開源 @fission-ai/openspec CLI(ubuntu),而非 macOS-only 的 Spectra GUI binary
Context
repo 目前有 13 個 active spectra change、60 個 canonical spec,但 spec ↔ code 的一致性
完全沒有自動把關(grep spectra .github/workflows/ 零命中)。後果已發生:change 0036
帶 invalid delta-spec、多個 change 進度長期卡在 0/N(0019 0/31、0031 0/36、0024 0/15、
0033 0/10)卻無人提醒。
目標是建兩道 gate:每次 PR 的快速確定性檢查 + 定期 full-scope 深度掃描。實作前 先釐清「用什麼工具、在哪個環境跑」,因為這直接受制於 Spectra 的散佈形態。
關鍵約束與發現(皆實測,非推測):
-
Spectra CLI 是 macOS-only 桌面 App。本機
spectra是/Applications/Spectra.app內 binary 的 symlink,透過brew install --cask spectra-app安裝,cask 標明Required: macOS,無 Linux build。scripts/setup-cloud.sh寫的npm install -g @heyu-ai/spectra在公開 npm 回 404(該 scope 未公開發布,且該行本身帶warnfallback)。 → 真 Spectra binary 無法在便宜的ubuntu-latestCI runner 跑。 -
但底層是開源的
@fission-ai/openspecv1.5.0(Node 20.19+,npm 安裝,ubuntu 相容)。 實測:從docs/目錄跑openspec validate --changes --json能解析本 repo 的docs/openspec/並驗證 change/spec;validate的 flag(--all/--changes/--specs/ --strict/--json)與 Spectra 完全相同 → Spectra 包的就是這套 OSS,只是再疊一層 GUI 與專屬加值。 -
validate/drift/analyze是編譯進 binary 的 subcommand(確定性、不吃 LLM), 不是 skill。/spectra-*Claude Code skill 只是薄包裝、shell out 去呼叫 binary subcommand 再排版(例如/spectra-drift實際跑spectra drift --json);drift 的 anchor 上限寫在 Rust 模組spectra_core::drift,是編譯碼而非 skill 邏輯。純終端機spectra drift <change>不需任何 skill 或 LLM。其中drift/analyze是 Spectra 專屬——OSS OpenSpec binary 沒有此二 subcommand(openspec --help只有validate、list、archive、status、doctor等)。唯一例外是verify(code↔spec 比對): Spectra 與 OSS 都沒有對應 binary subcommand,只有/spectra-verify//opsx:verify這種純 LLM agentic skill。 → 推論:drift/analyze因為是確定性 binary、理論上能 headless 跑 CI,唯一阻擋是 binary 只有 macOS 版。長期緩解路徑:https://github.com/howie/openspectra(開發中,目標提供 Linux-native CLI 以移除此限制);verify則不論平台都必須靠 LLM。 這三者現階段只能在 Mac(Spectra binary)或 headless Claude(verify)跑。 -
OpenSpec OSS 的 root 解析預設找
openspec/目錄,本 repo 在docs/openspec/,故 CI 須 以working-directory: docs執行才解析得到 root(從 repo 根跑會得到 0 items)。
Decision
結論:per-PR 的確定性 gate 用開源 @fission-ai/openspec 的 validate 在 ubuntu CI 跑
(pin 版本、零 macOS 成本);drift / analyze / verify 等 Spectra 專屬或 LLM 深度檢查,
留在 Mac 上的手動 make spectra-scan。
- 每次 PR(ubuntu CI):
npm install -g @fission-ai/openspec@<pinned>→working-directory: docs跑openspec validate <diff-scoped change/spec> --strict --json→ PR comment + 折進ci-statusrollup(兩階段 rollout:先 report-only,baseline 清乾淨後轉硬 gate)。 - 定期 full-scope(Mac 手動):
make spectra-scanloopspectra list --json跑真spectra drift+analyze,並對 in-progress change 提示/spectra-verify,聚合報告 → GitHub Issue + Slack。
工具邊界一句話:確定性、便宜、跨平台的 validate 上 ubuntu;專屬、LLM、macOS-bound 的
drift/analyze/verify 留 Mac。
Alternatives Considered
- 方案 A:macos-latest CI runner brew 裝真 Spectra — 考慮過,因為
macOS 分鐘數計費 10x、 95MB App 每次 brew 裝 2-3 分鐘、cask auto_updates=true 導致版本不可 pin(TestFlight 式的 版本漂移風險搬進 CI)成本與穩定性代價過高沒有選。 - 方案 B:ubuntu 用 Python 重寫 validate 格式檢查 — 考慮過,因為
要自己維護一套 delta-spec 格式驗證、且會跟 Spectra/OpenSpec 的真實規則靜默漂移(reimplementation drift)沒有選。 - 方案 C(採用):ubuntu 跑開源 OpenSpec CLI 的真
validate— 便宜 + 用真工具不重造輪子, 代價是OSS 1.5.0 vs Spectra 2.3.1 的格式規則可能緩慢分歧。緩解:pin 版本 + 納入 ADR-0006 的 vendor-drift 監控 + 上線前驗 OSS↔Spectra parity(確認兩者對 0036 等案例判定一致)。
Consequences
好:
- per-PR 確定性 gate 零 macOS CI 成本,跑在既有的 ubuntu CI 慣例上(
dorny/paths-filter的changesjob、ci-statusrollup、actions/github-scriptPR comment 全可復用)。 - 用真工具驗證(OpenSpec 是 Spectra 的上游),不需自行 reimplement 格式規則。
- 工具邊界清楚:誰能上 Linux CI、誰留 Mac,一目了然,未來新增檢查有依據可循。
壞 / 代償:
- OSS ↔ Spectra 版本分歧風險:1.5.0(OSS)與 2.3.1(Spectra)共用格式但 Spectra 有專屬擴充
(
spec-driven-yibischema、.openspec.yamlper-change metadata)。validate目前相容,但需 持續監控;若日後分歧過大,per-PR gate 退回方案 A(macos runner 跑真 Spectra)。 drift現階段訊號弱:多數 change 的.openspec.yaml缺created日期(實測 0040 有、 0039/0027 為 null),Time 與 Task-collision 維度失效,只剩 Structure(broken anchor)有用。 需先回填created才能讓 drift 在 Mac scan 發揮價值。- per-PR gate 與深度 scan 分屬兩個環境/工具,心智負擔比「單一工具一條龍」高。
中性:
- OpenSpec OSS root 解析需
working-directory: docs,是設定細節而非阻礙。 validate --all在本機會誤掃.claude/worktrees/*副本;CI 乾淨 checkout 無此問題,反而比 本機更準。- 本 ADR 只定「工具與環境邊界」;兩道 gate 的完整實作步驟見方案計畫
~/.claude/plans/gate-ci-merry-dewdrop.md,後續以實作 change 落地並回填related.change。
Postscript — 2026-07-03 實測 parity(issue #665 Part A Phase 1 落地)
實作前依「驗證步驟 1」實測 @fission-ai/openspec@1.5.0 vs spectra 2.3.1
(validate --changes [--strict],--strict 與否結果相同)。結論:parity 不成立——
OSS 為 stricter superset。 本 ADR「確認兩者對 0036 等案例判定一致」的假設需修正如下。
OSS 1.5.0 額外以 ERROR(Spectra 2.3.1 不擋)強制:
- 每個
### Requirementbody 必含SHALL/MUST(RFC 2119); - 每個 requirement 至少一個
#### Scenario:block; - 更嚴的 delta-section 結構(例:change
0041被判 "No deltas found")。
實測分歧範圍:Spectra 14/14 valid(0036 修 header 後);OSS 8/14 valid,額外判 invalid 的
是 0019 0020 0031 0036 0039 0041(皆 pre-existing、他人進行中的 change)。
對 Decision 的影響(不推翻,收斂實作)
- Decision 核心(OSS
validate上 ubuntu、drift/analyze留 Mac)維持。 - 分歧由既有兩項緩解吸收:(a) diff-scoped——gate 只驗每個 PR 動到的 change,pre-existing
OSS-only 債不擋無關 PR;(b) Phase 1 report-only——job 不進
ci-status、exit 0恆真, 純 PR comment。故 Phase 1 安全落地。 - Phase 2(硬 gate)新增前置條件:上述 6 個 baseline change 需先清到 OSS-valid(各 change
owner 協調),才可加
--fail-on-diff-invalid並接進ci-status。追蹤見 issue #703。 - 版本治理:CI pin 記於
docs/openspec/spectra-vendor/CI_OPENSPEC_VERSION,分歧量測納入 ADR-0006 Layer 2 vendor-drift 審查(見該 ADR 2026-07-03 Postscript)。
實作 PR:#702(issue #665 Part C 前置清理 + Part A Phase 1 report-only)。Phase 2 follow-up:
issue #703。related.change 仍為 N/A(純 CI/toolchain,無對應 OpenSpec capability delta)。
Phase 2 硬 gate 上線後,本 ADR 由 proposed 轉 accepted。
Decision 內描述的 Mac 端
make spectra-scan(定期 full-scope)屬 issue #665 Part B,尚未 實作;本 PR 僅落地 ubuntu 端的 per-PRvalidategate。在 Part B 落地前,make spectra-scan是規劃中的目標,非可執行指令。
Postscript — 2026-07-03 Phase 2 準備:OSS 不支援巢狀 capability(決策:gate 跳過)
推進 Phase 2 硬 gate 時,實測發現 OSS 1.5.0 無法解析本專案的巢狀 capability 布局
(specs/ENN-epic/FMM-feature/spec.md,openspec/CLAUDE.md「巢狀 spec」+issue #492 明訂的
支援慣例)。實例:change 0041 的 spec 結構完整(## MODIFIED Requirements+### Requirement
+#### Scenario+SHALL/MUST 齊備),但 OSS 對整個 change 回報 No deltas found——因為它只掃
specs/<cap>/spec.md 一層,看不到 specs/E02-child-profile/F028-.../spec.md。macOS Spectra
binary 則能正確解析。
這比「6 個 baseline 債」更根本:硬 gate 一旦上線,會擋下所有未來的巢狀-cap change PR, 而巢狀-cap 是專案支援的合法結構。
決策(deciders: howie,2026-07-03):gate 跳過巢狀-cap change(不硬擋),改由 Mac 端
spectra validate 覆蓋。實作於 scripts/openspec_validate_report.py
(change_has_nested_caps 結構偵測 → report 顯示 SKIPPED、--fail-on-diff-invalid 不計入)。
單層-cap change 仍走硬 gate。長期若 openspectra(Linux-native CLI)支援巢狀 cap,可再納入硬 gate。
Phase 2 硬 gate 上線的剩餘前置(本 PR 未 flip,維持 report-only):
- 5 個單層-cap baseline change(
0019/0020/0031/0036/0039)清到 OSS-valid——各 change owner 執行,清單見 issue #703。(0041因巢狀已由 gate 跳過,不在此列。) - baseline 清乾淨後:
ci.yml的 validate step 加--fail-on-diff-invalid+把 job 接進ci-statusneeds:與 failure rollup。 - 屆時本 ADR 由
proposed轉accepted。
本 PR(Phase 2 準備):實作巢狀-cap 跳過邏輯+測試,維持 report-only。related.change 續為 N/A。
Postscript — 2026-07-03 Accepted:Phase 2 dry-run 開關已開啟(issue #720 P1-1 / Q1 裁決)
本 ADR 依 2026-07-03 owner 裁決(deciders: howie,issue #720 Q1)由 proposed
轉 accepted:Phase 2 開關(--fail-on-diff-invalid)啟用,required-check
轉正在一週 dry-run 後執行。裁決內容:
scripts/openspec_validate_report.py的--fail-on-diff-invalid在.github/workflows/ci.yml的openspec-validatejob 開啟。- 開關本身即是 dry-run:job 仍不進
ci-status的needs:與 failure rollup,branch-protection 層級也維持非 required check(該項屬 GitHub repo settings 變更,由 owner 之後另行執行)。因此 job 現在可以變紅(診斷任何 touched change 為 diff-invalid),但紅燈不會擋 merge——這正是 dry-run 要觀察的訊號。 - dry-run 週期一週,預定 2026-07-10 檢視期間是否有 false positive(例如
OSS 1.5.0 比 Spectra 2.3.1 更嚴格判到的假警報,或巢狀 cap 跳過邏輯的邊界案例)。
一週後若無 false positive,才將此 job 加入
ci-statusneeds:陣列並在 GitHub repo settings 設為 required check(Phase 2 正式轉硬 gate)。 - 前一版本 Postscript 列出的「5 個 baseline change 需先清到 OSS-valid」前置 條件(issue #703)不再是本次開關的阻擋條件——因為 gate 是 diff-scoped, 只在 PR 實際觸碰到該 change 的 delta-spec 時才會判定,且 dry-run 期間紅燈 不擋 merge。清理 baseline debt 仍應在 issue #703 追蹤,作為降低 dry-run 期間雜訊、及未來轉正式硬 gate 前的收斂項目,但不再是「開啟 dry-run 開關」 本身的前置。
- 本 postscript 明確取代(supersede)前兩則 postscript 中「Phase 2 硬 gate
上線後,本 ADR 才由
proposed轉accepted」的時序條件:依 Q1 裁決, accepted 與 dry-run 開關同步生效,不等 required-check 轉正。前兩則 postscript 的該句自此僅具歷史紀錄性質。