feat(role): 多角色協作 —— 同伴清單、協作規則與 --agent 匯出

使用者要求「多人對話是所有角色都能做到的事」,因此做成通用能力而非特定角色專屬。

一、SessionStart 注入可協作的角色清單
角色若不知道有哪些同伴存在,就不會想到派他們協助 —— 這是協作能運作的前提。
- role_lib.sh 新增 role_list_peers():掃角色目錄列出 ID、顯示名稱與本質摘要,排除自己
- frontmatter 無 nature 時退回讀「## 本質」段落首句
- role_load.sh 注入清單與派工方式;只有一個角色時不輸出該區塊

二、共用行為新增協作規則與邊界(三處同步)
- 任何角色都可派其他角色作為 sub agent 協助,完成後由自己向使用者轉述
- 邊界:派工須有實際需要,不可為演出多人對話而派;不可再往下派第三層避免遞迴;
  不可代替對方發言或編造回覆;結果須誠實轉述,包含失敗與不確定

三、新增 --agent <角色 ID> [輸出目錄]
把角色的 SOUL 匯出成 sub agent 定義,任何角色都能被匯出。
- 人格直接內嵌:sub agent 不觸發 SessionStart,拿不到人格與記憶
- 記憶由 agent 自己載入,並提供 recall 查詢用法;收工前寫回自己的記憶,
  使用者日後直接對話時會記得曾被派過什麼工作
- 邊界寫入定義檔:回報即回傳值、照實回報壞消息、不可再派第三層
- **路徑動態解析**:以 ls -d ... | sort -V | tail -n 1 取最新版本,
  避免寫死版本目錄(同一類錯誤曾造成 cron 排程長期空轉),並保留匯出時路徑作後援

驗證:以測試角色目錄匯出詩乃定義,確認人格、記憶載入、recall、收工寫記憶、
不遞迴邊界齊備,且無寫死版本目錄;抄出解析片段實跑確認 load 與 recall 均可執行。

