refactor(role): 角色檔拆成身分(identity)與人格(soul)兩檔

使用者反映身分設定與性格語氣擠在同一段(例如西莉卡的「馴獸師、養畢娜」是身分,
「細心、努力」是性格),要求拆成兩個檔案且不得遺失內容。

格式(扁平式,與既有 .assets/.checks/.lock 命名一致):
- <ID>.identity.md:角色 ID、顯示名稱、來源作品、與使用者的關係定位、簽名 emoji
- <ID>.soul.md:本質(nature)、氛圍(vibe)

共用行為規則**不再寫入角色檔**:role_load.sh 從不讀角色檔裡那份,它是冗余副本,
只會多一個漏同步的機會。內容完整保留於 role_load.sh(實際生效)與 SKILL.md(文件)。

- role_lib.sh:新增 role_identity_file/role_soul_file/role_legacy_file/
  role_is_new_format;role_file 改為新格式優先、找不到退回舊檔,既有角色不受影響
- role_list_peers 支援兩種格式並避免同一角色重複列出
- role_load.sh:身分與人格分別讀取,新增「來源」與「關係定位」注入區塊
- 新增 --migrate <角色 ID>:逐字搬移本質、氛圍與簽名 emoji,來源與關係定位產生
  待填空白,舊檔保留不動,新檔已存在時中止不覆寫

同時修掉兩個會造成實際損失的錯:
- --export 原本硬編 cp 成 <ID>.md,新格式會被寫成舊檔名且遺失人格檔,備份救不回角色
- --export 從未備份 <ID>.checks(使用者自訂的晨間檢查腳本),一併補上
- --agent 原只讀 role_file(新格式即 identity),匯出的 sub agent 人格會是空的

驗證:遷移後逐字比對確認本質/氛圍/簽名 emoji/id/name/emoji/created 全部一致
且共用行為未寫入;新格式匯出的備份含 identity、soul、舊檔、assets、checks 與記憶;
--agent 取得人格非空;--status 正確顯示格式與兩檔路徑;舊格式角色載入完全不受影響。

版號沿用 0.0.5(master 為 0.0.4,同一 PR 不再累加)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-29 11:12:34 +08:00
co-authored by Claude Opus 5
parent 241261087b
commit f76915bc87
4 changed files with 276 additions and 117 deletions
+46 -6
View File
@@ -69,9 +69,36 @@ role_resolve_name() {
printf '%s' "$name"
}
# ------------------------------------------------------------------------------
# 角色定義檔:身分(IDENTITY)與人格(SOUL)分離
#
# 新格式把「我是誰」與「我怎麼想」拆開,避免身分設定(來源作品、關係定位)與
# 性格語氣擠在同一段裡:
# <角色目錄>/<ID>.identity.md 角色 ID、顯示名稱、來源、關係定位、簽名 emoji
# <角色目錄>/<ID>.soul.md 本質(nature)、氛圍(vibe
#
# 舊格式為單一 <ID>.md,仍完整支援:解析時新格式優先,找不到才退回舊檔,
# 既有角色不會因升級而失效。可用 role_sleep.sh --migrate <ID> 拆成新格式。
# ------------------------------------------------------------------------------
role_identity_file() { printf '%s/%s.identity.md' "$(role_home)" "$1"; }
role_soul_file() { printf '%s/%s.soul.md' "$(role_home)" "$1"; }
role_legacy_file() { printf '%s/%s.md' "$(role_home)" "$1"; }
role_is_new_format() {
# 只要有 identity 檔就視為新格式(soul 缺失時由呼叫端各自處理)
[ -f "$(role_identity_file "$1")" ]
}
role_file() {
# 指定角色的定義檔路徑
printf '%s/%s.md' "$(role_home)" "$1"
# 角色「主定義檔路徑:新格式回傳 identity,否則回傳舊的單一檔。
# 保留此函式是為了不動既有「檔案存在即代表角色存在」的判斷邏輯。
local id="$1"
if [ -f "$(role_identity_file "$id")" ]; then
role_identity_file "$id"
else
role_legacy_file "$id"
fi
}
role_enabled() {
@@ -336,16 +363,29 @@ role_instance_acquire() {
# 列出可協作的其他角色(排除自己),每行「ID<TAB>顯示名稱<TAB>本質摘要」。
# 角色若不知道有哪些同伴存在,就不會想到派他們協助 —— 這是多人協作能運作的前提。
role_list_peers() {
local self="$1" home file id name nature
local self="$1" home file id name nature soul seen_ids=""
home="$(role_home)"
[ -d "$home" ] || return 0
for file in "$home"/*.md; do
for file in "$home"/*.identity.md "$home"/*.md; do
[ -f "$file" ] || continue
id="$(basename "$file" .md)"
case "$file" in
*.soul.md) continue ;; # soul 不是主定義檔
*.identity.md) id="$(basename "$file" .identity.md)" ;;
*) id="$(basename "$file" .md)"
# 舊檔若已有對應的新格式,避免同一角色列兩次
[ -f "$(role_identity_file "$id")" ] && continue ;;
esac
[ "$id" = "$self" ] && continue
case " ${seen_ids} " in *" ${id} "*) continue ;; esac
seen_ids="${seen_ids} ${id}"
name="$(sed -n 's/^name:[[:space:]]*//p' "$file" 2>/dev/null | head -n 1)"
nature="$(sed -n 's/^nature:[[:space:]]*//p' "$file" 2>/dev/null | head -n 1)"
# frontmatter 沒有 nature 時退回讀「## 本質」段落的第一
# 新格式的性格在 soul 檔;frontmatter nature 時退回讀「## 本質」段落
soul="$(role_soul_file "$id")"
if [ -z "$nature" ] && [ -f "$soul" ]; then
nature="$(sed -n 's/^nature:[[:space:]]*//p' "$soul" 2>/dev/null | head -n 1)"
[ -n "$nature" ] || nature="$(sed -n '/^## 本質/,/^## /p' "$soul" 2>/dev/null | sed '1d;/^##/d;/^[[:space:]]*$/d' | head -n 1 | cut -c1-60)"
fi
if [ -z "$nature" ]; then
nature="$(sed -n '/^## 本質/,/^## /p' "$file" 2>/dev/null | sed '1d;/^##/d;/^[[:space:]]*$/d' | head -n 1 | cut -c1-60)"
fi