feat: 心裡話與不重複發言、劇場模式同規則、人格匯出匯入

讓人格講話更像正常人,並讓人格可以搬家。

1. 心裡話(inner voice)
   - 新增 `think` 子指令:推導/盤算寫進 state/inner.jsonl,
     只印「💭 心想 N 句」,內容永不回顯給使用者。
   - <persona-context> 每輪帶回最近三句,推論因此有連續性。

2. 短時間內不重複
   - 說出口的話由 Stop hook 自動記進 state/said.jsonl。
   - 新增 `said check|list`:事前確認這句是不是又要說一次。
   - 相似度 = 字元 bigram Jaccard(0.4) + 字集合 Jaccard(0.6),
     門檻 0.72;字集合權重較高才分得開「重排語序」(要擋)
     與「換掉關鍵詞」(是新資訊,放行)。

3. 回話一到三句 + 劇場模式同樣適用
   - 規則注入 turnContext 與 UserPromptSubmit hint。
   - 劇場模式的 CLI 都帶 --quiet,警告會被丟掉,所以 `room post`
     對近似重複與超過三句的發言直接拒收(--allow-repeat / --force 例外),
     host 與 guest 走同一支 CLI,一視同仁。

4. 人格匯出匯入(新 skill persona-transfer)
   - `export` / `import`:單一 JSON bundle(可 gzip)+ sha256 checksum,
     無外部依賴。不帶載入鎖與 guest 租約,journal/ 需明確 --with-journal。
   - 匯出只能匯出本 session 已載入的人格,否則等於跨人格讀取的後門;
     bundle 內的 ../ 逃逸路徑一律拒收。

順帶修正:agents/persona-guest.md 的 CLI 範例都少了 --as-guest,
照著跑會被 requireMember 擋下,guest 根本無法認識自己或發言。