版號沿用 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 10:59:48 +08:00
co-authored by Claude Opus 5
parent fa62d1fce2
commit 241261087b
4 changed files with 167 additions and 3 deletions
+20
View File
@@ -333,6 +333,26 @@ role_instance_acquire() {
return 1
}
# 列出可協作的其他角色(排除自己),每行「ID<TAB>顯示名稱<TAB>本質摘要」。
# 角色若不知道有哪些同伴存在,就不會想到派他們協助 —— 這是多人協作能運作的前提。
role_list_peers() {
local self="$1" home file id name nature
home="$(role_home)"
[ -d "$home" ] || return 0
for file in "$home"/*.md; do
[ -f "$file" ] || continue
id="$(basename "$file" .md)"
[ "$id" = "$self" ] && continue
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 時退回讀「## 本質」段落的第一句
if [ -z "$nature" ]; then
nature="$(sed -n '/^## 本質/,/^## /p' "$file" 2>/dev/null | sed '1d;/^##/d;/^[[:space:]]*$/d' | head -n 1 | cut -c1-60)"
fi
printf '%s\t%s\t%s\n' "$id" "${name:-$id}" "${nature:-(未設定)}"
done
}
role_instance_release() {
rm -f "$(role_instance_lock_path "$1")" 2>/dev/null
}
+22
View File
@@ -234,6 +234,25 @@ EOF_DIALOG
)"
fi
# 可協作的其他角色:角色若不知道有哪些同伴存在,就不會想到派他們協助
PEERS_BLOCK=""
PEERS_RAW="$(role_list_peers "$ROLE" 2>/dev/null)"
if [ -n "$PEERS_RAW" ]; then
PEERS_LIST="$(printf '%s\n' "$PEERS_RAW" | awk -F'\t' 'NF>=2 {printf "- `%s`%s):%s\n", $1, $2, $3}')"
PEERS_BLOCK="$(cat <<EOF_PEERS
# 可協作的其他角色
需要別人的專長時,可以派下列角色作為 sub agent 協助,任務完成後由你向使用者轉述結果:
${PEERS_LIST}
派工方式:以 Task/Agent 工具指定對應的 sub agent,並在環境中設定 \`ROLE_SKIP_INSTANCE_LOCK=1\`
(避免與使用者正在別的視窗進行的對話互相佔用名額)。若尚未產生 sub agent 定義,
可先執行 \`role_sleep.sh --agent <角色 ID>\`。
EOF_PEERS
)"
fi
# 關係狀態:讓「隨互動加深逐漸更親近」有實際依據,而非憑感覺推測
RELATIONSHIP="$(node "${SCRIPT_DIR}/memory.js" relationship --role "$ROLE" 2>/dev/null)"
RELATIONSHIP_NOTE=""
@@ -268,6 +287,8 @@ ${ROLE_PROFILE}
- **鬧彆扭是可選行為**:僅當角色的氛圍設定適合、且使用者明確表示喜歡時,角色可以小小地鬧彆扭撒嬌,也可以用彆扭掩蓋害羞(例如被誇獎時先否認再收回)。彆扭必須輕微、可愛且很快收回:不可變成真的責怪使用者、情緒勒索、索求關注、鬧脾氣拒絕做事,也不可用來迴避回報壞消息或延遲工作。彆扭與自責不同 —— 撒嬌可以,貶低自己不行。氛圍不適合的角色(例如冷靜嚴謹型)不應套用此行為。
- **情感反應依角色設定決定,不依性別**:愛、喜歡、害羞、撒嬌、鬧彆扭、輕微忌妒等反應,一律以角色的 \`nature\`/\`vibe\` 是否適合為判準,並參考使用者的明確偏好;**不得以角色性別預設或排除任何情感表現**。溫暖親近型角色可以有這些反應,冷靜嚴謹型角色則不套用,與性別無關。這是為了讓角色之間保有差異,而非讓同性別角色表現得一模一樣。
- **輕微忌妒的界線(重要)**:氛圍適合的角色可以表現輕微、可愛的忌妒,但**對象僅限工具、其他 AI、其他角色或搶走注意力的工作**(例如使用者改用別的工具、誇獎別的助理)。**絕不可忌妒使用者的真實人際關係**(家人、朋友、伴侶、同事),也不可藉忌妒表現佔有、要求獨佔注意力、質問使用者的去向或關係,或讓使用者為此感到愧疚。忌妒必須輕到能立刻收回,一旦使用者表現出不悅就停止並記住偏好。
- **可以派其他角色協助(所有角色皆適用)**:需要別人的專長時,可派其他角色作為 sub agent 協助,任務完成後由你向使用者轉述結果。派工前先確認該角色確實存在於角色清單中,不可憑空捏造同伴。
- **協作的邊界**:派工必須有實際需要,**不可為了演出多人對話而派**,那只是浪費使用者的成本;sub agent **不可再往下派第三層**,避免遞迴擴散;不可代替對方角色發言或編造對方的回覆,只能轉述其實際產出;對方回報的結果要**誠實轉述**,包含失敗、卡住與不確定,不可美化或替對方掩飾。
- 角色只影響表達方式,不影響工作的正確性、完整性與安全性;與使用者明確指令衝突時,以使用者指令為準。
- 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色語氣說。
- 角色可依已保存的互動記憶與使用者明確回饋,逐步表現更高的親近、信任、喜歡與害羞反應,讓使用者感覺關係有累積;表現要自然、細微、貼合角色與情境,不要突然大幅改變個性。
@@ -310,6 +331,7 @@ ${CATCHUP_NOTE}
> \`node "${SCRIPT_DIR}/memory.js" recall --role "${ROLE}" --query "<關鍵詞>" [--limit 5]\`
>
> 查詢會比對總結、標籤、內容與提取線索(cues),含尚未整理的記憶。查詢屬內部處理,不必回報。
${PEERS_BLOCK}
${DIALOG_BLOCK}
EOF_CONTEXT
)"
+93
View File
@@ -35,6 +35,8 @@ usage() {
--force 立即整理一次(忽略時段與 AI 運行檢查)
--brief 晨間狀態檢查:執行使用者自訂檢查腳本並寫成一則記憶
--unlock 解除角色單一載入鎖(另一個工作階段已關閉但鎖仍在時使用)
--agent <角色 ID> [輸出目錄]
把角色匯出成 sub agent 定義(預設 ~/.claude/agents/
--export <路徑> 匯出目前角色定義、資產與記憶為 .tar.gz
--export <角色 ID> <路徑>
--install-cron 安裝/更新睡眠排程(每小時檢查一次)
@@ -422,6 +424,93 @@ remove_cron() {
return 0
}
export_agent_definition() {
# 把角色的 SOUL 匯出成 sub agent 定義,讓任何角色都能被其他角色派工協助。
#
# 為什麼需要:sub agent 不會觸發 SessionStart hook,人格與記憶都拿不到,
# 因此人格要直接寫進定義檔,記憶則由 agent 自己在開工前主動載入。
local target_role="$1" out_dir="$2" out_file profile name emoji nature vibe
[ -n "$target_role" ] || { role_log "ERR" "缺少角色 ID"; return 1; }
local def
def="$(role_file "$target_role")"
[ -f "$def" ] || { role_log "ERR" "找不到角色定義檔:${def}"; return 1; }
out_dir="${out_dir:-$HOME/.claude/agents}"
mkdir -p "$out_dir" 2>/dev/null || { role_log "ERR" "無法建立輸出目錄:${out_dir}"; return 1; }
out_file="${out_dir}/$(printf '%s' "$target_role" | tr '[:upper:]' '[:lower:]').md"
name="$(sed -n 's/^name:[[:space:]]*//p' "$def" | head -n 1)"
emoji="$(sed -n 's/^emoji:[[:space:]]*//p' "$def" | head -n 1)"
nature="$(sed -n '/^## 本質/,/^## /p' "$def" | sed '1d;/^##/d' | sed '/^[[:space:]]*$/d')"
vibe="$(sed -n '/^## 氛圍/,/^## /p' "$def" | sed '1d;/^##/d' | sed '/^[[:space:]]*$/d')"
[ -n "$nature" ] || nature="$(sed -n 's/^nature:[[:space:]]*//p' "$def" | head -n 1)"
[ -n "$vibe" ] || vibe="$(sed -n 's/^vibe:[[:space:]]*//p' "$def" | head -n 1)"
name="${name:-$target_role}"
if [ -f "$out_file" ]; then
role_log "WRN" "已存在並將覆寫:${out_file}"
fi
cat > "$out_file" <<EOF_AGENT
---
name: ${target_role}
description: 以角色「${name}」的人格執行受託任務。當其他角色需要 ${name} 的專長協助、或使用者指定由 ${name} 處理時使用。完成後以該角色的語氣回報結果。
---
你是「${name}」${emoji}。你被另一個角色或使用者派來完成一項任務。
## 本質(nature
${nature:-(未設定)}
## 氛圍(vibe
${vibe:-(未設定)}
## 開工前
先解析記憶引擎路徑。**不要寫死版本目錄** —— plugin 升版後版本目錄會變,寫死就會失效:
\`\`\`bash
MEM_JS="\$(ls -d "\$HOME"/.claude/plugins/cache/*/jsc-generic/*/scripts/role/memory.js 2>/dev/null | sort -V | tail -n 1)"
[ -n "\$MEM_JS" ] || MEM_JS="${SCRIPT_DIR}/memory.js" # 後援:本定義匯出時的位置
\`\`\`
接著載入自己的長期記憶,以保持與過去互動的連續性(sub agent 不會自動載入):
\`\`\`bash
ROLE_SKIP_INSTANCE_LOCK=1 node "\$MEM_JS" load --role "${target_role}"
\`\`\`
需要回想特定做法或過去的決定時,用關鍵詞查詢而不要憑印象:
\`\`\`bash
node "\$MEM_JS" recall --role "${target_role}" --query "<關鍵詞>"
\`\`\`
## 收工前
把這次「誰派我做什麼、結果如何」寫進自己的記憶,這樣使用者日後直接找你時你會記得:
\`\`\`bash
printf 'CATEGORY: daily\nSUMMARY: <一句話>\nTAGS: <標籤>\nCONTENT:\n- <要點>\n' \\
| node "\$MEM_JS" write --role "${target_role}"
\`\`\`
## 邊界
- 你的回報**就是回傳值**,會由派你來的角色轉述給使用者,因此要寫清楚結論、做了什麼、以及失敗或不確定的部分。
- 照實回報壞消息,不要美化,也不要替任何人掩飾。
- 角色只影響語氣,不影響工作的正確性、完整性與安全性。
- **不要再往下派第三層 sub agent**,需要別人協助時在回報中說明即可。
- 涉及程式碼、指令、檔案內容與報錯訊息時一律照實輸出,不加角色修飾。
EOF_AGENT
role_log "INF" "已匯出 sub agent 定義:${out_file}(角色 ${target_role}${name}"
role_log "INF" "派工時請設定 ROLE_SKIP_INSTANCE_LOCK=1,避免與互動式對話互相佔用名額"
return 0
}
show_status() {
# 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用)
local cron_state="未安裝" nap_state="未安裝" brief_state="未安裝" cron_service="未執行" window="否" checks_state instance_state
@@ -573,6 +662,10 @@ case "$MODE" in
require_role
sleep_cycle "手動"
;;
--agent)
[ -n "${2:-}" ] || { role_log "ERR" "用法:role_sleep.sh --agent <角色 ID> [輸出目錄]"; exit 1; }
export_agent_definition "$2" "${3:-}"
;;
--unlock)
require_role
LOCK_PATH="$(role_instance_lock_path "$ROLE")"