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:
@@ -44,13 +44,13 @@ issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆
|
||||
|
||||
## 3. 跨 repo 署名(鐵律 — 絕不可漏)
|
||||
|
||||
所有 repo(mira / 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,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,**不地毯灌原文全文**。
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
# role-lib.sh — 兩軸身分判定函式庫(被 source,不獨立掛 hook)
|
||||
#
|
||||
# 出處:《分離導入規格》第二節「兩軸身分(scope × role,全部機械判定,agent 不自我判斷)」
|
||||
# +《JDD 導入規格》第四節「角色與權限」。
|
||||
#
|
||||
# ── 為什麼是「兩軸」而不是「一個角色設定」──────────────────────
|
||||
# scope(我在誰的地盤)=**安裝位置**決定:agent 在哪個資料夾醒來,
|
||||
# 就只讀得到那部憲法。來源= system-dev/.profile(install 時寫死)。
|
||||
# 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。PM(orchestrator)的動作——改 root.md/journeys.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 ────────────────────────
|
||||
# 三段 fallback:jq → 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)"
|
||||
}
|
||||
@@ -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> (<日期>)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user