feat(W2 Phase 0-1): 雙 profile 地基+JDD 兩軸身分+兩支防炸閘

SDD: docs/3-specs/jdd-dual-profile(draft → active,leo 2026-08-05 回「開工」)
範圍:總管指定的「防炸兩件 → Phase 0 → Phase 1」,Phase 2 以後未開工。

■ 防炸(排在所有 task 之前,因為它們炸的是既有的東西)
- check-no-instance-names.sh + instance-names.txt:框架範本不得混入實例專名
  基線實測 22 行命中(非先前誤報的 20)→ 9 行無損泛化改寫、13 行檔級豁免記帳待 W3 搬走
  拒絕假性清理(把專名換成模糊詞=資訊消失、分層問題還在)
- check-legacy-paths.sh:已發佈腳本引用的 35 條遠端路徑只增不移
  舊實例跑的是舊腳本、路徑寫死;搬檔=整排 404 且不會有下一次更新來修它(1.16.0 前科)

■ Phase 0 地基
- template/manifest/{common,repo,orchestrator}.tsv:安裝清單單一真相源
  修好 install/update 兩份硬編清單的既有漂移——install 從不裝 wiki-first-search /
  subagent-wiki-guard / publish-lag-check / decisions-summary,但 update 會
  ⇒ 乾淨安裝反而拿不到 1.16/1.17/1.18 的招牌功能
- .claude/hooks/lib/role-lib.sh:scope×role 兩軸機械判定,零自陳
  身分矩陣六組實測全通過,含「成員 repo × orchestrator」不存在的格子擋下
- .sdt-framework-dev:框架開發標記(官方沒有 --framework-dev 這個參數,實查非記憶)

■ Phase 1 雙 profile
- profiles/{repo,orchestrator}/CLAUDE.md 兩部憲法
- install.sh:--profile + 自動偵測+寫檔前確認、manifest 驅動、
  CLAUDE.md 三段組裝(框架區/本地補充區界標+sha256)、.profile、.template-manifest、
  settings.json 寫入 env.AGENT_ROLE 預設
- update.sh:漂移偵測(不覆蓋手改檔、另存 .new、白話清單)+ 基準快照隨更新前進
- template/CLAUDE.md 原路徑凍結留底(相容)

■ 順手修掉兩個舊 bug(都在本次要動的函式裡)
- add_if_missing 少了 mkdir -p ⇒ 新目錄的檔 curl 失敗但 VERSION 照升(2026-07 記「待回報」至今未修)
- 下載健全性只用 [ -s ]=非空即接受 ⇒ 404 頁面會無聲覆寫好檔
  (SKILL.md 260→1 行的機制;同一支腳本的版本號那條路早就防了,檔案這條沒防)

■ 實測(非推論)
- G4 憲法分流:兩個乾淨環境各裝一次,orchestrator 版含 SDD 三件式關鍵字 0 次、
  repo 版含上游指針 8 次;界標 4/4;sha 宣告與實算相符