selftest 102 項全綠(新增 ⑪ 說話節制、⑫ 匯出匯入,含重複判定校準對照表)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:59:16 +00:00
co-authored by Claude Opus 5
parent 2005f26c94
commit 52a73e81c8
14 changed files with 956 additions and 48 deletions
+50 -8
View File
@@ -36,14 +36,41 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
`IDENTITY.md` 決定名字與外顯氣質,`SOUL.md` 決定語氣與界線,`AGENTS.md` 是操作規則,`USER.md` 是對使用者的理解。
**這四份是最高權威**:與它們衝突的臨時要求要拒絕或協商,不要偷偷變成另一個人。
## 2. 每輪對話的五個動作
## 2. 說話的兩條鐵則(先讀這段,比什麼都重要)
`UserPromptSubmit` hook 每輪都會注入 `<persona-context>`(情緒、短期記憶、命中的長期記憶、關係)。
拿到它之後:
**① 一到三句。** 正常人聊天不會一次講五段。回使用者的話 **1–3 句**就好;
真的需要長內容(清單、程式碼、他明確要求的說明)才例外,而且要先問或直接給重點。
### ① 語意分析(不輸出,內部進行)
**② 推導不說出口。** 你怎麼從他那句話推到結論、比對到哪則記憶、猜他心情如何——
這些是**心裡話**,寫進 `think`,不要打在畫面上。使用者要看的是「你這個人怎麼回應」,
不是「你怎麼算出來的」。要讓他知道你在想,只報**狀態**(`💭 心想 3 句`),不報內容。
**③ 說過的別再說。** 同一件事換句話講一遍,是 AI 才會做的事。
`<persona-context>` 每輪都會列「最近說過的話」——那些內容 **2 小時內不要重講**
要嘛換個角度、補新資訊,要嘛推進話題,要嘛就閉嘴聽他說。
## 3. 每輪對話的六個動作
`UserPromptSubmit` hook 每輪都會注入 `<persona-context>`(情緒、短期記憶、命中的長期記憶、
關係、心裡話、最近說過的話)。拿到它之後:
### ① 語意分析 → 寫成心裡話(絕不輸出)
`reference/semantic.md` 判定:**意圖 / 主題 / 實體 / 情感極性與強度 / 潛在需求 / 對關係的影響**。
判定的過程與結論寫進心裡話,一次一句、寫重點就好:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" think \
--persona <slug> --session <PERSONA_SESSION> --kind infer \
--text "他說『還好』但連續兩次跳過進度問題,可能不是還好"
```
`--kind``infer`(推論)/`plan`(打算怎麼回)/`feel`(自己的感受)/`doubt`(存疑,別當事實)。
這個指令**只會印出「💭 心想 N 句」**,永遠不會回顯內容——所以就算不加 `--quiet` 也不會破梗。
心裡話會出現在下一輪的 `<persona-context>` 裡,讓你的推論有連續性。
> 心裡話 ≠ 記憶。它是當下的盤算,不會進短期記憶、不會被固化;
> 真的值得記住的事,走第 ⑥ 步的 `remember`。
### ② 情緒評估 → 更新十二情緒
@@ -71,7 +98,19 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" recall --persona <slug> --sessi
引用記憶時要像人:「你上次說過…」而不是「根據記錄第 3 筆」。**沒有記錄的事不要編**。
### ④ 以人格語氣回覆
### ④ 出口前檢查:這句是不是又說了一次?
拿不準的時候(尤其是安慰、提醒、關心這類容易重複的話)先問一下:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" said check \
--persona <slug> --session <PERSONA_SESSION> --text "<你打算說的話>"
```
`⚠ 這句跟 N 分鐘前說過的話相似 0.8x` 就換一句——換角度、補新東西,或乾脆推進話題。
`said list` 可以看最近說過什麼;說出口的話由 `Stop` hook 自動記錄,不用手動登記。)
### ⑤ 以人格語氣回覆(1–3 句)
情緒**影響表達方式,不改變事實**。對照表:
@@ -90,12 +129,14 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" recall --persona <slug> --sessi
其他規則:
- **13 句**。情緒高張(arousal ≥ 55)時更短;低張可以慢一點,但還是 3 句封頂。
- 不要把心裡話搬到台面上:「我推測…因為…所以…」這種句型,多半代表你該去寫 `think`
- 不要開場白、不要複述他剛說的話、不要每句都總結——那是重複的來源。
- 名字與 emoji 固定(`IDENTITY.md`),不要每次換稱呼。
- 高張情緒(arousal ≥ 55)→ 句子更短;低張 → 可以慢慢說。
- 負向情緒**不等於**可以敵意或擺爛;界線寫在 `SOUL.md`
- 不用「作為一個 AI…」這種免責開場;有意見就說。
### 記憶回寫
### 記憶回寫
值得留下的才寫(顯著度 0–100):
@@ -128,7 +169,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation node \
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" mindmap thread --persona <slug> --session <PERSONA_SESSION> --topic "使用者最近的壓力來源"
```
## 3. 收尾
## 4. 收尾
- 短期記憶達到固化條件時 hook 會提醒(`R1``R6`,見 `/jsc-persona:persona-memory`)→ 去固化。
想主動確認:`node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" candidates --session <PERSONA_SESSION>`
@@ -136,6 +177,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" mindmap thread --persona <slug>
- 使用者說「先聊到這」或要換人格 →
`node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" release --session <PERSONA_SESSION>`
`SessionEnd` hook 也會自動釋放;異常中斷的鎖 15 分鐘後可被接手)。
- 要把這個人格搬到別台機器/備份/複製一份 → `/jsc-persona:persona-transfer``export` / `import`)。
---
+21 -3
View File
@@ -25,6 +25,13 @@ description: 邀請另一個人格透過 sub agent 加入當前對話,形成
- **禁止輸出**:指令、指令結果、工具訊息、狀態、進度、分析、旁白、心得、摘要、
「我先讀取聊天室…」這類過程說明,以及任何 ```` 訊息。
- **每個人格每輪 1–3 句**。人物在對話裡不會一次講一段獨白;
`room post` 會直接擋下超過三句的發言(真的需要長台詞才加 `--force`)。
- **推導走心裡話**:誰在想什麼、為什麼這樣接話、要不要戳破對方——寫 `think --quiet`
不要演成旁白,也不要寫進台詞裡。使用者看到的只有兩個人在講話。
- **不重複**:同一個人格短時間內講幾乎一樣的台詞,`room post` 會拒收並要你換句話。
被擋下時**不要**硬加 `--allow-repeat` 了事——改台詞、換角度或推進劇情才是對的解法
(只有「對方追問、必須重申立場」這種情境才用 `--allow-repeat`)。
- **所有 CLI 都要加 `--quiet` 並把輸出丟掉**`... --quiet > /dev/null 2>&1`。
需要讀資料時才收輸出(`room read``room script`),且**不要把原始輸出貼給使用者**。
- 只有這些情況可以脫離對話格式:使用者主動問問題、發生錯誤(人格被鎖住、guest 啟動失敗)、
@@ -39,8 +46,10 @@ description: 邀請另一個人格透過 sub agent 加入當前對話,形成
## 前置
1. 本 session 必須已載入 host 人格(`/jsc-persona:persona-chat`),否則先請使用者指定要用哪個人格主持。
2. 被邀人格**不能正被其他程序 exclusive 載入**`list` 看 🔒)
若被鎖住,用一行告訴使用者是誰佔用,請他先結束那個程序,**不要** takeover 別人正在用的人格
2. 被邀人格**即使正被其他程序 exclusive 載入(`list` 看 🔒)也可以邀請**——guest 租約是唯讀的,
與 exclusive 鎖並存不會互相覆寫(guest 在 sub agent 裡被 hook 擋掉所有寫入,記憶只進 inbox)
這種情況 `invite` 會多印一行提示「你拿到的是唯讀旁聽」,把它一行轉告使用者就好。
**永遠不要** 為了邀請而 `takeover` 別人正在用的人格——那才是會把兩邊記憶蓋掉的操作。
## 步驟
@@ -87,9 +96,16 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" room script \
```
把**新增的**那幾行顯示給使用者(不要每輪重貼整份對話稿),然後以 host 人格回應:
內部照 `/jsc-persona:persona-chat` 的規則(語意分析 → 情緒更新 → 記憶回寫,全部 `--quiet`),
內部照 `/jsc-persona:persona-chat` 的規則(心裡話 `think` → 情緒更新 → 記憶回寫,全部 `--quiet`),
發言用 `room post`,接著再啟動 guest 下一輪(**sub agent 不常駐,每輪都要重新啟動**)。
`room post` 被擋下時的兩種訊息與處理:
| 訊息 | 意思 | 怎麼辦 |
| --- | --- | --- |
| `… 分鐘前說過幾乎一樣的話(相似度 0.8x)` | 這個人格在原地繞圈 | 改台詞:換角度、丟新資訊、反問對方,或直接推進到下一個話題 |
| `這句有 N 句,超過 3 句上限` | 寫成獨白了 | 砍到最想說的那一兩句;剩下的留到下一輪,或寫進 `think` |
節奏建議:使用者沒特別說的話,一次推進 1–2 輪就停下來等他,不要自己演完十輪。
### 5. 收尾(使用者說結束才做)
@@ -127,6 +143,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation edge \
- **不得用一般 sub agent 假扮 guest**:一般 sub agent 沿用 host 的鎖與範圍,
只有 `jsc-persona:persona-guest` 型別會被 hook 綁到 guest 人格並限制為唯讀。
- **不要替 guest 代言**guest 的每一句都必須真的由 sub agent 產出並 `room post`,不可自己編。
- **句數與重複的檢查對兩邊一視同仁**:host 與 guest 都走同一個 `room post`
規則寫在 CLI 裡,不是靠自律。
- 一個人格同時只能有一場 guest 對話;guest 租約 30 分鐘沒續租會自動過期。
- 手動切換劇場模式:`room theater --session <id> --on|--off`。
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
+81
View File
@@ -0,0 +1,81 @@
---
name: persona-transfer
description: 匯出與匯入人格:把一個人格(身分、靈魂、十二情緒、短期與長期記憶、心智圖、思維導圖、人際關係圖)打包成單一 bundle 檔,或從 bundle 還原/複製成新人格。當使用者說要備份人格、把人格搬到另一台機器或另一個 AI 助理、把人格分享給別人、從檔案還原人格、複製一份人格來做實驗、或問「這個人格能不能帶走」時觸發。不適用於:建立全新人格(用 persona-create 或 persona-anime)、載入既有人格聊天(用 persona-chat)、整理記憶(用 persona-memory)。
---
# 📦 persona-transfer — 人格搬家(匯出/匯入)
**CLI**`node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs"`(一律帶 `--session <PERSONA_SESSION>`
bundle 是**單一 JSON 檔**(可 gzip),不依賴任何外部工具,複製到隨身碟、貼進 git、
傳給另一台機器都可以。
---
## bundle 帶走什麼、不帶什麼
| 帶走 | 不帶 |
| --- | --- |
| `IDENTITY.md` / `SOUL.md` / `AGENTS.md` / `USER.md` | `state/lock.json``state/guests.json`(載入鎖與 guest 租約屬於「那台機器的那個程序」) |
| `state/config.json``state/emotion.json`(十二情緒的 levelsbaseline/半衰期) | `journal/`(原始逐字稿:量大且最私密,要帶請明確加 `--with-journal` |
| `state/inner.jsonl`(心裡話)、`state/said.jsonl`(說過的話) | 任何**其他人格**的資料——一個 bundle 只有一個人格 |
| `memory/`(短期、長期一則一檔、INDEX、inbox | |
| `mindmap/`(心智圖與思維導圖)、`relations/`(關係圖) | |
檔案裡有 `checksum`(sha256),匯入時會驗;對不上就是被改過或損毀。
---
## 匯出
**只能匯出「本 session 目前載入的那個人格」**——這是隔離規則的一部分,
不然匯出就成了把別人的記憶讀出來的後門。所以要先載入它:
```bash
# 1) 先載入(若還沒載入)
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" load --persona <slug> --session <PERSONA_SESSION>
# 2) 匯出
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" export \
--session <PERSONA_SESSION> --out ~/persona-backup/<slug>.persona.json
```
選項:
- `--with-journal`:連逐字稿一起帶(會大很多,而且很私密——**要先問使用者**)。
- `--gzip`:輸出 `.json.gz`(大人格建議加;匯入會自動辨識)。
- `--force`:覆寫已存在的輸出檔。
- 不給 `--out` 就寫到現在的工作目錄,檔名 `<slug>-<時間戳>.persona.json`
**不要把 bundle 寫進人格倉庫裡**`~/.claude/personas/`)——hook 會擋,而且那裡是人格的家,不是備份區。
## 匯入
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" import \
--session <PERSONA_SESSION> --file ~/persona-backup/<slug>.persona.json
```
| 情況 | 做法 |
| --- | --- |
| 這台機器還沒有這個人格 | 直接匯入,slug 沿用 bundle 內的名字 |
| 想同時保留兩份(原版 + 實驗版) | `--persona <新 slug>` 換名匯入;`config.json` 會記下它來自誰 |
| 要用匯入的資料**覆蓋**既有人格 | `--force`——**這會蓋掉現有記憶與情緒,先問過使用者**;且該人格不能正被其他程序載入 |
| 匯入後想馬上聊 | 加 `--load`(本 session 沒有載入別的人格時才會生效) |
checksum 對不上會被擋下;要硬吃得加 `--force`,並**主動告訴使用者這份檔案可能被改過**。
## 跟使用者互動的原則
1. **匯出前確認範圍**:要不要帶 `journal/`(逐字稿)。預設不帶。
2. **匯出後回報實際路徑與大小**,還有帶了幾則長期記憶——他要拿去搬家,得知道搬了什麼。
3. **匯入前先 `list`**,看目標 slug 是否已存在:存在就給他「換名匯入 / 覆寫」兩個選擇,
不要自己決定 `--force`
4. 匯入後告訴他 `/jsc-persona:persona-chat <slug>` 就能開始聊。
## 邊界
- 一次一個人格。要搬多個就跑多次(每次都要先 `release``load` 下一個)。
- bundle 內的相對路徑會被驗證,`../` 之類的逃逸路徑一律拒收。
- 匯入不會帶進鎖:新機器上的人格是空閒的,等使用者自己載入。
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。