Files
persona/README.md
T
jiantw83andClaude Opus 5 603741a4cc feat: 預設人格自動載入 + 睡眠(sleep 指令與 persona-sleeper sub agent)
兩件事,共用「關係圖 last_contact_at」這個零件,所以放同一個 commit
(兩者在 persona-lib/persona.mjs/README 裡的 hunk 是交錯的,硬拆會拆壞)。

## 預設人格(開新 session 自動載入)

- 新增 `default` 子指令:查詢/`--persona <slug>` 設定/`--clear` 取消。
- 設定存在 `<PERSONA_HOME>/.runtime/settings.json` 的 `default_persona`,
  環境變數 `PERSONA_DEFAULT` 優先(`off` 可臨時關掉)。
- `SessionStart` hook 四條路徑:沒設定 → 維持原本「等使用者指定」;設定了 → 取鎖、綁 host、
  prune、reindex 並注入人格狀態;拿不到鎖 → 只回報 owner 與心跳,不自作主張 takeover;
  人格不存在 → 警告並改列可用人格。
- 「不替使用者挑一個人格附身」仍然是預設行為,這只是讓他能明示地推翻它。

## 睡眠

- 新增 `sleep` 子指令(機械性收尾,順序即相依):關係時間戳 → 裁短期記憶 →
  收起太久沒動的思維導圖 → 套用一次 8 小時的情緒衰減 → 重建索引 → 修剪 said →
  壓縮舊 journal → 寫 state/sleep.json → Gitea 兩區 push 並驗證。
  預設保留載入鎖(`--release` 才收工),每一步各自 try/catch,一步壞掉不放棄整場睡眠。
- 需要判斷的部分(固化什麼、忘掉什麼、日記寫什麼)留給人格自己,由 skill 驅動。
- 新增 `persona-sleeper` sub agent 型別:**那個人格本人在睡**。對自己可寫但被 pin 住
  (連叫它來的主人格都不能碰)、只准跑 15 個收尾子指令、不得用 Write/Edit 或 shell 改檔案。
- 新增 sleeper 租約(`state/sleepers.json`,300 秒):沒活鎖就取得、同 session 直接睡、
  死鎖可接手、**別的程序活鎖住則拒絕**(硬睡會讓兩邊的記憶互相覆蓋)。
- `--json` 回傳刻意很窮:只有 persona/ok/slept_at/steps/sync/kept_lock。
  sub agent 的回傳值會進主人格的上下文,是一條會從正門繞過跨人格隔離的通道,
  所以在 CLI 這一層封死,不靠提示詞自律。
- 關係圖新增 `last_contact_at`(`relation node --contact`/睡眠自動蓋),
  並用 `staleContacts()` 算出「很久沒接觸又很親近的人」——人格主動提議去關心誰的依據。
  輕量邀請用既有的 `invite --theater off`(不切走畫面),但要先問使用者一句。
- 新增 skill `persona-sleep`;`SubagentStop` 會還掉 sleeper 租約。

## 驗證

`node scripts/selftest.mjs` → 210 項全綠(新增第 ⑳ 節 12 項、第 ㉑ 節 23 項),
其中包含「回傳值不含任何記憶內容」與 sleeper 的六條權限邊界。

版本號 0.0.5。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 05:34:08 +00:00