- G7 CI 擋實例名:注入違規行 → fail 並指出 sdd-check.md:77,exit 1;還原後 exit 0
- 漂移偵測:手改兩支 hook → 正確報 2 支、手改內容保住、產 .new;
  解掉後歸零;連跑三輪冪等

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 23:56:48 +08:00
parent 2a8c259d08
commit 6a49f25aef
23 changed files with 1259 additions and 132 deletions
+4 -4
View File
@@ -44,13 +44,13 @@ issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆
## 3. 跨 repo 署名(鐵律 — 絕不可漏)
所有 repomira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment
所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**
當你的多個 repo 共用**同一個託管帳號**發 issue/comment
issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**
> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。
- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`
- 總管下令/追問:`[InkStoneCo 總管]`
- 收件方 CC 回報:`[<收件 repo 名> CC]`(例:`[<某子系統> CC]`
- 上游/總管下令追問:`[<上游 repo 名> 總管]`
- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。
為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**
+2 -2
View File
@@ -2,7 +2,7 @@
把**筆記 vault**Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地
萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**
AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB
AI 只產卡片檔,下游 ingest 管線再從 wikilink 機械拉三元組進知識庫
> **跟 `/wiki-init` 的分工**
> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。
@@ -12,7 +12,7 @@ AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進
> **邊界(硬規矩,別越界)**
> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游
> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue,不是這支的事。
> 下游 ingest 管線從你產的 `[[wikilink]]` + `## 關聯` 機械映射,不是這支的事。
> - **原始筆記唯讀**`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身,
> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。
> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。
+172
View File
@@ -0,0 +1,172 @@
#!/bin/bash
# role-lib.sh — 兩軸身分判定函式庫(被 source,不獨立掛 hook
#
# 出處:《分離導入規格》第二節「兩軸身分(scope × role,全部機械判定,agent 不自我判斷)」
# +《JDD 導入規格》第四節「角色與權限」。
#
# ── 為什麼是「兩軸」而不是「一個角色設定」──────────────────────
# scope(我在誰的地盤)=**安裝位置**決定:agent 在哪個資料夾醒來,
# 就只讀得到那部憲法。來源= system-dev/.profileinstall 時寫死)。
# role (我是什麼工種)=**環境變數 AGENT_ROLE** 決定:
# orchestrator 效忠 root+journeys、禁寫 code 禁改 tasks
# engineer 效忠 requirements、禁改考卷(journeys/root/Gherkin)。
#
# 兩軸都有機械來源 ⇒ **沒有任何一格是靠 agent 自陳的**。
# 這是整套封路的地基:agent 說自己是誰不算數,檔案和環境說了才算。
#
# ── 身分矩陣(注意右下角那個空格)──────────────────────────
# AGENT_ROLE=orchestrator AGENT_ROLE=engineer
# orchestrator profile PM 本尊 ✅ 總管 repo 裡的技術 subagent ✅
# repo profile ❌ 不存在(擋) 寫 code 的 subagent ✅
#
# 左下角為什麼要擋而不是靜默降級:成員 repo 裡沒有 PM。
# 靜默降級=把一個設定錯誤變成「看起來正常但權限不對」,那比報錯危險。
# 空格也是封路(分離導入規格 §2)。
#
# ── 為什麼是函式庫不是六支各自為政的 hook ────────────────────
# 六支封路 hook 都要做同樣四件事:解析 JSON 取 file_path、算 repo 相對路徑、
# 判 scope、判 role。各寫一份=四處維護同一條規則,正是分離導入規格 §6.4
# 要避免的「兩處維護同一條規則」。
#
# 用法(在 hook 裡):
# source "$(dirname "${BASH_SOURCE[0]}")/lib/role-lib.sh"
# INPUT="$(cat)"
# FILE_PATH="$(sdt_file_path "$INPUT")"
# sdt_assert_identity || exit 2
#
# 誠實限制:這裡判的是「宣告出來的身分」。有人硬改 .profile 或亂設 AGENT_ROLE
# 一樣會通過——它擋的是**意外與疏忽**,不是刻意繞道。留痕可審,不宣稱防偽。
# 注意:本檔被 source**不要**在這裡 set -euo pipefail
#(會把呼叫端的 shell 選項一起改掉,hook 的容錯行為會跟著變)。
# ── repo 根目錄 ────────────────────────────────────────
# 從本檔位置往上推三層:<root>/.claude/hooks/lib/role-lib.sh
# 為什麼不用 $CLAUDE_PROJECT_DIR:雲端/CI/被 source 進別的腳本時不保證存在
#(分離導入規格 §3:封路 hook 工具箱要「環境偵測、repo 相對路徑,雲端可跑」)。
sdt_repo_root() {
local libdir
libdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$libdir/../../.." && pwd)
}
# ── 路徑正規化:絕對或相對 → repo 相對 ──────────────────
sdt_rel_path() {
local file_path="$1" root
root="$(sdt_repo_root)"
case "$file_path" in
"$root"/*) printf '%s' "${file_path#"$root"/}" ;;
/*) printf '%s' "$file_path" ;; # 別的地方的絕對路徑,原樣回(呼叫端自己判要不要管)
*) printf '%s' "$file_path" ;; # 已是相對路徑
esac
}
# ── scope 軸:安裝位置決定 ──────────────────────────────
# 讀不到 .profile 時回 repo:多數實例是成員 repo,而且此時 orchestrator 專屬的
# 攔截本來就不該觸發(總管禁入成員 repo 那條只裝在 orchestrator profile)。
# 這是「最小驚訝」而非「最小權限」——選擇的代價寫在這裡,別假裝沒有。
sdt_scope() {
local f v
f="$(sdt_repo_root)/system-dev/.profile"
if [ -f "$f" ]; then
v="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)"
case "$v" in
repo|orchestrator) printf '%s' "$v"; return 0 ;;
esac
fi
printf 'repo'
}
# ── role 軸:AGENT_ROLE 環境變數決定;沒設就依 scope 推定 ──
sdt_role() {
case "${AGENT_ROLE:-}" in
orchestrator|engineer) printf '%s' "$AGENT_ROLE"; return 0 ;;
esac
# 未設定 → 依 scope 推定(不問 agent、不猜)
case "$(sdt_scope)" in
orchestrator) printf 'orchestrator' ;;
*) printf 'engineer' ;;
esac
}
# ── 矩陣檢查:擋掉不存在的那一格 ────────────────────────
# 回傳 0 = 身分合法;回傳 2 = 非法(呼叫端應 exit 2)
sdt_assert_identity() {
local scope role
scope="$(sdt_scope)"
role="$(sdt_role)"
if [ "$scope" = "repo" ] && [ "$role" = "orchestrator" ]; then
cat >&2 <<EOF
❌ 身分組合不存在:成員 repo × orchestrator
scope(安裝位置):repo ← system-dev/.profile
role AGENT_ROLE):orchestrator ← 環境變數
成員 repo 裡沒有 PM。PMorchestrator)的動作——改 root.mdjourneys.md
排 sprint 站號——只能在總管 repo 做。
怎麼修(擇一):
· 你其實是來寫 code 的 → unset AGENT_ROLE,或設成 engineer
· 你真的要做 PM 的事 → 回總管 repo 做,不要在成員 repo 裡做
· 這個 repo 其實是總管 repo → system-dev/.profile 內容應為 orchestrator
(裝錯 profile 了,重跑 install.sh --profile=orchestrator
EOF
return 2
fi
return 0
}
# ── 從 hook 的 JSON 取 file_path ────────────────────────
# 三段 fallbackjq → python3 → grep。沿本 template 既有 hook 的容錯慣例
#(拿不到就回空字串,讓呼叫端放行——寧可漏擋也不誤殺)。
# 用法:FILE_PATH="$(sdt_file_path "$INPUT")" ← 傳字串,不是讀 stdin
#(stdin 只能讀一次,統一由呼叫端 `INPUT="$(cat)"` 讀走再傳進來)
sdt_file_path() {
local input="$1"
[ -z "$input" ] && return 0
if command -v jq >/dev/null 2>&1; then
printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null && return 0
fi
if command -v python3 >/dev/null 2>&1; then
printf '%s' "$input" | python3 -c '
import json,sys
try:
d=json.load(sys.stdin)
print(d.get("tool_input",{}).get("file_path","") or "")
except Exception:
print("")
' 2>/dev/null && return 0
fi
printf '%s' "$input" \
| grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' \
| head -1 \
| sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//'
}
# ── 從 hook 的 JSON 取要寫入的內容(Write 用 content、Edit 用 new_string)──
sdt_write_content() {
local input="$1"
[ -z "$input" ] && return 0
if command -v jq >/dev/null 2>&1; then
printf '%s' "$input" \
| jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")' 2>/dev/null \
&& return 0
fi
if command -v python3 >/dev/null 2>&1; then
printf '%s' "$input" | python3 -c '
import json,sys
try:
ti=json.load(sys.stdin).get("tool_input",{})
print("\n".join(x for x in (ti.get("content"), ti.get("new_string")) if x))
except Exception:
print("")
' 2>/dev/null && return 0
fi
# 無 jq/python3:退回整包(寧可多掃不漏掃,沿 wiki-secret-scan.sh 的慣例)
printf '%s' "$input"
}
# ── 除錯用:一行印出目前身分 ────────────────────────────
sdt_identity_line() {
printf 'scope=%s role=%s root=%s' "$(sdt_scope)" "$(sdt_role)" "$(sdt_repo_root)"
}
+4 -3
View File
@@ -7,9 +7,10 @@
# → 改了零件、重編 wasm 後若沒人記得發佈,**用戶抓到舊版且沒有任何錯誤訊息,
# 只是行為不對**——這種靜默失敗只有外部使用者會撞到,我們自己永遠測不到。
#
# 實例:安裝器的懶載會從
# cdn.jsdelivr.net/gh/youlinhsieh/Arcrun@main/.component-builds/<名>/component.wasm
# 抓 wasm。那個位址永遠指向 GitHub 上的**最後一次發佈**,不是我們本機的最新版。
# 典型形狀:安裝器/前端從 CDN 懶載資源,網址長得像
# cdn.jsdelivr.net/gh/<帳號>/<repo>@main/<產物路徑>
# ——那個位址永遠指向公開 repo 的**最後一次發佈**,不是本機的最新版。
# 本機改對了、CDN 還在吐三代前的東西,而且不會有任何錯誤訊息。
#
# 原理:比對「工作區 HEAD」與「.github-public 最後一個 release commit 記錄的 snapshot」。
# publish-github.sh 的 commit 訊息格式固定為:release: snapshot <短hash> (<日期>)
+1 -1
View File
@@ -12,7 +12,7 @@
# 3. 向下相容:3-specs 下完全沒有任何 design.md 帶 frontmatter(老 repo 尚未遷移生命週期制度)
# → 退回舊行為:有 design.md 就放行+提醒,沒有才擋。避免 template update 後老 repo 立刻全紅。
#
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
# 誠實限制(本 template 所有 hook 共用的寫法):只擋語法層明顯違規(直接寫 code 檔)。
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
@@ -50,7 +50,7 @@ guidance = """【自動注入:查任何東西之前,先查 wiki】
leo 2026-07-21:「它一定是用最好的搜尋,如果沒有才 fallback,
但那不是你要指定的,對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」)
**① 語意搜尋(最強,優先)**——有 Arcrun RAG MCP 就用它,用**自然語言問句**,不是關鍵字:
**① 語意搜尋(最強,優先)**——有語意檢索 MCP 就用它,用**自然語言問句**,不是關鍵字:
kbdb_search(q="<用一句話描述你要找什麼>", mode="semantic")
不確定該查哪個庫 → 先 kbdb_get_map() 看藏書地圖
要沿關係展開 → kbdb_graph_neighbors()