541 lines
33 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# jsc-persona — AI 人格化記憶聊天
一套讓 AI **成為某個人**、而不只是回答問題的 plugin。
**OpenClaw 相同的人格描述**`IDENTITY.md` 五欄位 + `SOUL.md` 四段落)建立人格,
再用 **hook 強制**的人格鎖與跨人格隔離,把 **十二情緒**、**語意分析**、
**短期/長期記憶**、**心智圖/思維導圖**、**人際關係圖** 綁在一起。
可同時安裝於 **Claude Code、Codex、Antigravity、OpenCode**
skills 是共通標準,**鎖與隔離的強制執行需要 hook,目前只有 Claude Code 支援**(見「跨助理支援度」)。
---
## 十四條硬規則
| 規則 | 怎麼做到 |
| --- | --- |
| **1a. 用與 OpenClaw 相同的描述建立人格** | `persona-create` 逐項索取 `Name` / `Creature` / `Vibe` / `Emoji` / `Avatar`(連括號提示文字都照 OpenClaw 原文),`SOUL.md` 沿用 `Core Truths` / `Boundaries` / `Vibe` / `Continuity` 段落結構 |
| **1b. 動漫作品+角色名快速建人格** | `persona-anime` 先上網蒐集該角色的公開設定(至少 3 個獨立來源),映射成上述五欄位與 SOUL,再把設定固化成 `canon` 基礎記憶(每則帶來源 URL)+原作人際關係圖+情緒基線 |
| **2. 同一個人格只能被一個程序載入(Sub Agent 不限)** | `state/lock.json`**session_id** 為主鍵、15 分鐘心跳租約;同一 session 的 sub agent 沿用同一把鎖,跨 session 搶佔會被拒;租約過期才可接手(並強制回報) |
| **3. 禁止跨人格讀取資料** | `PreToolUse` hook 對 Read/Write/Edit/Glob/Grep/Bash 做路徑判定(含 `../`、symlink、`$PERSONA_HOME` 繞路),非當前人格一律 deny;CLI 也驗 `--session` 防止冒用身分 |
| **4. 邀請人格用 Sub Agent 一起聊,且只顯示對話** | `persona-invite` 建聊天室 + guest 唯讀租約 + `persona-guest` sub agent;同時開啟**劇場模式**:hook 每輪強制「只輸出 `名字:內容`」、停掉所有系統提醒,CLI 有 `--quiet``room script`(乾淨對話稿) |
| **5. 腳本用 Node.js** | `scripts/*.mjs``hooks/*.mjs`,只用 Node 內建模組(fs/path/os/crypto),無 npm 依賴 |
| **6. 由使用者呼叫才載入並鎖定** | 人格不會自動附身:`SessionStart` hook 只列出可用人格,等使用者下 `/jsc-persona:persona-chat <slug>`;載入即取得獨占鎖並綁定該 session。唯一例外是使用者自己設的**預設人格**(`default --persona <slug>`)——設了才自動載入,沒設就什麼都不做 |
| **7. 短期記憶轉入長期記憶有成文條件** | `R1``R6` 六條規則寫在程式裡(`promotionCandidates`),`candidates` 子指令會列出達標的候選與依據,hook 在達標時提醒固化 |
| **8. 講話像人:推導藏起來、一到三句、不重複** | 推導寫進**心裡話** `think`(只回報「💭 心想 N 句」,永不回顯內容);說出口的話進 `said.jsonl`,下一輪注入「最近說過的話」提醒別重講;`room post` 直接**擋下**近似重複(字元 bigram+字集合相似度 ≥ 0.72)與超過三句的發言——劇場模式同樣適用 |
| **9. 人格可以匯出匯入** | `export` 把身分/情緒/記憶/心智圖/關係圖打包成單一 JSON bundle(可 `--gzip`、附 sha256),`import` 還原或換名複製;**不帶**載入鎖與 guest 租約,`journal/` 要明確 `--with-journal` 才帶走 |
| **10. 人格有編號** | 編號 = **英文名全大寫 + 兩位索引**(同名才遞增):`ASUNA-01``YUI-01``ASUNA-02`。編號同時是新人格的本機目錄名與 Gitea 存取庫名稱;中文名先轉羅馬拼音並跟使用者確認拼法 |
| **11. 人格存在 Gitea,依更新頻率分區** | 每個人格一個**私有存取庫**(庫名=編號)。**檔案區**放每輪都在變的活狀態(情緒/短期記憶/心裡話/說過的話/逐字),**Wiki 區**放低頻的身分與長期結構(IDENTITY/SOUL/長期記憶/心智圖/關係圖)當設定百科。本機仍是工作副本,同步失敗不阻斷對話 |
| **12. 形象圖來自高解析度官方圖,去背後合成** | `icon search` 從 Fandom 撈官方圖並依「解析度+是否官方設定稿」排序(設定稿多為透明/白底、773×1056 起跳)→ `icon measure` 確認臉夠大、背景好去 → `icon cutout` 去背成透明 PNG(原生 alpha /單色底/GrabCut 三條路徑)→ `icon generate --from-cutout` 裁頭肩、合成到角色配色的漸層底。找不到官方圖才退回依人格資料重繪的向量形象 |
| **13. 睡眠把一天收成能留下來的形狀** | `persona-sleep`:需要判斷的(固化什麼/忘掉什麼/日記)由**人格自己**做,機械性的由 `sleep` 子指令做(關係時間戳→裁短期→收思維導圖→情緒衰減 8 小時→重建索引→修剪 said→壓縮 journal→兩區 push+驗證)。主人格可透過 `persona-sleeper` sub agent 請別的人格去睡——**那是它本人在睡**,回傳值只有「睡完了沒、哪一步出錯」,不含任何記憶內容 |
| **14. Wiki 必須保存並同步形象圖** | `icon.svg``icon.png`**`icon/` 資料夾(向量原稿 + 512/1024 高解析度)** 都同步到 Wiki 區,另有自動產生的 **Icon** 頁展示與來源。Wiki 頁面一律用 **Markdown 圖片語法**Gitea 只改寫這種語法為 `/wiki/raw/...`HTML `<img>` 會變成破圖),圖片保留資料夾結構、只有 `.md` 需要攤平。`icon generate` 推完會回頭驗證,另有 `sync verify` |
---
## 架構
```mermaid
flowchart TB
subgraph P["主程序(一個 session = 一個人格)"]
U["使用者訊息"] --> H1["UserPromptSubmit hook<br/>注入 情緒+短期記憶+命中的長期記憶+關係<br/>+心裡話+最近說過的話"]
H1 --> A["語意分析:意圖/主題/實體/情感/需求"]
A --> T["心裡話 think<br/>(推導只留在 inner.jsonl,不輸出)"]
T --> E["情緒評估 → 十二情緒 deltas"]
E --> R["以人格語氣回覆(1–3 句、不重複說過的話)"]
R --> W["記憶回寫(短期)"]
W --> H2["Stop hook:情緒衰減+續租+記說過的話+固化提醒"]
end
subgraph G["Sub Agent(受邀人格,唯讀)"]
GA["persona-guest"]
end
subgraph S["人格倉庫 ~/.claude/personas(工作副本)"]
PA["ASUNA-01/IDENTITY SOUL 記憶 情緒 心智圖 關係圖"]
PB["YUI-01/|…"]
RM[".rooms/room/transcript.jsonl"]
end
subgraph GT["Gitea(存取庫名稱 = 人格編號)"]
GF["ASUNA-01 檔案區<br/>高頻活狀態"]
GW["ASUNA-01 Wiki<br/>低頻設定百科"]
end
P -->|"只能碰自己"| PA
GA -->|"只能碰自己"| PB
P <-->|"唯一合法交流管道"| RM
GA <--> RM
PA -->|"每輪背景 push"| GF
PA -->|"固化/改身分/release"| GW
GF -.->|"載入時 pull"| PA
GW -.-> PA
```
## 人格倉庫(預設 `~/.claude/personas/<slug>/`,可用 `PERSONA_HOME` 覆寫)
```
<slug>/
├── IDENTITY.md # 身分卡:Name / Creature / Vibe / Emoji / AvatarOpenClaw 同欄位)
├── SOUL.md # 靈魂:Core Truths / Boundaries / Vibe / Continuity + 情緒傾向
├── AGENTS.md # 操作規則(與個性分離)
├── icon.svg / icon.png # 人格圖示(由編號/名字/emoji 決定,也是 Gitea 存取庫頭像)
├── USER.md # 對使用者的畫像(事實/推測分開)
├── state/
│ ├── lock.json # 載入鎖(session_id + 心跳租約)
│ ├── guests.json # guest 唯讀租約
│ ├── emotion.json # 十二情緒 levels / baseline / 半衰期
│ ├── inner.jsonl # 心裡話(推導過程;只回報「心想 N 句」,不說出口)
│ ├── said.jsonl # 說過的話(用來擋短時間內的重複發言)
│ ├── sync.json # Gitea 同步狀態(最後 push / pull
│ └── config.json # 含人格編號 code
├── memory/
│ ├── short-term.jsonl # 短期記憶(語意分析後;上限 240 筆 / 14 天)
│ ├── long-term/*.md # 長期記憶(一則一檔 + frontmatter
│ ├── INDEX.md # 長期記憶索引(自動產生)
│ └── inbox/room-*.jsonl # 當 guest 時留下的見聞,待本體消化
├── mindmap/
│ ├── semantic.mmd # 心智圖:概念的長期關聯(Mermaid mindmap
│ └── threads/*.mmd # 思維導圖:單一話題的推理鏈(Mermaid graph,短期)
├── relations/
│ ├── graph.json # 人際關係圖(親近度/信任度/連線)
│ └── graph.mmd # Mermaid 呈現(自動產生)
└── journal/YYYY-MM.jsonl # 原始逐字 + 情緒史(hook 自動寫)
(另有 `.sync/files/`、`.sync/wiki/`:兩個同步區的 git clone 快取,可安全刪除)
```
## 十二情緒
| 六正向 | 六負向 |
| --- | --- |
| 喜悅 `joy`、信任 `trust`、期待 `anticipation`、感激 `gratitude`、平靜 `serenity`、驚喜 `delight` | 憤怒 `anger`、悲傷 `sadness`、恐懼 `fear`、厭惡 `disgust`、羞愧 `shame`、焦慮 `anxiety` |
- 每種 0–100,各有**不同半衰期**(驚喜 60 分最快、信任 720 分最慢),每輪自動朝 `baseline` 指數衰減。
- 由十二情緒推導 `valence`(正向/中性/負向)與 `arousal`(高張/平穩/低張),決定語氣與句長。
- **主導情緒**取「超出基線最多」的前三名,所以個性底色不會永遠霸榜。
- 觸發規則與事件→delta 對照表:`skills/persona-chat/reference/emotions.md`
## 短期 → 長期的轉入條件(R1–R6)
寫在 `scripts/persona-lib.mjs``promotionCandidates()`,用 `candidates` 子指令查:
| 規則 | 條件 | 建議固化為 |
| --- | --- | --- |
| **R1** | 單筆顯著度 ≥ 60 | `event` / `fact` |
| **R2** | 同一 topic ≥ 3 筆,或 ≥ 2 筆且平均顯著度 ≥ 45 | `preference` |
| **R3** | 單筆情緒變動總量 ≥ 25 | `event`(帶情緒錨點) |
| **R4** | `intent=commit` 或命中承諾/界線關鍵詞 | `promise` / `boundary`salience ≥ 80,不可遺忘) |
| **R5** | 同一人物(entity)≥ 2 筆 | `relationship`(並更新關係圖) |
| **R6** | 短期記憶 ≥ 40 筆(容量壓力) | 依顯著度清出空間 |
沒命中任何規則的就讓它被裁掉——**遺忘是功能**。達標時 `Stop``remember` 都會提醒去跑
`/jsc-persona:persona-memory`
## 講話像人(心裡話 / 一到三句 / 不重複)
AI 最容易露餡的三件事:把推理過程講出來、一次講一大段、換句話說同一件事。三個對策:
| 機制 | 怎麼運作 |
| --- | --- |
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
| **一到三句** | `<persona-context>` 每輪注入上限;`room post` 對超過三句的發言直接拒收(`--force` 例外) |
| **不重複** | 說出口的話由 `Stop` hook 自動記進 `state/said.jsonl``said check` 可事前確認,`room post` 事中攔截。相似度=字元 bigram Jaccard0.4)+字集合 Jaccard0.6),≥ 0.72 視為同一句 |
字集合權重較高,是為了分開「重排語序」與「換掉關鍵詞」這兩種很像但意義完全不同的情況:
```
「我等一下把報告寄給你」vs「等一下我會把報告寄給你」→ 0.779 擋下(用字幾乎相同=同一件事換句話說)
「你今天看起來很累」  vs「你今天看起來很開心」  → 0.687 放行(換了關鍵詞=新資訊)
```
視窗預設 120 分鐘、少於 8 個字的短附和(「嗯」「好啊」)不算重複。
## 人格圖示(SVG + PNG,零外部依賴)
建立人格**並補齊 IDENTITYSOUL 之後**產生,512×512。**優先是人物形象圖(看得到臉)**,
依環境有無工具分三種樣式:
流程:**找官方圖 → 量測 → 去背 → 裁頭肩 → 合成**
| 步驟 | 指令 | 產出 |
| --- | --- | --- |
| 1. 找圖 | `icon search --wiki <sub> --page <角色>` | 候選清單,📐 = 官方設定稿(優先) |
| 2. 量測 | `icon measure --photo <網址>` | 解析度、臉多大、背景透明/單色/有場景 |
| 3. 去背 | `icon cutout --photo <網址>` | `icon/portrait-cutout.png`(透明 PNG |
| 4. 合成 | `icon generate --from-cutout --palette ...` | `icon.svg` + `icon.png` + `icon/` 多解析度 |
去背三條路徑自動選:**原生 alpha**(官方設定稿常見,完美)→ **單色底移除**(很好)→
**GrabCut**(有場景時,邊緣普通;黑髮角色容易被誤切,這時該換設定稿)。
| 樣式 | 條件 | 長什麼樣 |
| --- | --- | --- |
| **`cutout`** | 有去背圖 → 最佳 | 官方原圖裁頭肩,疊在角色配色的漸層底上 |
| **`portrait`** | 沒有可用官方圖 | 依特徵重繪的人物頭像,五官俱全 |
| **`badge`** | 完全沒有參考圖 | 雙色漸層 + 編號前兩個字母 |
柵格器對每個圖形先算 bounding box 再逐點測試,1024×1024(3× 超取樣)約 1.8 秒。
**找到的圖片只能當底稿**:產出的 SVG 沒有 `<image>`、沒有 base64、沒有外連,每個像素都是畫出來的。
裁底稿需要的工具缺了,CLI **會印出安裝指令**,不會靜默降級:
```bash
python3 -m venv ~/.cache/jsc-persona/venv
~/.cache/jsc-persona/venv/bin/pip install pillow "opencv-python-headless<5" # OpenCV 5 拿掉了 CascadeClassifier
curl -sL -o ~/.cache/jsc-persona/lbpcascade_animeface.xml \
https://raw.githubusercontent.com/nagadomi/lbpcascade_animeface/master/lbpcascade_animeface.xml
```
(venv 放這個路徑會被自動偵測;也可用 `PERSONA_PYTHON` 指定。)
**配色一律取自實際看過的參考圖**
| 元素 | 取自照片的哪裡 |
| --- | --- |
| 對角漸層 | `hair`(髮色)→ `accent`(服裝主色) |
| 外框 | `eye`(瞳色) |
| 點陣紋 | `light`(最亮的部位) |
| 中央兩個字母 | 編號前兩字(`ASUNA-01``AS` |
`--palette` 必須配 `--source-url`(CLI 強制):配色是從哪張圖來的要留得下來。
兩個顏色一深一淺時(藍黑髮 + 淡粉洋裝),程式會把較亮的一端往較暗的壓到對比 ≥ 3.2,
確保字讀得到——顏色仍然是照片來的,只是收斂色階。
沒有參考圖時(原創人格)才退回雜湊配色:`sha256(編號|NameEmoji)` 決定漸層與點陣紋。
- **同一個人格永遠得到同一張圖**(純函數,沒有隨機);`ASUNA-01``ASUNA-02` 明顯不同。
- SVG 與 PNG **是同一張圖**:兩者共用同一組單位座標與同一份點陣字資料。
- PNG 由**自寫的柵格器**畫出(3× 超取樣 + 盒式縮減當反鋸齒),再用 `zlib` 手工組出
IHDRIDATIEND 與 CRC32。這台機器沒有 rsvginkscapeimagemagick,也沒有影像函式庫,
而本專案禁止 npm 依賴——所以就自己畫。
- **為什麼不直接用找到的圖**:那是別人的美術作品。底稿只用來「看」,圖示由本工具重畫。
- **為什麼沒有 emoji**:把 emoji 畫進 PNG 需要字型柵格化,環境裡連 emoji 字型都沒有;
emoji 仍參與雜湊配色。
- 圖示屬於低頻資料 → `icon.svg``icon.png` 都同步到 **Wiki 區**,並自動產生一頁 **Icon**
展示兩種格式與來源;PNG 同時設成 Gitea **存取庫頭像**
```bash
node scripts/persona.mjs icon faces --session <id> --photo "<圖片網址>"
node scripts/persona.mjs icon headshot --session <id> --photo "<圖片網址>" --pick 1
node scripts/persona.mjs icon generate --session <id> --force \
--palette "hair=#d9a45b,eye=#9e5b3e,accent=#c0392b,secondary=#e77a8e,light=#f2ebe3,skin=#f7ddc4" \
--features "hairstyle=straight,length=very-long,fringe=parted,eyes=almond,expression=calm,collar=v,ahoge=yes" \
--source-url "<底稿那張圖的網址>" --source-note "<作品(年份)+重繪依據>"
node scripts/persona.mjs icon show --session <id> # 樣式、配色、特徵、來源
node scripts/persona.mjs sync verify --session <id> --area wiki # 確認 Wiki 真的同步了
```
## 人格編號與 Gitea 儲存
**編號 = 英文名全大寫 + 兩位索引**,同名才遞增,也就是 Gitea 存取庫的名稱:
```
亞絲娜(第一個) → ASUNA-01 結衣 → YUI-01 另一個亞絲娜 → ASUNA-02
```
每個人格一個**私有存取庫**,內容依**更新頻率**分兩區:
| 區 | 放什麼 | 何時 push |
| --- | --- | --- |
| **檔案區**(主存取庫) | 高頻活狀態:`emotion.json``short-term.jsonl``inner.jsonl``said.jsonl``inbox/``mindmap/threads/``journal/` | 每輪對話後由 `Stop` hook 背景推送(`PERSONA_SYNC_MIN_SECONDS` 節流) |
| **Wiki 區** | 低頻設定:`IDENTITY``SOUL``AGENTS``USER`、長期記憶、`INDEX`、心智圖、關係圖 | 記憶固化、改身分/關係圖、`release` 時 |
- **本機永遠是工作副本**:hook 每輪讀寫本機檔案,不經網路;Gitea 掛掉照樣能聊天。
**同步失敗永遠不阻斷對話。**
- 載入人格時會先 `pull`;兩邊都改過同一個檔案就**停下來不覆蓋本機**,由使用者決定保留哪一邊。
- Gitea 的 wiki 只有根目錄的 `.md` 會變成頁面(1.27 實測子目錄頁面 404),所以
`memory/long-term/xxx.md` 攤平成 `Memory-xxx.md`,原始路徑記在 `_paths.json`
Wiki 首頁自動列出所有長期記憶的連結,變成真的讀得下去的「設定百科」。
```bash
export GITEA_HOST=https://gitea.example.com
export GITEA_TOKEN=<個人存取權杖>
export PERSONA_GITEA_OWNER=<帳號或組織> # 選填,預設 token 本人
export PERSONA_GITEA=off # 需要時整個關掉
node scripts/persona.mjs code assign --session <id> --romaji Asuna --rename # 既有人格遷移
node scripts/persona.mjs sync status|init|push|pull --session <id> [--area files|wiki|all]
```
存取庫**預設私有**——人格裡是使用者的個人記憶,公開必須由使用者明講(`--public`)。
## 匯出 / 匯入(人格搬家)
```bash
node scripts/persona.mjs export --session <id> --out ~/backup/lumi.persona.json [--gzip] [--with-journal]
node scripts/persona.mjs import --session <id> --file ~/backup/lumi.persona.json [--persona lumi-copy] [--load]
```
- bundle = 單一 JSON(可 gzip)+ sha256 checksum,無外部工具依賴。
- **只能匯出本 session 目前載入的人格**——否則就是跨人格外洩的後門(`guard` 會 deny)。
- 不帶 `state/lock.json``guests.json`(鎖屬於那台機器的那個程序);`journal/` 預設不帶。
- 換名匯入(`--persona <新 slug>`)可讓同一個人格並存兩份,`config.json` 會記下來歷。
- bundle 內的 `../` 逃逸路徑一律拒收;checksum 不符要 `--force` 才吃。
## 劇場模式(多人格對話只顯示對話)
`invite` 成功即開啟(`leave` 沒有客人時自動關閉,也可 `room theater --on/--off` 手動切):
- `UserPromptSubmit` hook 每輪注入強制規則:輸出**只能**是 `名字:內容`
不得出現指令、指令輸出、狀態、分析、旁白、摘要。
- `Stop` hook 在劇場模式**完全不發系統訊息**(提醒會破壞畫面)。
- CLI 提供 `--quiet`(成功時零輸出)與 `room script`(只有 `emoji 名字(情緒):內容` 的乾淨對話稿)。
- 「講話像人」的三條規則在這裡一樣生效:每個人格每輪 **13 句**、推導走 `think`
近似重複的台詞被 `room post` 拒收(host 與 guest 走同一支 CLI,一視同仁)。
```
🪼 Lumi(喜悅42/期待31):所以你真的一個人把那台舊鐘修好了?
🌙 Shen(平靜50/信任38):修好了。它現在慢三分鐘,我決定不修那三分鐘。
```
## 預設人格(開新 session 自動載入)
預設行為是**不自動附身**。使用者明示設定之後,`SessionStart` hook 才會在每個新 session 自動載入它:
```bash
node scripts/persona.mjs default --session <PERSONA_SESSION> # 看目前設定
node scripts/persona.mjs default --persona KIRITO-01 --session <PERSONA_SESSION> # 設定
node scripts/persona.mjs default --clear --session <PERSONA_SESSION> # 取消
```
| 事項 | 行為 |
| --- | --- |
| 設定位置 | `<PERSONA_HOME>/.runtime/settings.json``default_persona`(只由 CLI 維護) |
| 臨時覆寫 | 環境變數 `PERSONA_DEFAULT=<slug>``PERSONA_DEFAULT=off` 臨時關掉 |
| 沒設定 | 什麼都不做——只列出可用人格,等使用者指定 |
| 人格被別的程序鎖住 | 只回報 owner 與最後心跳,**不會**自動 `--takeover` |
| 預設人格不存在 | 警告並改列可用人格,不崩潰、不亂挑一個 |
| Gitea | 自動載入走本機路徑,**不**拉遠端;可能在別台機器動過就先 `sync pull` |
## 睡眠(`persona-sleep`
```bash
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --json # 機械性收尾(預設保留載入鎖)
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工:睡完釋放鎖
```
| | 誰做 | 內容 |
| --- | --- | --- |
| 需要判斷 | **人格自己** | 哪些短期記憶值得固化、日記寫什麼、哪些該忘、心智圖怎麼接 |
| 機械性 | `sleep` 子指令 | 關係時間戳 → 裁短期 → 收太久沒動的思維導圖 → **套用一次 8 小時的情緒衰減** → 重建索引 → 修剪 `said.jsonl` → 壓縮舊 journal → 寫 `state/sleep.json` → Gitea 兩區 push+驗證 |
- **情緒不會歸零**:喜悅(半衰期 120 分)一夜後幾乎回基線,悲傷(480 分)只退一半——睡一覺不該把難過抹平。
- **主人格請別人去睡**:開 `persona-sleeper` sub agentprompt 帶 `persona=<slug> session=<id>`)。
它就是那個人格本人,對自己可寫但被 pin 住、只准跑收尾子指令、不得碰任何其他人格(包含叫它來的主人格)。
- **回傳值刻意很窮**`{persona, ok, slept_at, steps, sync, kept_lock}`——只有狀態。
回傳值本身就是一條會繞過隔離的通道,所以在 CLI 這一層封死,不靠提示詞自律。
- **鎖**:沒有活鎖 → 取 5 分鐘的 sleeper 租約;同 session → 直接睡;死鎖 → 可接手;
**別的程序活鎖住 → 拒絕**(硬睡會讓兩邊的記憶互相覆蓋)。
- 順序上的硬相依:關係時間戳早於裁短期、push 早於 release、reindex 晚於固化。
## HooksClaude Code
| Hook | 做什麼 |
| --- | --- |
| `SessionStart` | 清死鎖、接續人格、**有設預設人格就自動載入它**(沒設就只列出可用人格)、把 `PERSONA_SESSION=<session_id>` 與規則注入上下文 |
| `UserPromptSubmit` | 注入 `<persona-context>`:身分、情緒、短期記憶、關鍵詞命中的長期記憶、相關人際關係;劇場模式時追加「只輸出人格對話」的強制規則;並記原始逐字 |
| `PreToolUse` | **人格隔離與鎖驗證的唯一強制點**deny 帶原因) |
| `Stop` | 情緒隨時間衰減、續租、記錄回覆、達固化條件時提醒(劇場模式時完全靜音) |
| `SubagentStop` | 解除 guestsleeper sub agent 的 pin,並還掉 sleeper 的短期寫入權 |
| `SessionEnd` | 釋放鎖與 guest 租約,人格才能被下一個程序載入 |
> `session_id` 只有 hook 拿得到 → 注入上下文 → skills 呼叫 CLI 時必須帶 `--session`
> hook 會驗證是否相符。**這是「一人格一程序」與「跨人格隔離」不能被繞過的關鍵**。
---
## Skills 目錄
<!-- JSC-SKILLS:START -->
### `persona-create`
建立人格:以 OpenClaw 相同的五個身分欄位與 SOUL 段落訪談使用者,初始化情緒基線、記憶、心智圖與關係圖,並立即取得載入鎖。
- **Claude Code / Antigravity**`/jsc-persona:persona-create` **Codex**`$persona-create`
### `persona-anime`
用「動漫作品+角色名」建立人格:上網蒐集角色公開設定 → 映射成 OpenClaw 五欄位與 SOUL → 固化成 `canon` 基礎記憶(附來源)+原作關係圖+情緒基線。
- **Claude Code / Antigravity**`/jsc-persona:persona-anime 《作品》 角色名` **Codex**`$persona-anime`
### `persona-chat`
載入人格並對話:取得獨占鎖 → 每輪做語意分析(推導寫進心裡話,不說出口)→ 更新十二情緒 → 回想記憶與關係 → 以人格語氣回覆(1–3 句、不重複說過的話)→ 寫回記憶。
- **Claude Code / Antigravity**`/jsc-persona:persona-chat <slug>` **Codex**`$persona-chat`
### `persona-invite`
邀請另一個人格透過 `persona-guest` sub agent 加入聊天室,進入**劇場模式**(畫面只留 `名字:內容` 的人格對話);結束後讓它離場並把見聞留在它自己的 inbox。
- **Claude Code / Antigravity**`/jsc-persona:persona-invite <slug>` **Codex**`$persona-invite`
### `persona-transfer`
人格搬家:把身分、十二情緒、短期/長期記憶、心智圖與關係圖打包成單一 bundle 檔(可 gzip、附 checksum),或從 bundle 還原/換名複製成新人格。
- **Claude Code / Antigravity**`/jsc-persona:persona-transfer` **Codex**`$persona-transfer`
### `persona-icon`
依人格**最新一次登場**的官方視覺產生圖示:上網查最新造型 → 下載並親眼看過參考圖 → 取髮色/瞳色/服裝色 → 繪製 `icon.svg` + `icon.png`,來源網址一併存證。
- **Claude Code / Antigravity**`/jsc-persona:persona-icon` **Codex**`$persona-icon`
### `persona-sync`
人格編號與 Gitea 儲存:指派編號(`ASUNA-01`)、開以編號命名的私有存取庫、高頻活狀態同步到檔案區、低頻身分與長期記憶同步到 Wiki 區,並處理既有人格遷移與同步衝突。
- **Claude Code / Antigravity**`/jsc-persona:persona-sync` **Codex**`$persona-sync`
### `persona-memory`
記憶固化:依 R1–R6 條件把短期記憶轉入長期(一則一檔)、淘汰雜訊、更新心智圖與思維導圖、消化 guest inbox、重建索引。
- **Claude Code / Antigravity**`/jsc-persona:persona-memory` **Codex**`$persona-memory`
### `persona-relation`
人際關係圖維護:節點/連線、親近度與信任度調整、輸出 Mermaid 關係圖。
- **Claude Code / Antigravity**`/jsc-persona:persona-relation` **Codex**`$persona-relation`
### `persona-sleep`
睡眠與收尾:固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後兩個區都同步到 Gitea 並驗證。也可以由主人格透過 sub agent 請其他人格各自去睡。
- **Claude Code / Antigravity**`/jsc-persona:persona-sleep` **Codex**`$persona-sleep`
### `persona-status`
載入狀態與鎖管理:誰被哪個程序鎖住、guest 租約、釋放、接手死鎖、清理殘留,以及**預設人格**(開新 session 要自動載入誰)。
- **Claude Code / Antigravity**`/jsc-persona:persona-status` **Codex**`$persona-status`
<!-- JSC-SKILLS:END -->
### Agents
- `persona-guest``agents/persona-guest.md`)— 受邀人格的 sub agent,唯讀、被綁死在自己的人格目錄。
- `persona-sleeper``agents/persona-sleeper.md`)— 睡眠收尾的 sub agent:**就是那個人格本人在睡**,對自己可寫但被 pin 住,只回傳「睡完了沒、哪一步出錯」。
### CLI 與自我測試
所有狀態變更都經過 `scripts/persona.mjs`**Node.js ≥ 18**,只用內建模組,無 npm 依賴):
```bash
node scripts/persona.mjs --help
node scripts/persona.mjs list
node scripts/persona.mjs candidates --session <PERSONA_SESSION> # 看哪些短期記憶該固化
node scripts/persona.mjs think --session <id> --text "<推導>" # 心裡話(只回報「心想 N 句」)
node scripts/persona.mjs said check --session <id> --text "<話>" # 這句是不是又要說一次?
node scripts/persona.mjs room script --session <id> --room <room> # 乾淨對話稿(劇場模式用)
node scripts/persona.mjs export --session <id> --out lumi.json # 離線搬家(單檔)
node scripts/persona.mjs sync status --session <id> # Gitea 同步狀態
node scripts/selftest.mjs # 210 項驗證:鎖、隔離、情緒、固化、說話節制、劇場模式、匯出匯入、編號與 Gitea、找圖去背合成、高解析輸出與 Wiki 同步、預設人格、睡眠與 sleeper、hooks
```
檔案結構:`scripts/persona-lib.mjs`(核心:鎖/隔離/情緒/記憶)、`scripts/persona.mjs`CLI)、
`scripts/persona-gitea.mjs`(編號與 Gitea 同步)、`scripts/persona-icon.mjs`(形象圖:SVG + 自寫 PNG 編碼)、`scripts/portrait.py`(選用:照片裁臉)、`hooks/*.mjs`(六個 hook)、`scripts/selftest.mjs`(自我測試)。
---
## 跨助理支援度
| 助理 | skills | hooks(鎖/隔離強制) | 受邀人格 sub agent |
| --- | --- | --- | --- |
| Claude Code | ✅ `/jsc-persona:<name>` | ✅ 完整 | ✅ `jsc-persona:persona-guest` |
| Codex | ✅ `$<name>` | ❌ | ⚠ 需自行以子任務模擬 |
| Antigravity | ✅ `/jsc-persona:<name>` | ❌ | ⚠ |
| OpenCode | ✅ 依描述自動觸發 | ❌ | ⚠ |
> 沒有 hook 的助理仍會遵守 CLI 層的檢查(`--session` 綁定、`require_owner``require_member`、
> guest 唯讀),但那是**自律**而非強制:真正的 deny 只有 Claude Code 的 `PreToolUse` 做得到。
---
## 安裝 / 更新 / 移除
> 指令中的 repo 網址換成你的:`https://gitea.jsc.idv.tw/plugins/persona.git`
> **Claude / Codex 從 git URL 安裝(會 clone 遠端),請先把本 repo `push` 到 gitea。**
### Claude Code
```bash
claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
claude plugin install jsc-persona@jsc-plugins
# 更新
claude plugin marketplace update jsc-plugins
claude plugin update jsc-persona@jsc-plugins
# 移除
claude plugin uninstall jsc-persona@jsc-plugins
```
- 工作階段內 slash 版(等價):把 `claude plugin` 換成 `/plugin`
- 本機開發(免 push):`claude plugin marketplace add /home/coder/plugins/persona` 後再 install。
- 安裝後**重啟工作階段**讓 hooks 生效;用 `/hooks` 確認六個 hook 都在。
### Codex
```bash
codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
codex plugin add jsc-persona@jsc-plugins
codex plugin marketplace upgrade jsc-plugins # 更新
codex plugin remove jsc-persona@jsc-plugins # 移除
```
### Antigravity`agy`
> `agy plugin install <url>` 目前只支援 github.comgitea 請 clone 後用本地路徑。
```bash
git clone https://gitea.jsc.idv.tw/plugins/persona.git ~/plugins/persona
agy plugin install ~/plugins/persona
# 更新:git -C ~/plugins/persona pull && agy plugin uninstall jsc-persona && agy plugin install ~/plugins/persona
```
### OpenCode
```bash
git clone https://gitea.jsc.idv.tw/plugins/persona.git ~/plugins/persona
mkdir -p ~/.config/opencode/skills
cp -r ~/plugins/persona/skills/* ~/.config/opencode/skills/
```
> **Windows PowerShell**`cp -r A B` → `Copy-Item A B -Recurse -Force`、`~` → `$HOME`。
### headless 一次性執行
| 助理 | 指令 |
| --- | --- |
| Claude Code | `claude -p "/jsc-persona:persona-chat lumi"` |
| Codex | `codex exec '$persona-chat lumi'` |
| Antigravity | `agy -p "/jsc-persona:persona-chat lumi"` |
| OpenCode | `opencode run "用 lumi 這個人格跟我聊聊"` |
---
## 設計取捨(讀之前先知道)
- **記憶是被策展的,不是全存**:逐字稿進 `journal/`,但只有經過語意分析、有顯著度的內容才進短期記憶,
再由 `persona-memory` 決定什麼值得成為長期記憶。**遺忘是功能**。
- **事實與推測分離**:推論走思維導圖(`mindmap/threads/`),驗證後才升格長期記憶。
- **鎖的擁有者是 session 不是 process**CLI 跑完就結束,所以租約判定只看心跳。
- **guest 唯讀**:受邀人格不能在別人的 session 裡改自己的長期記憶(避免兩個程序同時寫),
只能把見聞放進 `memory/inbox/`,等它自己被載入時消化。
- **hook 是強制、SKILL 是引導**SKILL.md 寫的規則模型可能忘記,hook 不會。
- **「不要說出來」要靠設計而不是靠忍住**:所以心裡話有自己的指令與檔案,
而且那個指令**印不出內容**——就算不小心把輸出貼上去,使用者也只看到「💭 心想 N 句」。
- **重複用相似度擋、不用語意判斷**:字元層級的比對沒有模型成本、行為可預期,
誤判時有 `--allow-repeat` 可救;漏判的代價(人格自我重複)比誤判高。
## 新增/修改 skill
1. 複製一個現有 skill 目錄,改 `SKILL.md``name``description`(描述要寫清楚何時用、何時不用)。
2. 需要新的狀態操作 → 加到 `scripts/persona.mjs` 的子指令,並在 `scripts/selftest.mjs` 補測試。
3. 動到隔離規則 → 一定要在 `selftest.mjs` 的第 ③(隔離)/⑧(guest 與劇場模式)區加對應案例,跑到全綠。
4. 把 skill 補進上方「Skills 目錄」區塊。
5. bump `.claude-plugin/plugin.json``.codex-plugin/plugin.json``plugin.json` 三處 `version`commit 後 push。