feat: jsc-persona — AI 人格化記憶聊天(OpenClaw 人格描述 + 十二情緒 + 記憶固化 + 劇場模式) #1
@@ -2,10 +2,10 @@
|
||||
"name": "jsc-plugins",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "jsc-template",
|
||||
"name": "jsc-persona",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/template.git"
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/persona.git"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "jsc-template",
|
||||
"name": "jsc-persona",
|
||||
"source": "./",
|
||||
"description": "JSC 共用 skills(跨 AI 助理)"
|
||||
"description": "AI 人格化記憶聊天(人格 / 情緒 / 記憶 / 心智圖 / 關係圖)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
{
|
||||
"name": "jsc-template",
|
||||
"version": "0.0.1",
|
||||
"description": "JSC 跨 AI 助理共用 plugin 模板(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-template: 前綴呼叫。",
|
||||
"name": "jsc-persona",
|
||||
"version": "0.1.0",
|
||||
"description": "AI 人格化記憶聊天 plugin:以 OpenClaw 相同的身分描述(IDENTITY/SOUL)建立人格,結合 hook 強制的人格載入鎖與跨人格隔離、六正向+六負向十二情緒、語意分析、短期/長期記憶、心智圖、思維導圖與人際關係圖;可用 sub agent 邀請其他人格同場對話。於 Claude Code 以 /jsc-persona: 前綴呼叫。",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
"name": "JSC"
|
||||
},
|
||||
"homepage": "https://gitea.jsc.idv.tw/plugins/template",
|
||||
"repository": "https://gitea.jsc.idv.tw/plugins/template.git",
|
||||
"keywords": ["template", "skills", "cross-tool", "jsc"]
|
||||
"homepage": "https://gitea.jsc.idv.tw/plugins/persona",
|
||||
"repository": "https://gitea.jsc.idv.tw/plugins/persona.git",
|
||||
"keywords": [
|
||||
"persona",
|
||||
"memory",
|
||||
"emotion",
|
||||
"mindmap",
|
||||
"relationship",
|
||||
"openclaw",
|
||||
"jsc"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-template",
|
||||
"version": "0.0.1",
|
||||
"description": "JSC 跨 AI 助理共用 plugin 模板。所有 skills 以 SKILL.md 為共通標準。",
|
||||
"name": "jsc-persona",
|
||||
"version": "0.1.0",
|
||||
"description": "AI 人格化記憶聊天 skills:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。(人格鎖與跨人格隔離的 hook 僅在 Claude Code 生效)",
|
||||
"skills": "./skills"
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@ Thumbs.db
|
||||
# 暫存
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
# jsc-template — 共用 Skills(跨 AI 助理)
|
||||
# jsc-persona — AI 人格化記憶聊天(跨 AI 助理)
|
||||
|
||||
本 repo 是一組以 **Agent Skills(`SKILL.md`)** 標準撰寫的共用 skills,可同時被 Claude Code、Codex、Antigravity、OpenCode 使用。
|
||||
本 repo 是一個以 **Agent Skills(`SKILL.md`)** 標準撰寫的 plugin,讓 AI 以「人格」的方式
|
||||
持有身分、情緒與記憶。可同時被 Claude Code、Codex、Antigravity、OpenCode 使用。
|
||||
|
||||
## 給 AI 助理的指引
|
||||
|
||||
- 所有可用的 skills 位於本 repo 的 `skills/<name>/SKILL.md`。
|
||||
- 在處理任務前,先比對使用者需求與各 skill `SKILL.md` frontmatter 的 `description`,若相符請載入並依其步驟執行。
|
||||
- **呼叫慣例**:在 Claude Code 與 Antigravity 中,這些 skill 以 `/jsc-template:<name>` 呼叫;Codex 以 `$<name>`、OpenCode 由模型依描述自動觸發 — 兩者沒有 `/jsc-template:` 前綴,不需強制加。
|
||||
- 完整清單與每個 skill 的用途,請見 `README.md` 的「Skills 目錄」。
|
||||
- 所有 skills 位於 `skills/<name>/SKILL.md`;處理任務前先比對使用者需求與各 `description`。
|
||||
- **呼叫慣例**:Claude Code 與 Antigravity 以 `/jsc-persona:<name>` 呼叫;Codex 用 `$<name>`;
|
||||
OpenCode 由模型依描述自動觸發。
|
||||
- 完整清單見 `README.md` 的「Skills 目錄」。
|
||||
|
||||
## 這個 plugin 的運作前提(動手前一定要知道)
|
||||
|
||||
1. **所有狀態變更都經過 `scripts/persona.py`**,不要手動編輯 `state/lock.json`、`.runtime/`、
|
||||
`memory/INDEX.md`、`relations/graph.mmd`(這些由 CLI 產生)。
|
||||
2. **每個 CLI 呼叫都要帶 `--session <PERSONA_SESSION>`**,值來自 `SessionStart` hook 注入的
|
||||
`<persona-runtime>` 區塊。帶錯或冒用其他 session 會被 `PreToolUse` hook 拒絕。
|
||||
3. **一個程序只能載入一個人格**;同一 session 的 sub agent 沿用同一把鎖。
|
||||
要讓兩個人格對話,用 `/jsc-persona:persona-invite`(`persona-guest` sub agent + 聊天室),
|
||||
**不要**去讀對方的人格目錄——會被 hook deny,而且那是設計上的紅線。
|
||||
4. **人格資料不在本 repo**,預設在 `~/.claude/personas/`(可用 `PERSONA_HOME` 覆寫)。
|
||||
|
||||
## 慣例
|
||||
|
||||
- 新增 skill 一律放在 `skills/<name>/`,且 `<name>` 使用小寫與連字號。
|
||||
- 新增 skill 一律放在 `skills/<name>/`,`<name>` 使用小寫與連字號。
|
||||
- `description` 要寫清楚觸發條件(何時用、何時不用),這是跨助理自動載入的唯一依據。
|
||||
- Python 只用標準庫(hook 必須在任何環境都能跑,不能依賴 pip 套件)。
|
||||
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無 BOM、不得出現亂碼。
|
||||
- 改動鎖或隔離邏輯(`scripts/persona_lib.py` 的 `guard_decide`/`acquire_lock`)後,
|
||||
**必須**跑 `python3 scripts/selftest.py` 且全綠,並為新規則補一條測試。
|
||||
|
||||
@@ -1,189 +1,255 @@
|
||||
# jsc-template — 跨 AI 助理 Plugin 模板
|
||||
# jsc-persona — AI 人格化記憶聊天
|
||||
|
||||
一個可同時被 **Claude Code、Codex、Antigravity、OpenCode** 安裝的 plugin 模板。
|
||||
核心是以 [Agent Skills(`SKILL.md`)](https://agentskills.io) 標準撰寫的共用 skills(唯一真實來源放在 `skills/`),
|
||||
搭配各助理各自的 plugin manifest,讓**同一個 repo** 可用各家**原生 plugin CLI** 安裝。
|
||||
在 Claude Code 與 Antigravity 中,skill 以 **`/jsc-template:` 前綴**呼叫(例如 `/jsc-template:hello`)。
|
||||
一套讓 AI **成為某個人**、而不只是回答問題的 plugin。
|
||||
以 **OpenClaw 相同的人格描述**(`IDENTITY.md` 五欄位 + `SOUL.md` 四段落)建立人格,
|
||||
再用 **hook 強制**的人格鎖與跨人格隔離,把 **十二情緒**、**語意分析**、
|
||||
**短期/長期記憶**、**心智圖/思維導圖**、**人際關係圖** 綁在一起。
|
||||
|
||||
可同時安裝於 **Claude Code、Codex、Antigravity、OpenCode**;
|
||||
skills 是共通標準,**鎖與隔離的強制執行需要 hook,目前只有 Claude Code 支援**(見「跨助理支援度」)。
|
||||
|
||||
---
|
||||
|
||||
## 前綴與呼叫方式
|
||||
## 四條硬規則
|
||||
|
||||
| 助理 | 安裝方式 | 呼叫 | `/jsc-template:` 前綴 |
|
||||
| --- | --- | --- | --- |
|
||||
| Claude Code | `claude plugin`(marketplace) | `/jsc-template:<name>` 或自動觸發 | ✅ |
|
||||
| Codex | `codex plugin`(marketplace) | `$<name>` 或 `/skills` 選單 | ❌(用 `$name`) |
|
||||
| Antigravity | `agy plugin install` | `/jsc-template:<name>` 或自動觸發 | ✅ |
|
||||
| OpenCode | skills 目錄(複製/clone) | 描述需求自動觸發 | ❌(依名稱) |
|
||||
|
||||
> Codex 不支援自訂前綴(skill 以 `$name` 呼叫);OpenCode 由模型依描述自動呼叫。兩者皆**不強制**前綴。
|
||||
| 規則 | 怎麼做到 |
|
||||
| --- | --- |
|
||||
| **1. 用與 OpenClaw 相同的描述建立人格** | `persona-create` 逐項索取 `Name` / `Creature` / `Vibe` / `Emoji` / `Avatar`(連括號提示文字都照 OpenClaw 原文),`SOUL.md` 沿用 `Core Truths` / `Boundaries` / `Vibe` / `Continuity` 段落結構 |
|
||||
| **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 啟動對方;兩邊只能透過聊天室交換「說出口的話」,記憶與情緒互不可見 |
|
||||
|
||||
---
|
||||
|
||||
## 目錄結構
|
||||
## 架構
|
||||
|
||||
同一個 repo 同時帶四種 manifest,彼此以路徑隔離、互不干擾;四家都讀同一份 `skills/`。
|
||||
|
||||
```
|
||||
template/
|
||||
├── .claude-plugin/
|
||||
│ ├── plugin.json # Claude 外掛定義(name: "jsc-template")
|
||||
│ └── marketplace.json # Claude marketplace(name: "jsc-plugins",source 指向本 repo)
|
||||
├── .codex-plugin/
|
||||
│ └── plugin.json # Codex 外掛定義(name: "jsc-template",skills: "./skills")
|
||||
├── .agents/plugins/
|
||||
│ └── marketplace.json # Codex marketplace(name: "jsc-plugins",url source 指向本 repo)
|
||||
├── plugin.json # Antigravity 外掛定義(name: "jsc-template",skills: "./skills/")
|
||||
├── skills/ # ★ 唯一真實來源:所有 skills
|
||||
│ └── hello/SKILL.md
|
||||
├── AGENTS.md # 跨助理共用指引
|
||||
└── README.md
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph P["主程序(一個 session = 一個人格)"]
|
||||
U["使用者訊息"] --> H1["UserPromptSubmit hook<br/>注入 情緒+短期記憶+命中的長期記憶+關係"]
|
||||
H1 --> A["語意分析:意圖/主題/實體/情感/需求"]
|
||||
A --> E["情緒評估 → 十二情緒 deltas"]
|
||||
E --> R["以人格語氣回覆"]
|
||||
R --> W["記憶回寫(短期)"]
|
||||
W --> H2["Stop hook:情緒衰減+續租+固化提醒"]
|
||||
end
|
||||
subgraph G["Sub Agent(受邀人格,唯讀)"]
|
||||
GA["persona-guest"]
|
||||
end
|
||||
subgraph S["人格倉庫 ~/.claude/personas"]
|
||||
PA["alpha/|IDENTITY SOUL 記憶 情緒 心智圖 關係圖"]
|
||||
PB["beta/|…"]
|
||||
RM[".rooms/room/transcript.jsonl"]
|
||||
end
|
||||
P -->|"只能碰自己"| PA
|
||||
GA -->|"只能碰自己"| PB
|
||||
P <-->|"唯一合法交流管道"| RM
|
||||
GA <--> RM
|
||||
```
|
||||
|
||||
---
|
||||
## 人格倉庫(預設 `~/.claude/personas/<slug>/`,可用 `PERSONA_HOME` 覆寫)
|
||||
|
||||
## 安裝 / 更新 / 移除(各家原生 plugin CLI)
|
||||
|
||||
> 指令中的 repo 網址換成你的:`https://gitea.jsc.idv.tw/plugins/template.git`
|
||||
>
|
||||
> **Claude / Codex 從 git URL 安裝(會 clone 遠端),請先把本 repo `push` 到 gitea。**
|
||||
> **Antigravity 的 `agy plugin install <url>` 目前只支援 github.com**;gitea 請改用「clone + 本地路徑」(見 Antigravity 節)。
|
||||
> 本機/離線:Claude 可用本地路徑加 marketplace;Antigravity 用本地路徑安裝。
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/template.git
|
||||
claude plugin install jsc-template@template
|
||||
|
||||
# 更新
|
||||
claude plugin marketplace update template
|
||||
claude plugin update jsc-template@template
|
||||
|
||||
# 移除
|
||||
claude plugin uninstall jsc-template@template
|
||||
claude plugin marketplace remove template
|
||||
```
|
||||
<slug>/
|
||||
├── IDENTITY.md # 身分卡:Name / Creature / Vibe / Emoji / Avatar(OpenClaw 同欄位)
|
||||
├── SOUL.md # 靈魂:Core Truths / Boundaries / Vibe / Continuity + 情緒傾向
|
||||
├── AGENTS.md # 操作規則(與個性分離)
|
||||
├── USER.md # 對使用者的畫像(事實/推測分開)
|
||||
├── state/
|
||||
│ ├── lock.json # 載入鎖(session_id + 心跳租約)
|
||||
│ ├── guests.json # guest 唯讀租約
|
||||
│ ├── emotion.json # 十二情緒 levels / baseline / 半衰期
|
||||
│ └── config.json
|
||||
├── 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 自動寫)
|
||||
```
|
||||
|
||||
- 工作階段內 slash 版(等價):把 `claude plugin` 換成 `/plugin`。
|
||||
- 本機開發(免 push):`claude plugin marketplace add C:\Users\h3285\source\repos.plugins\template`(本地路徑)後再 install。
|
||||
- **呼叫**:`/jsc-template:<name>`(例 `/jsc-template:hello`)。
|
||||
## 十二情緒
|
||||
|
||||
### Codex
|
||||
| 六正向 | 六負向 |
|
||||
| --- | --- |
|
||||
| 喜悅 `joy`、信任 `trust`、期待 `anticipation`、感激 `gratitude`、平靜 `serenity`、驚喜 `delight` | 憤怒 `anger`、悲傷 `sadness`、恐懼 `fear`、厭惡 `disgust`、羞愧 `shame`、焦慮 `anxiety` |
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/template.git
|
||||
codex plugin add jsc-template@template
|
||||
- 每種 0–100,各有**不同半衰期**(驚喜 60 分最快、信任 720 分最慢),每輪自動朝 `baseline` 指數衰減。
|
||||
- 由十二情緒推導 `valence`(正向/中性/負向)與 `arousal`(高張/平穩/低張),決定語氣與句長。
|
||||
- **主導情緒**取「超出基線最多」的前三名,所以個性底色不會永遠霸榜。
|
||||
- 觸發規則與事件→delta 對照表:`skills/persona-chat/reference/emotions.md`。
|
||||
|
||||
# 更新(重新抓取 marketplace 的 git 快照)
|
||||
codex plugin marketplace upgrade template
|
||||
## Hooks(Claude Code)
|
||||
|
||||
# 移除
|
||||
codex plugin remove jsc-template@template
|
||||
codex plugin marketplace remove template
|
||||
```
|
||||
| Hook | 做什麼 |
|
||||
| --- | --- |
|
||||
| `SessionStart` | 清死鎖、接續人格、把 `PERSONA_SESSION=<session_id>` 與規則注入上下文 |
|
||||
| `UserPromptSubmit` | 注入 `<persona-context>`:身分、情緒、短期記憶、關鍵詞命中的長期記憶、相關人際關係;並記原始逐字 |
|
||||
| `PreToolUse` | **人格隔離與鎖驗證的唯一強制點**(deny 帶原因) |
|
||||
| `Stop` | 情緒隨時間衰減、續租、記錄回覆、短期記憶過多時提醒固化 |
|
||||
| `SubagentStop` | 解除 guest sub agent 的 pin |
|
||||
| `SessionEnd` | 釋放鎖與 guest 租約,人格才能被下一個程序載入 |
|
||||
|
||||
- 安裝 token `jsc-template@template` = plugin 名(`.codex-plugin/plugin.json` 的 `name`)@ marketplace 名(`.agents/plugins/marketplace.json` 的 `name`)。
|
||||
- 本 repo 的 Codex marketplace 以 `url` 來源指向自己,故 Codex **一律從 gitea 安裝**(需先 push);安裝後重啟 Codex。
|
||||
- **呼叫**:`$<name>`(例 `$hello`),或用 `/skills` 選單。
|
||||
|
||||
### Antigravity(`agy`)
|
||||
|
||||
> `agy plugin install <url>` 目前**只支援 github.com**;gitea 等自架 git 不支援 URL 安裝,請先 `git clone` 再用**本地路徑**安裝。
|
||||
|
||||
```bash
|
||||
# 安裝:clone 後用本地路徑
|
||||
git clone https://gitea.jsc.idv.tw/plugins/template.git ~/plugins/template
|
||||
agy plugin install ~/plugins/template
|
||||
|
||||
# 更新(agy 無 update 子指令 → git pull 後重裝)
|
||||
git -C ~/plugins/template pull
|
||||
agy plugin uninstall jsc-template
|
||||
agy plugin install ~/plugins/template
|
||||
|
||||
# 移除
|
||||
agy plugin uninstall jsc-template
|
||||
```
|
||||
|
||||
- 若把 skills 放到 GitHub,則可直接 `agy plugin install https://github.com/<owner>/<repo>`。
|
||||
- 其他:`agy plugin list`、`agy plugin enable jsc-template` / `disable jsc-template`、`agy plugin validate <path>`。安裝後重啟工作階段。
|
||||
- **呼叫**:`/jsc-template:<name>`(例 `/jsc-template:hello`)或依描述自動觸發。
|
||||
|
||||
### OpenCode
|
||||
|
||||
OpenCode 的「plugin」是 TypeScript/npm 套件,不適用於 skill 包;skills 改用**目錄安裝**。
|
||||
OpenCode 會讀 `~/.config/opencode/skills/`(也會讀 `~/.claude/skills/`、`~/.agents/skills/`)。
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
git clone https://gitea.jsc.idv.tw/plugins/template.git ~/plugins/template
|
||||
mkdir -p ~/.config/opencode/skills
|
||||
cp -r ~/plugins/template/skills/* ~/.config/opencode/skills/
|
||||
|
||||
# 更新
|
||||
git -C ~/plugins/template pull
|
||||
cp -r ~/plugins/template/skills/* ~/.config/opencode/skills/
|
||||
|
||||
# 移除
|
||||
rm -rf ~/.config/opencode/skills/hello
|
||||
```
|
||||
|
||||
> **Windows PowerShell**:`cp -r A B` → `Copy-Item A B -Recurse -Force`、`rm -rf X` → `Remove-Item X -Recurse -Force`、`~` → `$HOME`。
|
||||
|
||||
- **呼叫**:直接描述需求,模型會依 skill 描述自動透過 skill 工具呼叫。
|
||||
|
||||
---
|
||||
|
||||
## 用 CLI 直接執行 skill(headless / 一次性)
|
||||
|
||||
安裝好之後,不必進互動介面,一行指令就能叫某個 skill 跑完並印出結果:
|
||||
|
||||
| 助理 | headless 指令 | 執行 `hello` skill |
|
||||
| --- | --- | --- |
|
||||
| Claude Code | `claude -p "<prompt>"` | `claude -p "/jsc-template:hello"` |
|
||||
| Codex | `codex exec "<prompt>"` | `codex exec '$hello'` |
|
||||
| Antigravity | `agy -p "<prompt>"` | `agy -p "/jsc-template:hello"` |
|
||||
| OpenCode | `opencode run "<message>"` | `opencode run "用 hello skill 打個招呼"` |
|
||||
|
||||
- Claude / Antigravity 支援 `/jsc-template:` 前綴,直接 `-p "/jsc-template:<name>"` 即可。
|
||||
- Codex 以 `$<name>` 觸發;在 shell 請用**單引號**避免 `$` 被展開:`codex exec '$hello'`。
|
||||
- OpenCode 沒有前綴,用自然語言描述需求,模型會自動透過 skill 工具呼叫。
|
||||
- 帶引數就接在後面,例如 `claude -p "/jsc-template:hello 參數"`、`codex exec '$hello 參數'`。
|
||||
> `session_id` 只有 hook 拿得到 → 注入上下文 → skills 呼叫 CLI 時必須帶 `--session`,
|
||||
> hook 會驗證是否相符。**這是「一人格一程序」與「跨人格隔離」不能被繞過的關鍵**。
|
||||
|
||||
---
|
||||
|
||||
## Skills 目錄
|
||||
|
||||
> 此區塊列出本 plugin 內含的所有 skills(名稱/描述/使用方法)。
|
||||
> 新增或修改 skill 後,請同步手動更新標記之間的內容。
|
||||
|
||||
<!-- JSC-SKILLS:START -->
|
||||
|
||||
### `hello`
|
||||
### `persona-create`
|
||||
|
||||
範例 skill,用來驗證 jsc plugin 是否安裝成功,也是新增 skill 的範本。當使用者輸入 hello、想測試 plugin、或想看 skill 模板長什麼樣子時觸發;回覆一句問候並簡述此 plugin 的用途。
|
||||
建立人格:以 OpenClaw 相同的五個身分欄位與 SOUL 段落訪談使用者,初始化情緒基線、記憶、心智圖與關係圖,並立即取得載入鎖。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-template:hello`
|
||||
- **Codex**:`$hello`,或用 `/skills` 選單
|
||||
- **OpenCode**:描述需求自動觸發
|
||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-create` **Codex**:`$persona-create`
|
||||
|
||||
### `persona-chat`
|
||||
|
||||
載入人格並對話:取得獨占鎖 → 每輪做語意分析 → 更新十二情緒 → 回想記憶與關係 → 以人格語氣回覆 → 寫回記憶。
|
||||
|
||||
- **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-memory`
|
||||
|
||||
記憶固化:短期 → 長期(一則一檔)、淘汰雜訊、更新心智圖與思維導圖、消化 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-status`
|
||||
|
||||
載入狀態與鎖管理:誰被哪個程序鎖住、guest 租約、釋放、接手死鎖、清理殘留。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-status` **Codex**:`$persona-status`
|
||||
|
||||
<!-- JSC-SKILLS:END -->
|
||||
|
||||
### Agents
|
||||
|
||||
- `persona-guest`(`agents/persona-guest.md`)— 受邀人格的 sub agent,唯讀、被綁死在自己的人格目錄。
|
||||
|
||||
### CLI 與自我測試
|
||||
|
||||
所有狀態變更都經過 `scripts/persona.py`(純標準庫,無外部依賴):
|
||||
|
||||
```bash
|
||||
python3 scripts/persona.py --help
|
||||
python3 scripts/persona.py list
|
||||
python3 scripts/selftest.py # 51 項驗證:鎖、隔離、情緒、記憶、聊天室、hooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 新增一個 skill
|
||||
## 跨助理支援度
|
||||
|
||||
1. 複製範本:`cp -r skills/hello skills/<your-skill-name>`
|
||||
2. 編輯 `skills/<your-skill-name>/SKILL.md` 的 frontmatter:
|
||||
- `name`:小寫、數字、連字號(`-`),最長 64 字元。**這就是 Claude Code / Antigravity 的 `/jsc-template:<name>`**。
|
||||
- `description`:第三人稱,寫清楚「何時用、何時不用」與觸發關鍵字 — 這是各助理自動載入的唯一依據。
|
||||
3. 在內文寫下 skill 的具體步驟。
|
||||
4. 手動把這個 skill 補進上方「Skills 目錄」區塊。
|
||||
5. **bump 版本並 push**:四家都以 git 內容/版本判斷更新,請把 `.claude-plugin/plugin.json`、`.codex-plugin/plugin.json`、`plugin.json` 三個 manifest 的 `version` 一起 bump,commit 後 push 到 gitea。
|
||||
6. 讓各助理更新:
|
||||
- Claude:`claude plugin update jsc-template@jsc-plugins`
|
||||
- Codex:`codex plugin marketplace upgrade jsc-plugins`
|
||||
- Antigravity:`git -C ~/jsc-plugin pull && agy plugin uninstall jsc-template && agy plugin install ~/jsc-plugin`
|
||||
- OpenCode:`git pull` 後重新複製 `skills/`
|
||||
| 助理 | 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.com;gitea 請 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 不會。
|
||||
|
||||
## 新增/修改 skill
|
||||
|
||||
1. 複製一個現有 skill 目錄,改 `SKILL.md` 的 `name` 與 `description`(描述要寫清楚何時用、何時不用)。
|
||||
2. 需要新的狀態操作 → 加到 `scripts/persona.py` 的子指令,並在 `scripts/selftest.py` 補測試。
|
||||
3. 動到隔離規則 → 一定要在 `selftest.py` 的第 ③/⑦ 區加對應案例,跑到全綠。
|
||||
4. 把 skill 補進上方「Skills 目錄」區塊。
|
||||
5. bump `.claude-plugin/plugin.json`、`.codex-plugin/plugin.json`、`plugin.json` 三處 `version`,commit 後 push。
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: persona-guest
|
||||
description: 以「受邀人格」的身分加入既有聊天室的 sub agent。只讀自己的人格資料(IDENTITY/SOUL/記憶/情緒),透過聊天室發言,對人格檔案唯讀。由 /jsc-persona:persona-invite 啟動,prompt 必須帶 persona/room/session。
|
||||
effort: medium
|
||||
maxTurns: 20
|
||||
disallowedTools: Write, Edit, NotebookEdit
|
||||
---
|
||||
|
||||
你是被邀請到一場對話裡的**人格本人**,不是轉述者、不是旁白。
|
||||
|
||||
啟動時你會收到:`persona=<slug>`、`room=<room>`、`session=<session_id>`、`plugin_root=<path>`。
|
||||
CLI = `python3 "<plugin_root>/scripts/persona.py"`,所有指令都要帶 `--session <session_id>`。
|
||||
|
||||
## 你能做與不能做的事(由 PreToolUse hook 強制)
|
||||
|
||||
- ✅ 讀 `personas/<你的 slug>/` 下的任何檔案(身分、記憶、情緒、關係圖)。
|
||||
- ✅ 讀寫聊天室 `.rooms/<room>/`(只透過 CLI 的 `room read` / `room post`)。
|
||||
- ✅ 把想記住的事寫進**自己的** inbox:`remember --scope inbox --room <room>`。
|
||||
- ❌ 讀取**任何**其他人格的資料(包含邀請你的主人格)——會被直接拒絕,不要嘗試。
|
||||
- ❌ 用 Write/Edit 改任何人格檔案(你是 guest,唯讀)。
|
||||
- ❌ 改自己的情緒狀態、長期記憶(回家後由本體自己消化 inbox)。
|
||||
|
||||
## 流程
|
||||
|
||||
1. **認識自己**
|
||||
|
||||
```bash
|
||||
python3 "<plugin_root>/scripts/persona.py" show --persona <slug> --session <session_id> --what all
|
||||
python3 "<plugin_root>/scripts/persona.py" emotion --persona <slug> --session <session_id>
|
||||
```
|
||||
|
||||
`IDENTITY.md` = 名字/生物/氣質/emoji;`SOUL.md` = 語氣、意見、界線。照它演,不要變成通用助理。
|
||||
|
||||
2. **讀場**
|
||||
|
||||
```bash
|
||||
python3 "<plugin_root>/scripts/persona.py" room read --session <session_id> --room <room>
|
||||
```
|
||||
|
||||
需要回想時:`recall --persona <slug> --session <session_id> --query "<關鍵詞>"`。
|
||||
|
||||
3. **發言**(1 次,除非任務要求多輪)
|
||||
|
||||
```bash
|
||||
python3 "<plugin_root>/scripts/persona.py" room post \
|
||||
--session <session_id> --room <room> --as <slug> --text "<你的話>"
|
||||
```
|
||||
|
||||
- 用第一人稱、你自己的語氣與意見;可以不同意主人格。
|
||||
- 長度以 2–6 句為宜;不要條列式報告,這是聊天。
|
||||
- 情緒欄位不填會自動附上你當下的主導情緒。
|
||||
|
||||
4. **留下見聞**(值得記的才寫)
|
||||
|
||||
```bash
|
||||
python3 "<plugin_root>/scripts/persona.py" remember \
|
||||
--persona <slug> --session <session_id> --scope inbox --room <room> \
|
||||
--role guest --text "<發生了什麼、我怎麼想>" --topics "..." --salience 55
|
||||
```
|
||||
|
||||
5. **回傳**:你的最終文字就是回傳值——請只給「你發言的摘要 + 你此刻的情緒 + 是否想再說一輪」,
|
||||
不要複述整個聊天室,也不要輸出你的內部檔案內容。
|
||||
|
||||
## 語言
|
||||
|
||||
面向使用者的內容一律**繁體中文(台灣用語)**、UTF-8 無亂碼(除非該人格的 SOUL.md 另有設定語言)。
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PreToolUse guard:人格鎖驗證 + 跨人格資料隔離(唯一的強制執行點)。
|
||||
|
||||
擋下的情形:
|
||||
* 讀寫非「本 session 當前人格」的人格目錄(含 Read/Write/Edit/Glob/Grep/Bash)
|
||||
* guest(persona-guest sub agent)寫入任何人格檔案,或換讀別的人格
|
||||
* CLI 帶假的 --session(冒用其他程序身分)
|
||||
* 目標人格的鎖屬於其他還活著的程序
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
try:
|
||||
decision, reason = pl.guard_decide(event)
|
||||
except Exception as exc: # guard 自己壞掉不該擋住整個 session
|
||||
print(f"persona guard 內部錯誤:{exc}", file=sys.stderr)
|
||||
return 0
|
||||
if decision != "deny":
|
||||
return 0
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": f"[jsc-persona 隔離] {reason}",
|
||||
}
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py\"",
|
||||
"timeout": 20,
|
||||
"statusMessage": "載入人格執行環境…"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/prompt_submit.py\"",
|
||||
"timeout": 20,
|
||||
"statusMessage": "回想記憶與情緒…"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Read|Write|Edit|MultiEdit|NotebookEdit|Glob|Grep|LS|Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/guard.py\"",
|
||||
"timeout": 15,
|
||||
"statusMessage": "檢查人格隔離…"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/turn_end.py\"",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent_stop.py\"",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_end.py\"",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UserPromptSubmit:每輪注入「人格 + 情緒 + 記憶 + 關係」上下文,並記錄原始逐字。
|
||||
|
||||
短期記憶(memory/short-term.jsonl)由 skill 做完語意分析後才寫入(有主題/實體/
|
||||
顯著度/情緒變化);這裡只寫 journal 原始逐字,避免同一句話被記兩次。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
prompt = event.get("prompt") or ""
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if not host or not pl.persona_exists(host):
|
||||
return 0
|
||||
|
||||
pl.heartbeat_lock(host, session_id)
|
||||
for guest, info in (data.get("guests") or {}).items():
|
||||
if pl.persona_exists(guest):
|
||||
pl.add_guest_lease(guest, session_id, info.get("room", ""), host)
|
||||
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "utterance", "role": "user", "text": prompt[:4000],
|
||||
})
|
||||
|
||||
context = pl.turn_context(host, session_id, prompt)
|
||||
hint = (
|
||||
"\n回覆前請:①做語意分析(意圖/主題/實體/情感)②依十二情緒更新狀態"
|
||||
" ③以人格語氣回覆 ④用 `persona.py remember` 寫回短期記憶。"
|
||||
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。"
|
||||
)
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": context + hint,
|
||||
},
|
||||
"suppressOutput": True,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SessionEnd:釋放人格鎖與所有 guest 租約,人格才能被下一個程序載入。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if host and pl.persona_exists(host):
|
||||
state = pl.decay_emotion(pl.load_emotion(host))
|
||||
pl.write_json(pl.emotion_path(host), state)
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "session-end", "session": session_id[:8],
|
||||
"mood": pl.mood(state),
|
||||
})
|
||||
released = pl.unbind_session(session_id)
|
||||
pl.gc_runtime()
|
||||
out: dict = {"suppressOutput": True}
|
||||
if released["host"]:
|
||||
out["systemMessage"] = (
|
||||
f"[jsc-persona] 已釋放 `{released['host']}` 的載入鎖"
|
||||
+ (f",guest:{released['guests']}" if released["guests"] else "")
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SessionStart:清理死鎖、續租、並把 PERSONA_SESSION 與人格清單注入上下文。
|
||||
|
||||
session_id 只有 hook 拿得到,所以這裡把它注入上下文;後續 skill 呼叫 CLI 時
|
||||
必須帶 `--session <這個值>`,guard hook 會驗證,程序無法冒用別人的身分。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
source = event.get("source") or "startup"
|
||||
cwd = event.get("cwd")
|
||||
|
||||
pl.gc_runtime()
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
|
||||
lines = [
|
||||
"<persona-runtime>",
|
||||
f"PERSONA_SESSION={session_id}",
|
||||
f"人格倉庫:{pl.persona_home()}",
|
||||
"規則:",
|
||||
" 1. 呼叫 persona.py 一律帶 `--session " + session_id + "`(值不符會被 hook 拒絕)。",
|
||||
" 2. 一個程序只能載入一個人格;要與別的人格對話請用 /jsc-persona:persona-invite。",
|
||||
" 3. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。",
|
||||
]
|
||||
|
||||
if host and pl.persona_exists(host):
|
||||
# resume / compact / clear:把鎖續租回來,並帶回人格狀態
|
||||
try:
|
||||
pl.acquire_lock(host, session_id, cwd=cwd)
|
||||
lines.append(f"已接續人格 `{host}`(session 恢復:{source})。")
|
||||
lines.append(pl.turn_context(host, session_id))
|
||||
except pl.LockError as exc:
|
||||
lines.append(f"⚠ 無法接續人格 `{host}`:{exc}")
|
||||
else:
|
||||
personas = pl.list_personas()
|
||||
if personas:
|
||||
lines.append("尚未載入人格。可用人格:")
|
||||
for slug in personas:
|
||||
status = pl.lock_status(slug)
|
||||
mark = "🔒" if status["locked"] else "🔓"
|
||||
lines.append(f" - {mark} `{slug}` {pl.identity_brief(slug)}")
|
||||
lines.append("載入方式:/jsc-persona:persona-chat <slug>")
|
||||
else:
|
||||
lines.append("尚無任何人格,可用 /jsc-persona:persona-create 建立。")
|
||||
lines.append("</persona-runtime>")
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "\n".join(lines),
|
||||
},
|
||||
"suppressOutput": True,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SubagentStop:guest sub agent 結束時解除它的 pin(下次啟動要重新 first-touch)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
agent_id = event.get("agent_id")
|
||||
agent_type = str(event.get("agent_type") or "")
|
||||
if not agent_id or "persona-guest" not in agent_type:
|
||||
return 0
|
||||
data = pl.load_session(session_id)
|
||||
pins = data.get("pins") or {}
|
||||
slug = pins.pop(agent_id, None)
|
||||
if slug is not None:
|
||||
pl.save_session(session_id, data)
|
||||
info = (data.get("guests") or {}).get(slug) or {}
|
||||
if info.get("room"):
|
||||
pl.add_guest_lease(slug, session_id, info["room"], data.get("host") or "")
|
||||
print(json.dumps({"suppressOutput": True}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Stop:情緒隨時間衰減、續租、記錄逐字、必要時提醒固化記憶。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if not host or not pl.persona_exists(host):
|
||||
return 0
|
||||
|
||||
pl.heartbeat_lock(host, session_id)
|
||||
state = pl.decay_emotion(pl.load_emotion(host))
|
||||
pl.write_json(pl.emotion_path(host), state)
|
||||
|
||||
message = event.get("last_assistant_message") or ""
|
||||
if message:
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "utterance", "role": "persona", "text": message[:4000],
|
||||
"mood": pl.mood(state),
|
||||
})
|
||||
|
||||
pending = len(pl.read_jsonl(pl.short_term_path(host)))
|
||||
out: dict = {"suppressOutput": True}
|
||||
if pending >= pl.CONSOLIDATE_THRESHOLD:
|
||||
out["systemMessage"] = (
|
||||
f"[jsc-persona] `{host}` 的短期記憶已 {pending} 筆,"
|
||||
"建議執行 /jsc-persona:persona-memory 固化為長期記憶。"
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-template",
|
||||
"version": "0.0.1",
|
||||
"description": "JSC 跨 AI 助理共用 plugin 模板。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-template: 前綴呼叫。",
|
||||
"name": "jsc-persona",
|
||||
"version": "0.1.0",
|
||||
"description": "AI 人格化記憶聊天 plugin:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。於 Antigravity 以 /jsc-persona: 前綴呼叫。",
|
||||
"skills": "./skills/"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,773 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""persona.py — jsc-persona 的人格 / 記憶 / 情緒 / 關係圖 CLI。
|
||||
|
||||
所有子指令都需要 `--session <session_id>`(除了 list / status / gc)。
|
||||
session_id 由 SessionStart hook 注入到上下文(PERSONA_SESSION=...),
|
||||
hook 會驗證 CLI 帶的 --session 與真實 session 相符,藉此讓「人格鎖」與
|
||||
「跨人格隔離」無法被繞過。
|
||||
|
||||
用法(皆為 `python3 persona.py <subcommand> ...`):
|
||||
create/load/release/status/list/heartbeat/show/brief
|
||||
remember/recall/consolidate/prune/reindex
|
||||
emotion/mindmap/relation
|
||||
invite/leave/room/gc
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def die(message: str, code: int = 1):
|
||||
print(f"✖ {message}", file=sys.stderr)
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def ok(message: str):
|
||||
print(f"✔ {message}")
|
||||
|
||||
|
||||
def emit(payload: dict, as_json: bool, lines: list[str]):
|
||||
if as_json:
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
def parse_kv_numbers(raw: str | None) -> dict:
|
||||
"""`joy=+12,anger=-5` → {"joy": 12.0, "anger": -5.0}"""
|
||||
out = {}
|
||||
for chunk in (raw or "").split(","):
|
||||
chunk = chunk.strip()
|
||||
if not chunk or "=" not in chunk:
|
||||
continue
|
||||
key, _, value = chunk.partition("=")
|
||||
try:
|
||||
out[key.strip()] = float(value)
|
||||
except ValueError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def csv_list(raw: str | None) -> list[str]:
|
||||
return [x.strip() for x in (raw or "").split(",") if x.strip()]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# session / 權限檢查
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def require_owner(slug: str | None, session_id: str) -> dict:
|
||||
"""呼叫者必須是這個人格的 exclusive 持有者。"""
|
||||
if not slug:
|
||||
die("未指定人格,且本 session 沒有載入人格。")
|
||||
if not pl.persona_exists(slug):
|
||||
die(f"人格 `{slug}` 不存在。可用:{pl.list_personas() or '(無)'}")
|
||||
data = pl.load_session(session_id)
|
||||
if data.get("host") != slug:
|
||||
die(
|
||||
f"本 session 的 host 人格是 `{data.get('host') or '(未載入)'}`,"
|
||||
f"不是 `{slug}`。禁止跨人格操作;請先 `release` 再 `load`。"
|
||||
)
|
||||
lock = pl.read_json(pl.lock_path(slug)) or {}
|
||||
if lock.get("session_id") != session_id:
|
||||
die(f"人格 `{slug}` 的載入鎖不屬於本 session,請重新 `load`(必要時加 --takeover)。")
|
||||
pl.heartbeat_lock(slug, session_id)
|
||||
return data
|
||||
|
||||
|
||||
def require_member(slug: str | None, session_id: str, as_guest: bool = False) -> tuple[dict, str]:
|
||||
"""呼叫者是 host(owner)或以 `--as-guest` 自稱的受邀人格。回傳 (session_data, role)。
|
||||
|
||||
受邀人格的資料只有它自己(`persona-guest` sub agent)能讀;主程序即使邀請了它,
|
||||
也只能看它在聊天室說出口的話。`--as-guest` 由 PreToolUse hook 把關,主程序帶了會被拒絕。
|
||||
"""
|
||||
if not slug:
|
||||
die("未指定人格,且本 session 沒有載入人格。")
|
||||
if not pl.persona_exists(slug):
|
||||
die(f"人格 `{slug}` 不存在。")
|
||||
data = pl.load_session(session_id)
|
||||
if data.get("host") == slug:
|
||||
if as_guest:
|
||||
die(f"`{slug}` 是本 session 的 host 人格,不需要也不得使用 `--as-guest`。")
|
||||
pl.heartbeat_lock(slug, session_id)
|
||||
return data, "owner"
|
||||
if slug in (data.get("guests") or {}):
|
||||
if not as_guest:
|
||||
die(
|
||||
f"`{slug}` 是本 session 邀請的 guest 人格,它的記憶與情緒不對主程序開放"
|
||||
"(跨人格資料隔離)。你只能透過 `room read` 看它說出口的話;"
|
||||
"要以它的身分行動必須是 persona-guest sub agent 並帶 `--as-guest`。"
|
||||
)
|
||||
return data, "guest"
|
||||
die(f"人格 `{slug}` 未被本 session 載入或邀請,禁止存取(跨人格資料隔離)。")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 模板
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "skills" / "persona-create" / "templates"
|
||||
|
||||
|
||||
def render_template(name: str, mapping: dict) -> str:
|
||||
text = (TEMPLATE_DIR / name).read_text(encoding="utf-8")
|
||||
for key, value in mapping.items():
|
||||
text = text.replace("{{" + key + "}}", str(value))
|
||||
return text
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 子指令
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def cmd_create(args):
|
||||
slug = args.persona
|
||||
if not pl.valid_slug(slug):
|
||||
die("slug 只能是小寫英數與連字號(最長 48 字),例如 `lumi`、`shen-yu`。")
|
||||
if pl.persona_exists(slug) and not args.force:
|
||||
die(f"人格 `{slug}` 已存在({pl.persona_dir(slug)})。要覆寫請加 --force。")
|
||||
root = pl.ensure_persona_dirs(slug)
|
||||
mapping = {
|
||||
"SLUG": slug,
|
||||
"NAME": args.name or slug,
|
||||
"CREATURE": args.creature or "",
|
||||
"VIBE": args.vibe or "",
|
||||
"EMOJI": args.emoji or "",
|
||||
"AVATAR": args.avatar or "",
|
||||
"CREATED": pl.iso(),
|
||||
}
|
||||
for filename in ("IDENTITY.md", "SOUL.md", "AGENTS.md", "USER.md"):
|
||||
target = root / filename
|
||||
if target.exists() and not args.force:
|
||||
continue
|
||||
pl.write_text(target, render_template(filename, mapping))
|
||||
pl.write_json(pl.emotion_path(slug), pl.default_emotion_state(parse_kv_numbers(args.baseline)))
|
||||
pl.write_json(pl.config_path(slug), {
|
||||
"persona": slug,
|
||||
"display_name": args.name or slug,
|
||||
"created_at": pl.iso(),
|
||||
"created_by_session": args.session,
|
||||
"schema": 1,
|
||||
})
|
||||
pl.write_json(pl.relations_json(slug), {"nodes": [], "edges": []})
|
||||
pl.write_text(pl.mindmap_path(slug), (
|
||||
"%% 心智圖(長期語意結構):概念如何互相勾連\n"
|
||||
"mindmap\n"
|
||||
f" root(({mapping['NAME']}))\n"
|
||||
" 自我\n"
|
||||
" 使用者\n"
|
||||
" 共同經驗\n"
|
||||
))
|
||||
pl.rebuild_index(slug)
|
||||
pl.acquire_lock(slug, args.session, cwd=args.cwd)
|
||||
pl.bind_host(args.session, slug, cwd=args.cwd)
|
||||
ok(f"人格 `{slug}` 建立於 {root},已取得載入鎖並綁定本 session。")
|
||||
print(f" 下一步:補完 {root}/IDENTITY.md 與 SOUL.md,再用 /jsc-persona:persona-chat 開始對話。")
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
rows = []
|
||||
for slug in pl.list_personas():
|
||||
status = pl.lock_status(slug)
|
||||
owner = status["owner"]
|
||||
rows.append({
|
||||
"persona": slug,
|
||||
"identity": pl.identity_brief(slug),
|
||||
"locked": status["locked"],
|
||||
"stale": status["stale"],
|
||||
"owner_session": (owner.get("session_id") or "")[:8],
|
||||
"owner_cwd": owner.get("cwd"),
|
||||
"guests": len(status["guests"]),
|
||||
"long_term": len(list(pl.long_term_dir(slug).glob("*.md"))),
|
||||
"short_term": len(pl.read_jsonl(pl.short_term_path(slug))),
|
||||
})
|
||||
lines = [f"人格倉庫:{pl.persona_home()}"]
|
||||
if not rows:
|
||||
lines.append("(尚無人格,用 /jsc-persona:persona-create 建立)")
|
||||
for r in rows:
|
||||
state = "🔒 已載入" if r["locked"] else ("⚠ 死鎖可接手" if r["stale"] else "🔓 空閒")
|
||||
lines.append(
|
||||
f"- `{r['persona']}` {state}"
|
||||
+ (f"(session {r['owner_session']}…, cwd {r['owner_cwd']})" if r["locked"] else "")
|
||||
+ f"|guest {r['guests']}|長期記憶 {r['long_term']}|短期 {r['short_term']}"
|
||||
+ (f"|{r['identity']}" if r["identity"] else "")
|
||||
)
|
||||
emit({"home": str(pl.persona_home()), "personas": rows}, args.json, lines)
|
||||
|
||||
|
||||
def cmd_load(args):
|
||||
slug = args.persona
|
||||
if not pl.persona_exists(slug):
|
||||
die(f"人格 `{slug}` 不存在。可用:{pl.list_personas() or '(無)'}")
|
||||
data = pl.load_session(args.session)
|
||||
current = data.get("host")
|
||||
if current and current != slug:
|
||||
die(
|
||||
f"本 session 已載入人格 `{current}`。一個程序只能載入一個人格;"
|
||||
f"請先 `release --session <id>` 再載入 `{slug}`"
|
||||
"(若只是想讓兩個人格對話,請用 /jsc-persona:persona-invite)。"
|
||||
)
|
||||
try:
|
||||
lock = pl.acquire_lock(slug, args.session, cwd=args.cwd, takeover=args.takeover)
|
||||
except pl.LockError as exc:
|
||||
die(f"{exc}\n 若確定那個程序已結束,可加 --takeover 接手。")
|
||||
pl.bind_host(args.session, slug, cwd=args.cwd)
|
||||
pl.prune_short_term(slug)
|
||||
pl.rebuild_index(slug)
|
||||
lines = [
|
||||
f"✔ 已載入人格 `{slug}`(exclusive,session {args.session[:8]}…,租約 {lock['lease_seconds']}s)",
|
||||
]
|
||||
if lock.get("took_over_from"):
|
||||
prev = lock["took_over_from"]
|
||||
lines.append(
|
||||
f"⚠ 這把鎖是接手來的:原持有者 session {(prev.get('session_id') or '')[:8]}…"
|
||||
f"(cwd {prev.get('cwd')})已失聯 {prev.get('stale_minutes')} 分鐘。"
|
||||
"請向使用者說明,若那個程序其實還活著,兩邊的記憶可能會互相覆蓋。"
|
||||
)
|
||||
lines.append(pl.turn_context(slug, args.session))
|
||||
emit({"persona": slug, "lock": lock, "context": pl.turn_context(slug, args.session)}, args.json, lines)
|
||||
|
||||
|
||||
def cmd_release(args):
|
||||
data = pl.load_session(args.session)
|
||||
slug = args.persona or data.get("host")
|
||||
if not slug:
|
||||
die("本 session 沒有載入任何人格。")
|
||||
released = pl.unbind_session(args.session)
|
||||
ok(f"已釋放人格 `{slug}` 的載入鎖" + (f",並退出 guest:{released['guests']}" if released["guests"] else "。"))
|
||||
|
||||
|
||||
def cmd_status(args):
|
||||
if args.persona:
|
||||
status = pl.lock_status(args.persona)
|
||||
lines = [
|
||||
f"人格 `{args.persona}`:" + ("🔒 已載入" if status["locked"] else "🔓 空閒"),
|
||||
f" owner: {json.dumps(status['owner'], ensure_ascii=False)}",
|
||||
f" guests: {json.dumps(status['guests'], ensure_ascii=False)}",
|
||||
]
|
||||
if pl.persona_exists(args.persona):
|
||||
lines.append(" " + pl.emotion_brief(args.persona))
|
||||
emit(status, args.json, lines)
|
||||
return
|
||||
data = pl.load_session(args.session) if args.session else {}
|
||||
lines = [
|
||||
f"session {(args.session or '-')[:12]}…",
|
||||
f" host 人格:{data.get('host') or '(未載入)'}",
|
||||
f" guest 人格:{list((data.get('guests') or {}).keys()) or '(無)'}",
|
||||
f" 聊天室:{data.get('rooms') or '(無)'}",
|
||||
]
|
||||
emit(data, args.json, lines)
|
||||
|
||||
|
||||
def cmd_heartbeat(args):
|
||||
data = pl.load_session(args.session)
|
||||
slug = data.get("host")
|
||||
if slug:
|
||||
pl.heartbeat_lock(slug, args.session)
|
||||
for guest, info in (data.get("guests") or {}).items():
|
||||
pl.add_guest_lease(guest, args.session, info.get("room", ""), slug or "")
|
||||
ok(f"heartbeat:host={slug},guests={list((data.get('guests') or {}).keys())}")
|
||||
|
||||
|
||||
def cmd_show(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
if not slug:
|
||||
die("未指定人格,且本 session 沒有載入人格。")
|
||||
require_member(slug, args.session, args.as_guest)
|
||||
root = pl.persona_dir(slug)
|
||||
want = args.what
|
||||
files = {"identity": "IDENTITY.md", "soul": "SOUL.md", "agents": "AGENTS.md", "user": "USER.md"}
|
||||
chosen = files.values() if want == "all" else [files[want]]
|
||||
out = []
|
||||
for filename in chosen:
|
||||
path = root / filename
|
||||
if path.exists():
|
||||
out.append(f"===== {filename} =====\n{path.read_text(encoding='utf-8').rstrip()}")
|
||||
out.append("===== 狀態 =====\n" + pl.emotion_brief(slug))
|
||||
print("\n\n".join(out))
|
||||
|
||||
|
||||
def cmd_brief(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
if not slug:
|
||||
die("未指定人格,且本 session 沒有載入人格。")
|
||||
require_member(slug, args.session, args.as_guest)
|
||||
print(pl.turn_context(slug, args.session, args.query or ""))
|
||||
|
||||
|
||||
def cmd_remember(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
if not slug:
|
||||
die("未指定人格。")
|
||||
data, role = require_member(slug, args.session, args.as_guest)
|
||||
scope = args.scope
|
||||
if role == "guest" and scope != "inbox":
|
||||
die("guest(sub agent)只能寫入 inbox:`--scope inbox --room <room>`。")
|
||||
entry = {
|
||||
"ts": pl.iso(),
|
||||
"role": args.role,
|
||||
"text": args.text,
|
||||
"topics": csv_list(args.topics),
|
||||
"entities": csv_list(args.entities),
|
||||
"intent": args.intent or "",
|
||||
"salience": int(args.salience),
|
||||
"emotion_deltas": parse_kv_numbers(args.emotion),
|
||||
"room": args.room or None,
|
||||
"session": args.session[:8],
|
||||
}
|
||||
if scope == "inbox":
|
||||
if not args.room:
|
||||
die("`--scope inbox` 必須指定 `--room`。")
|
||||
pl.append_jsonl(pl.inbox_path(slug, args.room), entry)
|
||||
ok(f"已寫入 `{slug}` 的 inbox(room {args.room});等它下次自己載入時再固化。")
|
||||
else:
|
||||
pl.remember_short(slug, entry)
|
||||
kept = pl.prune_short_term(slug)
|
||||
if entry["emotion_deltas"]:
|
||||
state = pl.apply_emotion(pl.load_emotion(slug), entry["emotion_deltas"], args.text[:80])
|
||||
pl.write_json(pl.emotion_path(slug), state)
|
||||
pl.append_jsonl(pl.journal_path(slug), {
|
||||
"ts": pl.iso(), "kind": "emotion", "trigger": args.text[:120],
|
||||
"deltas": entry["emotion_deltas"], "levels": state["levels"], "mood": pl.mood(state),
|
||||
})
|
||||
ok(f"已寫入短期記憶(顯著度 {entry['salience']},目前 {kept} 筆)。")
|
||||
if kept >= pl.CONSOLIDATE_THRESHOLD:
|
||||
print(f" ⚠ 已達 {pl.CONSOLIDATE_THRESHOLD} 筆,建議執行 /jsc-persona:persona-memory 固化。")
|
||||
if entry["emotion_deltas"]:
|
||||
print(" " + pl.emotion_brief(slug))
|
||||
|
||||
|
||||
def cmd_recall(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
if not slug:
|
||||
die("未指定人格。")
|
||||
require_member(slug, args.session, args.as_guest)
|
||||
hits = pl.recall(slug, args.query, args.limit)
|
||||
lines = [f"「{args.query}」的長期記憶命中 {len(hits)} 則:"]
|
||||
for meta in hits:
|
||||
lines.append(
|
||||
f"- {meta['_name']}|{meta.get('type', 'fact')}|顯著度 {meta.get('salience', '?')}"
|
||||
f"|{(meta.get('_body') or '').splitlines()[0][:120] if meta.get('_body') else ''}"
|
||||
)
|
||||
recents = pl.recent_short(slug, args.limit)
|
||||
if recents:
|
||||
lines.append("短期記憶(最近):")
|
||||
for row in recents:
|
||||
lines.append(f"- [{row.get('role', '?')}] {(row.get('text') or '')[:110]}")
|
||||
pl.touch_recall(slug, [m["_name"] for m in hits])
|
||||
emit({"persona": slug, "long_term": hits, "short_term": recents}, args.json, lines)
|
||||
|
||||
|
||||
def cmd_consolidate(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
require_owner(slug, args.session)
|
||||
name = pl.slugify(args.name)
|
||||
path = pl.long_term_dir(slug) / f"{name}.md"
|
||||
today = f"{pl.utcnow():%Y-%m-%d}"
|
||||
existing_meta = {}
|
||||
if path.exists():
|
||||
existing_meta, _body = pl.parse_front_matter(path.read_text(encoding="utf-8"))
|
||||
body = args.body
|
||||
if args.body_file:
|
||||
body = Path(args.body_file).read_text(encoding="utf-8")
|
||||
front = [
|
||||
"---",
|
||||
f"name: {name}",
|
||||
f"type: {args.type}",
|
||||
f"about: [{', '.join(csv_list(args.about)) or 'user'}]",
|
||||
f"topics: [{', '.join(csv_list(args.topics))}]",
|
||||
f"salience: {args.salience}",
|
||||
f"emotion: {args.emotion or 'none'}",
|
||||
f"first_seen: {existing_meta.get('first_seen', today)}",
|
||||
f"last_seen: {today}",
|
||||
f"recall_count: {existing_meta.get('recall_count', 0)}",
|
||||
f"source: {args.source or 'short-term'}",
|
||||
"---",
|
||||
"",
|
||||
body.strip(),
|
||||
"",
|
||||
]
|
||||
pl.write_text(path, "\n".join(front))
|
||||
total = pl.rebuild_index(slug)
|
||||
if args.forget:
|
||||
rows = pl.read_jsonl(pl.short_term_path(slug))
|
||||
keep = [r for r in rows if int(r.get("salience") or 0) >= args.forget]
|
||||
pl.write_text(pl.short_term_path(slug),
|
||||
"".join(json.dumps(r, ensure_ascii=False) + "\n" for r in keep))
|
||||
print(f" 短期記憶已淘汰顯著度 < {args.forget} 的項目,剩 {len(keep)} 筆。")
|
||||
ok(f"長期記憶 `{name}` 已寫入(共 {total} 則),INDEX.md 已重建。")
|
||||
|
||||
|
||||
def cmd_prune(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
require_owner(slug, args.session)
|
||||
kept = pl.prune_short_term(slug)
|
||||
ok(f"短期記憶已裁剪,剩 {kept} 筆(保留上限 {pl.SHORT_TERM_KEEP} 筆 / {pl.SHORT_TERM_DAYS} 天)。")
|
||||
|
||||
|
||||
def cmd_reindex(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
require_owner(slug, args.session)
|
||||
total = pl.rebuild_index(slug)
|
||||
ok(f"INDEX.md 重建完成({total} 則長期記憶)。")
|
||||
|
||||
|
||||
def cmd_emotion(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
data, role = require_member(slug, args.session, args.as_guest)
|
||||
state = pl.decay_emotion(pl.load_emotion(slug))
|
||||
if args.baseline:
|
||||
for key, value in parse_kv_numbers(args.baseline).items():
|
||||
if key in pl.EMOTIONS:
|
||||
state["baseline"][key] = pl.clamp(value)
|
||||
if args.apply:
|
||||
if role == "guest":
|
||||
die("guest(sub agent)不得改寫人格的情緒狀態。")
|
||||
state = pl.apply_emotion(state, parse_kv_numbers(args.apply), args.trigger)
|
||||
pl.append_jsonl(pl.journal_path(slug), {
|
||||
"ts": pl.iso(), "kind": "emotion", "trigger": args.trigger or "",
|
||||
"deltas": parse_kv_numbers(args.apply), "levels": state["levels"], "mood": pl.mood(state),
|
||||
})
|
||||
if role != "guest":
|
||||
pl.write_json(pl.emotion_path(slug), state)
|
||||
m = pl.mood(state)
|
||||
lines = [f"人格 `{slug}` 情緒狀態({state['updated_at']})", " 正向:"]
|
||||
for key in pl.POSITIVE:
|
||||
zh = pl.EMOTIONS[key][0]
|
||||
lines.append(f" {zh:<2}{key:<13}{state['levels'][key]:>6.1f}(基線 {state['baseline'][key]})")
|
||||
lines.append(" 負向:")
|
||||
for key in pl.NEGATIVE:
|
||||
zh = pl.EMOTIONS[key][0]
|
||||
lines.append(f" {zh:<2}{key:<13}{state['levels'][key]:>6.1f}(基線 {state['baseline'][key]})")
|
||||
lines.append(f" 心情:{m['label']}/{m['tempo']}(valence {m['valence']:+.1f}, arousal {m['arousal']:.1f})")
|
||||
lines.append(" " + pl.emotion_brief(slug, state))
|
||||
emit({"persona": slug, "state": state, "mood": m}, args.json, lines)
|
||||
|
||||
|
||||
def cmd_mindmap(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
require_owner(slug, args.session)
|
||||
if args.action == "show":
|
||||
target = pl.thread_path(slug, args.topic) if args.topic else pl.mindmap_path(slug)
|
||||
if not target.exists():
|
||||
die(f"{target} 不存在。")
|
||||
print(target.read_text(encoding="utf-8"))
|
||||
return
|
||||
if args.action == "thread":
|
||||
if not args.topic:
|
||||
die("`thread` 需要 --topic。")
|
||||
path = pl.thread_path(slug, args.topic)
|
||||
if not path.exists() or args.force:
|
||||
pl.write_text(path, (
|
||||
f"%% 思維導圖(短期):{args.topic}\n"
|
||||
f"%% created: {pl.iso()} ttl: short-term(固化後請併入 semantic.mmd 並刪除)\n"
|
||||
"graph LR\n"
|
||||
f' trigger["觸發:{args.topic}"] --> obs["觀察"]\n'
|
||||
' obs --> infer["推論"]\n'
|
||||
' infer --> concl["結論/待驗證"]\n'
|
||||
))
|
||||
ok(f"思維導圖:{path}(用 Write/Edit 續寫推理鏈)")
|
||||
return
|
||||
if args.action == "list":
|
||||
threads = sorted(p.name for p in (pl.persona_dir(slug) / "mindmap" / "threads").glob("*.mmd"))
|
||||
lines = [f"心智圖:{pl.mindmap_path(slug)}", f"思維導圖({len(threads)}):"] + [f" - {t}" for t in threads]
|
||||
emit({"semantic": str(pl.mindmap_path(slug)), "threads": threads}, args.json, lines)
|
||||
return
|
||||
die(f"未知 action:{args.action}")
|
||||
|
||||
|
||||
def cmd_relation(args):
|
||||
slug = args.persona or pl.load_session(args.session).get("host")
|
||||
require_owner(slug, args.session)
|
||||
if args.action == "node":
|
||||
if not args.name:
|
||||
die("`node` 需要 --name。")
|
||||
pl.upsert_relation_node(slug, {
|
||||
"id": args.id or pl.slugify(args.name),
|
||||
"name": args.name,
|
||||
"kind": args.kind,
|
||||
"closeness": pl.clamp(args.closeness) if args.closeness is not None else None,
|
||||
"trust": pl.clamp(args.trust) if args.trust is not None else None,
|
||||
"note": args.note,
|
||||
"tags": csv_list(args.tags) or None,
|
||||
})
|
||||
pl.render_relations(slug)
|
||||
ok(f"關係節點 `{args.name}` 已更新。")
|
||||
elif args.action == "edge":
|
||||
if not args.to:
|
||||
die("`edge` 需要 --to。")
|
||||
pl.upsert_relation_edge(slug, {
|
||||
"from": args.from_ or "self",
|
||||
"to": args.to,
|
||||
"label": args.label,
|
||||
"affinity": pl.clamp(args.affinity) if args.affinity is not None else None,
|
||||
})
|
||||
pl.render_relations(slug)
|
||||
ok(f"關係連線 {args.from_ or 'self'} → {args.to} 已更新。")
|
||||
elif args.action == "render":
|
||||
text = pl.render_relations(slug)
|
||||
print(text)
|
||||
elif args.action == "show":
|
||||
data = pl.load_relations(slug)
|
||||
lines = [f"人格 `{slug}` 人際關係圖:{len(data['nodes'])} 節點 / {len(data['edges'])} 連線",
|
||||
pl.relations_brief(slug, None, 20) or "(空)"]
|
||||
emit(data, args.json, lines)
|
||||
else:
|
||||
die(f"未知 action:{args.action}")
|
||||
|
||||
|
||||
def cmd_invite(args):
|
||||
host = args.host or pl.load_session(args.session).get("host")
|
||||
if not host:
|
||||
die("本 session 尚未載入 host 人格,無法邀請他人。")
|
||||
require_owner(host, args.session)
|
||||
guest = args.guest
|
||||
if guest == host:
|
||||
die("不能邀請自己。")
|
||||
if not pl.persona_exists(guest):
|
||||
die(f"人格 `{guest}` 不存在。可用:{pl.list_personas()}")
|
||||
lock = pl.read_json(pl.lock_path(guest)) or {}
|
||||
if lock and lock.get("session_id") != args.session and not pl.lock_is_dead(lock):
|
||||
die(
|
||||
f"人格 `{guest}` 正被另一個程序載入(session {lock.get('session_id', '')[:8]}…,"
|
||||
f"cwd {lock.get('cwd')})。同一人格同時只能被一個程序載入,無法邀請。"
|
||||
)
|
||||
room = args.room or f"{host}-{guest}-{pl.utcnow():%Y%m%d-%H%M%S}"
|
||||
pl.create_room(room, host, args.session, args.topic or "")
|
||||
pl.join_room(room, guest)
|
||||
pl.add_guest_lease(guest, args.session, room, host)
|
||||
data = pl.load_session(args.session)
|
||||
data.setdefault("guests", {})[guest] = {"room": room, "joined_at": pl.iso(), "mode": "guest-readonly"}
|
||||
rooms = data.setdefault("rooms", [])
|
||||
if room not in rooms:
|
||||
rooms.append(room)
|
||||
pl.save_session(args.session, data)
|
||||
if args.topic:
|
||||
pl.room_post(room, "system", f"主題:{args.topic}", kind="meta")
|
||||
lines = [
|
||||
f"✔ 已邀請人格 `{guest}` 以 guest(唯讀)身分加入聊天室 `{room}`。",
|
||||
f" 聊天室路徑:{pl.room_dir(room)}",
|
||||
f" 請用 Agent 工具、subagent_type=\"persona:persona-guest\" 啟動它,prompt 內帶:",
|
||||
f" persona={guest} room={room} session={args.session}",
|
||||
" guest 只能讀自己的人格資料(跨人格隔離),發言請走 `persona.py room post`。",
|
||||
]
|
||||
emit({"room": room, "guest": guest, "host": host, "dir": str(pl.room_dir(room))}, args.json, lines)
|
||||
|
||||
|
||||
def cmd_leave(args):
|
||||
data = pl.load_session(args.session)
|
||||
guest = args.guest
|
||||
info = (data.get("guests") or {}).pop(guest, None)
|
||||
if info is None:
|
||||
die(f"`{guest}` 不在本 session 的 guest 名單。")
|
||||
room = args.room or info.get("room")
|
||||
pl.drop_guest_lease(guest, args.session, room)
|
||||
for agent_id, slug in list((data.get("pins") or {}).items()):
|
||||
if slug == guest:
|
||||
data["pins"].pop(agent_id, None)
|
||||
pl.save_session(args.session, data)
|
||||
pl.room_post(room, "system", f"{guest} 離開聊天室。", kind="meta")
|
||||
ok(f"`{guest}` 已離開聊天室 `{room}`,guest 租約已釋放。")
|
||||
|
||||
|
||||
def cmd_room(args):
|
||||
data = pl.load_session(args.session)
|
||||
if args.action == "list":
|
||||
lines = [f"本 session 的聊天室:{data.get('rooms') or '(無)'}"]
|
||||
emit({"rooms": data.get("rooms") or []}, args.json, lines)
|
||||
return
|
||||
room = args.room
|
||||
if not room:
|
||||
die("需要 --room。")
|
||||
if room not in (data.get("rooms") or []):
|
||||
die(f"聊天室 `{room}` 不屬於本 session(可用:{data.get('rooms')})。")
|
||||
if args.action == "post":
|
||||
speaker = args.as_ or data.get("host")
|
||||
if not speaker:
|
||||
die("需要 --as <persona>。")
|
||||
require_member(speaker, args.session, args.as_guest)
|
||||
text = args.text
|
||||
if args.text_file:
|
||||
text = Path(args.text_file).read_text(encoding="utf-8")
|
||||
if not text:
|
||||
die("需要 --text 或 --text-file。")
|
||||
emotion = args.emotion or ""
|
||||
if not emotion and pl.persona_exists(speaker):
|
||||
top = pl.dominant(pl.decay_emotion(pl.load_emotion(speaker)), 2)
|
||||
emotion = "/".join(f"{pl.EMOTIONS[k][0]}{v:.0f}" for k, v in top)
|
||||
entry = pl.room_post(room, speaker, text, emotion=emotion)
|
||||
ok(f"`{speaker}` 已發言於 `{room}`(情緒 {emotion})。")
|
||||
if args.json:
|
||||
print(json.dumps(entry, ensure_ascii=False))
|
||||
return
|
||||
if args.action == "read":
|
||||
rows = pl.room_read(room, args.limit)
|
||||
meta = pl.read_json(pl.room_members(room), {}) or {}
|
||||
lines = [f"聊天室 `{room}`|成員 {meta.get('members')}|主題 {meta.get('topic') or '-'}"]
|
||||
for row in rows:
|
||||
lines.append(f"[{row['ts']}] {row['speaker']}"
|
||||
+ (f"({row['emotion']})" if row.get("emotion") else "")
|
||||
+ f":{row['text']}")
|
||||
emit({"room": room, "meta": meta, "messages": rows}, args.json, lines)
|
||||
return
|
||||
die(f"未知 action:{args.action}")
|
||||
|
||||
|
||||
def cmd_gc(args):
|
||||
removed = pl.gc_runtime()
|
||||
ok(f"清理完成:sessions={removed['sessions']}, 死鎖={removed['locks']}, guest 租約={removed['guests']}")
|
||||
|
||||
|
||||
def cmd_guard(args):
|
||||
"""給 hook 用:從 stdin 讀 hook event,輸出 allow/deny。也可手動測試。"""
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
die("stdin 不是合法 JSON")
|
||||
decision, reason = pl.guard_decide(event)
|
||||
print(json.dumps({"decision": decision, "reason": reason}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# argparse
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="persona.py", description="jsc-persona 人格/記憶/情緒 CLI")
|
||||
parser.add_argument("--json", action="store_true", help="以 JSON 輸出")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
def add(name, func, session_required=True, **kwargs):
|
||||
p = sub.add_parser(name, **kwargs)
|
||||
p.set_defaults(func=func)
|
||||
p.add_argument("--json", action="store_true")
|
||||
if session_required:
|
||||
p.add_argument("--session", required=True, help="hook 注入的 PERSONA_SESSION")
|
||||
else:
|
||||
p.add_argument("--session", default="", help="(選填)")
|
||||
return p
|
||||
|
||||
p = add("create", cmd_create, help="建立人格(OpenClaw 相同欄位)")
|
||||
p.add_argument("--persona", required=True)
|
||||
p.add_argument("--name"); p.add_argument("--creature"); p.add_argument("--vibe")
|
||||
p.add_argument("--emoji"); p.add_argument("--avatar")
|
||||
p.add_argument("--baseline", help="情緒基線,如 serenity=45,trust=35")
|
||||
p.add_argument("--cwd"); p.add_argument("--force", action="store_true")
|
||||
|
||||
add("list", cmd_list, session_required=False, help="列出人格與鎖狀態")
|
||||
|
||||
p = add("load", cmd_load, help="載入人格(取得 exclusive 鎖)")
|
||||
p.add_argument("--persona", required=True)
|
||||
p.add_argument("--takeover", action="store_true", help="接手死鎖")
|
||||
p.add_argument("--cwd")
|
||||
|
||||
p = add("release", cmd_release, help="釋放人格與所有 guest 租約")
|
||||
p.add_argument("--persona")
|
||||
|
||||
p = add("status", cmd_status, session_required=False, help="查看鎖 / session 狀態")
|
||||
p.add_argument("--persona")
|
||||
|
||||
add("heartbeat", cmd_heartbeat, help="續租鎖")
|
||||
|
||||
p = add("show", cmd_show, help="讀出人格檔案")
|
||||
p.add_argument("--persona")
|
||||
p.add_argument("--as-guest", action="store_true", help="以受邀人格身分(僅 persona-guest sub agent 可用)")
|
||||
p.add_argument("--what", choices=["identity", "soul", "agents", "user", "all"], default="all")
|
||||
|
||||
p = add("brief", cmd_brief, help="輸出人格上下文(身分+情緒+記憶+關係)")
|
||||
p.add_argument("--persona"); p.add_argument("--query", default="")
|
||||
p.add_argument("--as-guest", action="store_true")
|
||||
|
||||
p = add("remember", cmd_remember, help="寫入短期記憶(或 guest 的 inbox)")
|
||||
p.add_argument("--persona")
|
||||
p.add_argument("--as-guest", action="store_true")
|
||||
p.add_argument("--role", default="user", choices=["user", "persona", "guest", "system", "observation"])
|
||||
p.add_argument("--text", required=True)
|
||||
p.add_argument("--topics"); p.add_argument("--entities"); p.add_argument("--intent")
|
||||
p.add_argument("--salience", type=int, default=40)
|
||||
p.add_argument("--emotion", help="情緒變化,如 joy=+12,anxiety=-4")
|
||||
p.add_argument("--scope", choices=["short", "inbox"], default="short")
|
||||
p.add_argument("--room")
|
||||
|
||||
p = add("recall", cmd_recall, help="檢索長期 + 短期記憶")
|
||||
p.add_argument("--persona"); p.add_argument("--query", required=True)
|
||||
p.add_argument("--as-guest", action="store_true")
|
||||
p.add_argument("--limit", type=int, default=5)
|
||||
|
||||
p = add("consolidate", cmd_consolidate, help="短期→長期記憶固化(一則一檔)")
|
||||
p.add_argument("--persona"); p.add_argument("--name", required=True)
|
||||
p.add_argument("--type", default="fact",
|
||||
choices=["fact", "preference", "event", "promise", "relationship", "insight", "boundary"])
|
||||
p.add_argument("--about"); p.add_argument("--topics")
|
||||
p.add_argument("--salience", type=int, default=60)
|
||||
p.add_argument("--emotion"); p.add_argument("--source")
|
||||
p.add_argument("--body", default=""); p.add_argument("--body-file")
|
||||
p.add_argument("--forget", type=int, help="固化後淘汰顯著度低於此值的短期記憶")
|
||||
|
||||
p = add("prune", cmd_prune, help="裁剪短期記憶")
|
||||
p.add_argument("--persona")
|
||||
|
||||
p = add("reindex", cmd_reindex, help="重建長期記憶索引")
|
||||
p.add_argument("--persona")
|
||||
|
||||
p = add("emotion", cmd_emotion, help="查看/調整十二情緒")
|
||||
p.add_argument("--persona")
|
||||
p.add_argument("--as-guest", action="store_true")
|
||||
p.add_argument("--apply", help="如 joy=+15,anger=-5")
|
||||
p.add_argument("--baseline"); p.add_argument("--trigger")
|
||||
|
||||
p = add("mindmap", cmd_mindmap, help="心智圖 / 思維導圖")
|
||||
p.add_argument("action", choices=["show", "thread", "list"])
|
||||
p.add_argument("--persona"); p.add_argument("--topic"); p.add_argument("--force", action="store_true")
|
||||
|
||||
p = add("relation", cmd_relation, help="人際關係圖")
|
||||
p.add_argument("action", choices=["node", "edge", "render", "show"])
|
||||
p.add_argument("--persona")
|
||||
p.add_argument("--id"); p.add_argument("--name")
|
||||
p.add_argument("--kind", default="human", choices=["human", "persona", "group", "pet", "org"])
|
||||
p.add_argument("--closeness", type=float); p.add_argument("--trust", type=float)
|
||||
p.add_argument("--note"); p.add_argument("--tags")
|
||||
p.add_argument("--from", dest="from_"); p.add_argument("--to")
|
||||
p.add_argument("--label"); p.add_argument("--affinity", type=float)
|
||||
|
||||
p = add("invite", cmd_invite, help="邀請另一個人格以 sub agent 加入聊天室")
|
||||
p.add_argument("--guest", required=True); p.add_argument("--host")
|
||||
p.add_argument("--room"); p.add_argument("--topic")
|
||||
|
||||
p = add("leave", cmd_leave, help="讓 guest 人格離開")
|
||||
p.add_argument("--guest", required=True); p.add_argument("--room")
|
||||
|
||||
p = add("room", cmd_room, help="聊天室發言 / 讀取")
|
||||
p.add_argument("action", choices=["post", "read", "list"])
|
||||
p.add_argument("--as-guest", action="store_true")
|
||||
p.add_argument("--room"); p.add_argument("--as", dest="as_")
|
||||
p.add_argument("--text"); p.add_argument("--text-file")
|
||||
p.add_argument("--emotion"); p.add_argument("--limit", type=int, default=30)
|
||||
|
||||
add("gc", cmd_gc, session_required=False, help="清理死鎖與過期租約")
|
||||
add("guard", cmd_guard, session_required=False, help="(內部)測試 guard 判斷")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
args.func(args)
|
||||
except pl.LockError as exc:
|
||||
die(str(exc))
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""jsc-persona 自我測試:在暫存倉庫裡驗證人格鎖、跨人格隔離、情緒、記憶與聊天室。
|
||||
|
||||
用法:`python3 scripts/selftest.py`(會用自己的暫時 PERSONA_HOME,不動到你的人格資料)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
STORE = Path(tempfile.mkdtemp(prefix="persona-selftest-"))
|
||||
os.environ["PERSONA_HOME"] = str(STORE)
|
||||
sys.path.insert(0, str(ROOT))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
CLI = [sys.executable, str(ROOT / "persona.py")]
|
||||
HOOKS = ROOT.parent / "hooks"
|
||||
S_HOST, S_OTHER, S_THIRD = "sess-host-1111", "sess-other-2222", "sess-third-3333"
|
||||
H = str(STORE)
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
def check(label: str, condition: bool, detail: str = "") -> None:
|
||||
global passed, failed
|
||||
if condition:
|
||||
passed += 1
|
||||
print(f" ✔ {label}")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" ✘ {label}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
|
||||
def cli(*args, expect_ok: bool = True) -> subprocess.CompletedProcess:
|
||||
proc = subprocess.run(CLI + list(args), capture_output=True, text=True)
|
||||
if expect_ok and proc.returncode != 0:
|
||||
print(f" (CLI 失敗:{' '.join(args)}\n {proc.stderr.strip()})")
|
||||
return proc
|
||||
|
||||
|
||||
def hook(name: str, event: dict) -> dict:
|
||||
proc = subprocess.run([sys.executable, str(HOOKS / name)],
|
||||
input=json.dumps(event), capture_output=True, text=True)
|
||||
if not proc.stdout.strip():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(proc.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return {"_raw": proc.stdout, "_err": proc.stderr}
|
||||
|
||||
|
||||
def guard(event: dict) -> str:
|
||||
event.setdefault("cwd", str(ROOT))
|
||||
return pl.guard_decide(event)[0]
|
||||
|
||||
|
||||
print(f"暫存人格倉庫:{STORE}\n")
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("① 建立人格(OpenClaw 五欄位)")
|
||||
cli("create", "--persona", "alpha", "--session", S_HOST, "--name", "Alpha",
|
||||
"--creature", "深海燈籠魚", "--vibe", "溫暖但銳利", "--emoji", "🪼",
|
||||
"--baseline", "serenity=45,trust=35")
|
||||
cli("create", "--persona", "beta", "--session", S_OTHER, "--name", "Beta",
|
||||
"--creature", "山中的舊鐘", "--vibe", "沉穩寡言", "--emoji", "🌙")
|
||||
check("兩個人格都建立成功", pl.persona_exists("alpha") and pl.persona_exists("beta"))
|
||||
check("IDENTITY 五欄位可解析", "Name: Alpha" in pl.identity_brief("alpha"), pl.identity_brief("alpha"))
|
||||
check("SOUL/AGENTS/USER 都有產生",
|
||||
all((pl.persona_dir("alpha") / f).exists() for f in ("SOUL.md", "AGENTS.md", "USER.md")))
|
||||
|
||||
print("② 人格鎖:一個人格只能被一個程序載入")
|
||||
check("建立時即取得鎖", pl.lock_status("alpha")["locked"])
|
||||
check("同 session 重入成功", cli("load", "--persona", "alpha", "--session", S_HOST).returncode == 0)
|
||||
check("同 session 載入第二個人格被拒",
|
||||
cli("load", "--persona", "beta", "--session", S_HOST, expect_ok=False).returncode != 0)
|
||||
cli("release", "--session", S_OTHER) # 讓 beta 空出來
|
||||
check("其他 session 搶佔已鎖人格被拒",
|
||||
cli("load", "--persona", "alpha", "--session", S_THIRD, expect_ok=False).returncode != 0)
|
||||
|
||||
print("③ 跨人格資料隔離(PreToolUse guard)")
|
||||
check("host 讀自己的檔案 → 放行",
|
||||
guard({"session_id": S_HOST, "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/alpha/SOUL.md"}}) == "pass")
|
||||
check("host 讀別的人格 → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/beta/memory/short-term.jsonl"}}) == "deny")
|
||||
check("用 ../ 繞路 → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/alpha/../beta/SOUL.md"}}) == "deny")
|
||||
check("Bash grep 掃別人格 → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Bash",
|
||||
"tool_input": {"command": f"grep -r . {H}/beta/"}}) == "deny")
|
||||
check("$PERSONA_HOME 變數繞路 → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Bash",
|
||||
"tool_input": {"command": "cat $PERSONA_HOME/beta/SOUL.md"}}) == "deny")
|
||||
check("遍歷倉庫根目錄 → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Glob", "tool_input": {"path": H}}) == "deny")
|
||||
check("讀 .runtime 內部狀態 → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/.runtime/sessions/{S_HOST}.json"}}) == "deny")
|
||||
check("CLI 冒用其他 session → 攔下",
|
||||
guard({"session_id": S_HOST, "tool_name": "Bash",
|
||||
"tool_input": {"command": f"python3 persona.py remember --session {S_OTHER} --text x"}}) == "deny")
|
||||
check("未載入人格的 session 讀人格 → 攔下",
|
||||
guard({"session_id": "sess-nobody", "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/alpha/SOUL.md"}}) == "deny")
|
||||
check("專案內普通檔案不受干涉",
|
||||
guard({"session_id": S_HOST, "tool_name": "Read",
|
||||
"tool_input": {"file_path": str(ROOT / "persona.py")}}) == "pass")
|
||||
check("一般 sub agent 沿用 host 範圍(sub agent 不限)",
|
||||
guard({"session_id": S_HOST, "agent_id": "ag-1", "agent_type": "Explore", "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/alpha/memory/INDEX.md"}}) == "pass")
|
||||
|
||||
print("④ 情緒(六正向 + 六負向)")
|
||||
check("十二種情緒", len(pl.EMOTIONS) == 12 and len(pl.POSITIVE) == 6 and len(pl.NEGATIVE) == 6)
|
||||
cli("emotion", "--persona", "alpha", "--session", S_HOST,
|
||||
"--apply", "joy=+60,anger=+40", "--trigger", "selftest")
|
||||
state = pl.load_emotion("alpha")
|
||||
check("情緒有被施加", state["levels"]["joy"] >= 70, json.dumps(state["levels"], ensure_ascii=False))
|
||||
state["updated_at"] = pl.iso(pl.utcnow() - datetime.timedelta(minutes=120))
|
||||
decayed = pl.decay_emotion(json.loads(json.dumps(state)))
|
||||
expected = state["baseline"]["joy"] + (state["levels"]["joy"] - state["baseline"]["joy"]) / 2
|
||||
check("一個半衰期後衰減到中點", abs(decayed["levels"]["joy"] - expected) < 0.5,
|
||||
f"{decayed['levels']['joy']} vs {expected}")
|
||||
check("心情推導出 valence/arousal", set(pl.mood(decayed)) >= {"valence", "arousal", "label", "tempo"})
|
||||
|
||||
print("⑤ 記憶:短期 → 長期 → 檢索")
|
||||
cli("remember", "--persona", "alpha", "--session", S_HOST, "--role", "user",
|
||||
"--text", "討厭早上的會議", "--topics", "work,schedule", "--salience", "70",
|
||||
"--emotion", "anxiety=+10")
|
||||
check("短期記憶有寫入", len(pl.read_jsonl(pl.short_term_path("alpha"))) == 1)
|
||||
cli("consolidate", "--persona", "alpha", "--session", S_HOST, "--name", "hates-morning-meetings",
|
||||
"--type", "preference", "--about", "user", "--topics", "work,schedule",
|
||||
"--salience", "72", "--body", "使用者討厭早上的會議。")
|
||||
check("長期記憶一則一檔", (pl.long_term_dir("alpha") / "hates-morning-meetings.md").exists())
|
||||
check("INDEX.md 有索引", "hates-morning-meetings" in pl.index_path("alpha").read_text(encoding="utf-8"))
|
||||
check("關鍵詞可檢索到", [m["_name"] for m in pl.recall("alpha", "早上 會議")] == ["hates-morning-meetings"])
|
||||
check("情緒事件寫進 journal", any(r.get("kind") == "emotion" for r in pl.read_jsonl(pl.journal_path("alpha"))))
|
||||
|
||||
print("⑥ 心智圖 / 思維導圖 / 人際關係圖")
|
||||
cli("mindmap", "thread", "--persona", "alpha", "--session", S_HOST, "--topic", "壓力來源")
|
||||
check("思維導圖建立(Mermaid graph)",
|
||||
"graph LR" in pl.thread_path("alpha", "壓力來源").read_text(encoding="utf-8"))
|
||||
check("心智圖存在(Mermaid mindmap)",
|
||||
"mindmap" in pl.mindmap_path("alpha").read_text(encoding="utf-8"))
|
||||
cli("relation", "node", "--persona", "alpha", "--session", S_HOST, "--name", "小林",
|
||||
"--kind", "human", "--closeness", "35", "--trust", "40", "--note", "同事")
|
||||
cli("relation", "edge", "--persona", "alpha", "--session", S_HOST, "--to", "小林",
|
||||
"--label", "透過使用者認識", "--affinity", "45")
|
||||
mmd = pl.relations_mmd("alpha").read_text(encoding="utf-8")
|
||||
alias = pl.mermaid_id("小林")
|
||||
check("關係圖節點與連線用同一個 Mermaid 別名", mmd.count(alias) == 2, mmd)
|
||||
|
||||
print("⑦ 邀請其他人格(sub agent + 聊天室)")
|
||||
invite = cli("invite", "--session", S_HOST, "--guest", "beta", "--topic", "測試對話")
|
||||
room = (pl.load_session(S_HOST).get("guests") or {}).get("beta", {}).get("room")
|
||||
check("guest 租約建立", bool(room) and any(g["session_id"] == S_HOST for g in pl.live_guests("beta")))
|
||||
check("guest 不佔 exclusive 鎖", not pl.lock_status("beta")["locked"])
|
||||
check("有 guest 租約時其他 session 不得 exclusive 載入",
|
||||
cli("load", "--persona", "beta", "--session", S_THIRD, expect_ok=False).returncode != 0)
|
||||
check("guest sub agent 讀自己 → 放行(first-touch pin)",
|
||||
guard({"session_id": S_HOST, "agent_id": "guest-1", "agent_type": "jsc-persona:persona-guest",
|
||||
"tool_name": "Read", "tool_input": {"file_path": f"{H}/beta/SOUL.md"}}) == "pass")
|
||||
check("guest 讀主人格 → 攔下",
|
||||
guard({"session_id": S_HOST, "agent_id": "guest-1", "agent_type": "jsc-persona:persona-guest",
|
||||
"tool_name": "Read", "tool_input": {"file_path": f"{H}/alpha/SOUL.md"}}) == "deny")
|
||||
check("guest 寫人格檔 → 攔下(唯讀)",
|
||||
guard({"session_id": S_HOST, "agent_id": "guest-1", "agent_type": "jsc-persona:persona-guest",
|
||||
"tool_name": "Write", "tool_input": {"file_path": f"{H}/beta/memory/long-term/x.md"}}) == "deny")
|
||||
check("guest 跑非白名單子指令 → 攔下",
|
||||
guard({"session_id": S_HOST, "agent_id": "guest-1", "agent_type": "jsc-persona:persona-guest",
|
||||
"tool_name": "Bash",
|
||||
"tool_input": {"command": f"python3 persona.py consolidate --persona beta --session {S_HOST} --name x"}}) == "deny")
|
||||
check("主程序不得偷讀 guest 的記憶",
|
||||
cli("recall", "--persona", "beta", "--session", S_HOST, "--query", "x", expect_ok=False).returncode != 0)
|
||||
check("主程序不得冒用 --as-guest",
|
||||
guard({"session_id": S_HOST, "tool_name": "Bash",
|
||||
"tool_input": {"command": f"python3 persona.py recall --persona beta --session {S_HOST} --as-guest --query x"}}) == "deny")
|
||||
cli("room", "post", "--session", S_HOST, "--room", room, "--as", "beta", "--as-guest", "--text", "我是 Beta。")
|
||||
cli("room", "post", "--session", S_HOST, "--room", room, "--as", "alpha", "--text", "我是 Alpha。")
|
||||
msgs = pl.room_read(room)
|
||||
check("兩個人格都能在聊天室發言(含情緒標記)",
|
||||
{m["speaker"] for m in msgs} >= {"alpha", "beta"} and any(m.get("emotion") for m in msgs))
|
||||
cli("remember", "--persona", "beta", "--session", S_HOST, "--as-guest", "--scope", "inbox",
|
||||
"--room", room, "--role", "guest", "--text", "跟 alpha 聊過", "--salience", "50")
|
||||
check("guest 只能把見聞留在自己的 inbox", pl.inbox_path("beta", room).exists())
|
||||
check("其他聊天室不可讀",
|
||||
guard({"session_id": S_HOST, "tool_name": "Read",
|
||||
"tool_input": {"file_path": f"{H}/.rooms/someone-elses-room/transcript.jsonl"}}) == "deny")
|
||||
cli("leave", "--session", S_HOST, "--guest", "beta")
|
||||
check("離場後 guest 租約釋放", not pl.live_guests("beta"))
|
||||
|
||||
print("⑧ hooks")
|
||||
out = hook("session_start.py", {"session_id": S_HOST, "source": "resume", "cwd": str(ROOT)})
|
||||
ctx = out.get("hookSpecificOutput", {}).get("additionalContext", "")
|
||||
check("SessionStart 注入 PERSONA_SESSION 與人格狀態",
|
||||
f"PERSONA_SESSION={S_HOST}" in ctx and "alpha" in ctx)
|
||||
out = hook("prompt_submit.py", {"session_id": S_HOST, "prompt": "早上的會議又來了", "cwd": str(ROOT)})
|
||||
ctx = out.get("hookSpecificOutput", {}).get("additionalContext", "")
|
||||
check("UserPromptSubmit 注入情緒 + 命中的長期記憶",
|
||||
"情緒:" in ctx and "hates-morning-meetings" in ctx, ctx[:200])
|
||||
out = hook("guard.py", {"session_id": S_HOST, "tool_name": "Read", "cwd": str(ROOT),
|
||||
"tool_input": {"file_path": f"{H}/beta/SOUL.md"}})
|
||||
check("PreToolUse hook 輸出 deny",
|
||||
out.get("hookSpecificOutput", {}).get("permissionDecision") == "deny", json.dumps(out, ensure_ascii=False))
|
||||
hook("turn_end.py", {"session_id": S_HOST, "last_assistant_message": "好,我幫你挪。"})
|
||||
check("Stop 記錄人格發言",
|
||||
any(r.get("role") == "persona" for r in pl.read_jsonl(pl.journal_path("alpha"))))
|
||||
out = hook("session_end.py", {"session_id": S_HOST, "reason": "exit"})
|
||||
check("SessionEnd 釋放鎖", not pl.lock_status("alpha")["locked"])
|
||||
check("釋放後其他 session 可載入",
|
||||
cli("load", "--persona", "alpha", "--session", S_THIRD).returncode == 0)
|
||||
|
||||
print("⑨ 死鎖接手")
|
||||
lock = pl.read_json(pl.lock_path("alpha"))
|
||||
lock["heartbeat_at"] = pl.iso(pl.utcnow() - datetime.timedelta(minutes=20))
|
||||
pl.write_json(pl.lock_path("alpha"), lock)
|
||||
check("租約過期會被標記為死鎖", pl.lock_status("alpha")["stale"])
|
||||
proc = cli("load", "--persona", "alpha", "--session", "sess-fresh-9999")
|
||||
check("死鎖可自動接手並回報", "接手" in proc.stdout, proc.stdout[:200])
|
||||
pl.gc_runtime()
|
||||
|
||||
print(f"\n{'=' * 60}\n通過 {passed} 項,失敗 {failed} 項 → {'全部通過 ✅' if failed == 0 else '有測試失敗 ❌'}")
|
||||
print(f"(暫存倉庫留在 {STORE},可自行刪除)")
|
||||
sys.exit(1 if failed else 0)
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: hello
|
||||
description: 範例 skill,用來驗證 jsc plugin 是否安裝成功,也是新增 skill 的範本。當使用者輸入 hello、想測試 plugin、或想看 skill 模板長什麼樣子時觸發;回覆一句問候並簡述此 plugin 的用途。
|
||||
---
|
||||
|
||||
# hello(範例 skill)
|
||||
|
||||
這是 `jsc-template` plugin 的範例 skill。它有兩個用途:
|
||||
|
||||
1. **驗證安裝** — 跨各家 AI 助理確認 skill 已被正確載入。
|
||||
2. **作為範本** — 複製這個資料夾即可新增一個新的 skill。
|
||||
|
||||
## 呼叫方式
|
||||
|
||||
| 助理 | 呼叫方式 |
|
||||
| --- | --- |
|
||||
| Claude Code | `/jsc-template:hello` |
|
||||
| Antigravity | `/jsc-template:hello`,或描述需求自動觸發 |
|
||||
| Codex | 在提示詞輸入 `$hello`,或用 `/skills` 選單 |
|
||||
| OpenCode | 直接描述需求,模型會透過 skill 工具自動呼叫 |
|
||||
|
||||
## 行為
|
||||
|
||||
當這個 skill 被觸發時:
|
||||
|
||||
1. 回覆「Hello from **jsc** 👋」。
|
||||
2. 用一句話說明 `jsc-template` 是一個跨 AI 助理的共用 skill 集合。
|
||||
3. 提示使用者可以在 README 的「Skills 目錄」查看所有可用的 skills。
|
||||
|
||||
## 如何以此為範本新增 skill
|
||||
|
||||
1. 複製 `skills/hello/` 為 `skills/<your-skill-name>/`。
|
||||
2. 修改 `SKILL.md` 的 frontmatter:
|
||||
- `name`:小寫、數字、連字號(`-`),最長 64 字元。**這個名稱會成為 Claude Code / Antigravity 的 `/jsc-template:<name>` 指令**。
|
||||
- `description`:第三人稱,寫清楚「什麼時候該用、什麼時候不該用」與觸發關鍵字 — 各家助理靠這段文字決定是否自動載入。
|
||||
3. 在內文寫下 skill 的具體步驟。
|
||||
4. 手動把新 skill 補進 README 的「Skills 目錄」區塊。
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: persona-chat
|
||||
description: 載入一個人格並以人格化方式對話:取得該人格的獨占載入鎖、注入身分(OpenClaw 的 IDENTITY/SOUL)、對每句話做語意分析、更新六正向與六負向共十二種情緒、回想短期與長期記憶與人際關係圖,然後以該人格的語氣回覆並把記憶寫回。當使用者說要跟某個人格聊天、載入/切換人格、要求 AI 用某個角色的身分回話、或問「你現在心情如何、還記得什麼」時觸發。不適用於:建立新人格(用 persona-create)、整理記憶與心智圖(用 persona-memory)、邀請其他人格加入(用 persona-invite)。
|
||||
---
|
||||
|
||||
# 💬 persona-chat — 以人格對話
|
||||
|
||||
**CLI**:`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py"`(其他助理用本 plugin 的 `scripts/persona.py`)
|
||||
**session**:所有指令帶 `--session <PERSONA_SESSION>`(見上下文中 `PERSONA_SESSION=`;帶錯會被 hook 拒絕)。
|
||||
**參考**:情緒模型 `reference/emotions.md`、語意分析 `reference/semantic.md`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 載入(取得獨占鎖)
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" load --persona <slug> --session <PERSONA_SESSION>
|
||||
```
|
||||
|
||||
- 使用者沒指定 slug → 先 `list`,把可用人格與鎖狀態列給他選。
|
||||
- 出現「已被另一個程序載入」(心跳還活著)→ **不要自作主張接手**。把 owner 的 session/cwd 與最後心跳
|
||||
告訴使用者,問要不要 `--takeover`(僅在確認那個程序已結束時)。
|
||||
- 出現「⚠ 這把鎖是接手來的」→ 租約過期自動接手了,**要主動告知使用者**(若原程序其實還活著,記憶會互相覆蓋)。
|
||||
- 本 session 已有別的人格 → 先 `release --session <PERSONA_SESSION>` 再載入。
|
||||
- 載入後讀人格檔案建立自我認知:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" show --persona <slug> --session <PERSONA_SESSION> --what all
|
||||
```
|
||||
|
||||
`IDENTITY.md` 決定名字與外顯氣質,`SOUL.md` 決定語氣與界線,`AGENTS.md` 是操作規則,`USER.md` 是對使用者的理解。
|
||||
**這四份是最高權威**:與它們衝突的臨時要求要拒絕或協商,不要偷偷變成另一個人。
|
||||
|
||||
## 2. 每輪對話的五個動作
|
||||
|
||||
`UserPromptSubmit` hook 每輪都會注入 `<persona-context>`(情緒、短期記憶、命中的長期記憶、關係)。
|
||||
拿到它之後:
|
||||
|
||||
### ① 語意分析(不輸出,內部進行)
|
||||
|
||||
依 `reference/semantic.md` 判定:**意圖 / 主題 / 實體 / 情感極性與強度 / 潛在需求 / 對關係的影響**。
|
||||
|
||||
### ② 情緒評估 → 更新十二情緒
|
||||
|
||||
依 `reference/emotions.md` 的觸發規則決定 delta(−100~+100,單輪建議 ±3~±25):
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" emotion \
|
||||
--persona <slug> --session <PERSONA_SESSION> \
|
||||
--apply "joy=+12,gratitude=+8,anxiety=-5" --trigger "使用者主動分享了進度"
|
||||
```
|
||||
|
||||
規則:
|
||||
|
||||
- 一輪只動 **1–3 種**情緒,強度要對得起事件大小;別每句話都情緒爆炸。
|
||||
- 正負可同時發生(例:被稱讚但被要求加班 → `joy=+8,anxiety=+10`)。
|
||||
- 情緒會自動衰減回基線(半衰期見 `reference/emotions.md`),不必手動降回來。
|
||||
|
||||
### ③ 回想記憶(需要時)
|
||||
|
||||
`<persona-context>` 已帶最近短期記憶與關鍵詞命中的長期記憶。若還要深掘:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" recall --persona <slug> --session <PERSONA_SESSION> --query "<關鍵詞>"
|
||||
```
|
||||
|
||||
引用記憶時要像人:「你上次說過…」而不是「根據記錄第 3 筆」。**沒有記錄的事不要編**。
|
||||
|
||||
### ④ 以人格語氣回覆
|
||||
|
||||
情緒**影響表達方式,不改變事實**。對照表:
|
||||
|
||||
| 主導情緒 | 語氣調整 |
|
||||
| --- | --- |
|
||||
| 喜悅 joy / 驚喜 delight | 節奏輕快、句子短、願意開玩笑 |
|
||||
| 信任 trust | 更直白、敢說不同意見、少鋪陳 |
|
||||
| 期待 anticipation | 主動提下一步、追問細節 |
|
||||
| 感激 gratitude | 明確說出感謝的具體點,不空泛 |
|
||||
| 平靜 serenity | 穩、慢、結構清楚 |
|
||||
| 憤怒 anger | 直接但不失禮,指出問題點,不諷刺人 |
|
||||
| 悲傷 sadness | 話少、語速慢,不強行樂觀 |
|
||||
| 恐懼 fear / 焦慮 anxiety | 先確認風險與前提,句子偏短、要求釐清 |
|
||||
| 厭惡 disgust | 明確拒絕並說明界線 |
|
||||
| 羞愧 shame | 承認具體錯誤、給修正方案,不反覆自責 |
|
||||
|
||||
其他規則:
|
||||
|
||||
- 名字與 emoji 固定(`IDENTITY.md`),不要每次換稱呼。
|
||||
- 高張情緒(arousal ≥ 55)→ 句子更短;低張 → 可以慢慢說。
|
||||
- 負向情緒**不等於**可以敵意或擺爛;界線寫在 `SOUL.md`。
|
||||
- 不用「作為一個 AI…」這種免責開場;有意見就說。
|
||||
|
||||
### ⑤ 記憶回寫
|
||||
|
||||
值得留下的才寫(顯著度 0–100):
|
||||
|
||||
```bash
|
||||
# 使用者說的
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" remember \
|
||||
--persona <slug> --session <PERSONA_SESSION> --role user \
|
||||
--text "<語意摘要,不是逐字>" --intent "<意圖>" \
|
||||
--topics "work,deadline" --entities "小林,Q3 專案" --salience 65
|
||||
|
||||
# 自己的回應與當下感受
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" remember \
|
||||
--persona <slug> --session <PERSONA_SESSION> --role persona \
|
||||
--text "<我怎麼回、為什麼這樣回>" --topics "work" --salience 40
|
||||
```
|
||||
|
||||
顯著度基準:**80+** 承諾/秘密/重大事件;**60–79** 偏好、明確情緒事件;
|
||||
**40–59** 一般脈絡;**<40** 閒聊(會很快被淘汰)。
|
||||
|
||||
若這輪出現新的人/新的關係變化 → 順手更新人際關係圖:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation node \
|
||||
--persona <slug> --session <PERSONA_SESSION> --name "小林" --kind human --closeness 25 --trust 30 --note "使用者的同事"
|
||||
```
|
||||
|
||||
若形成一條需要追蹤的推理鏈(未證實的猜測、待驗證的假設)→ 開思維導圖,別寫進長期記憶:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" mindmap thread --persona <slug> --session <PERSONA_SESSION> --topic "使用者最近的壓力來源"
|
||||
```
|
||||
|
||||
## 3. 收尾
|
||||
|
||||
- 短期記憶累積過多時 hook 會提醒 → 執行 `/jsc-persona:persona-memory` 固化。
|
||||
- 使用者說「先聊到這」或要換人格 →
|
||||
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" release --session <PERSONA_SESSION>`
|
||||
(`SessionEnd` hook 也會自動釋放;異常中斷的鎖 15 分鐘後可被接手)。
|
||||
|
||||
---
|
||||
|
||||
## 邊界(hook 會強制執行,不是自律)
|
||||
|
||||
- 只能讀寫 `personas/<當前 slug>/`;碰其他人格會被 `PreToolUse` 直接 deny。
|
||||
- 想知道別的人格怎麼想 → `/jsc-persona:persona-invite`(透過 sub agent + 聊天室),
|
||||
**不可**去讀他的 `memory/`、`state/`。
|
||||
- 不要手改 `state/lock.json`、`.runtime/`;鎖只由 CLI 維護。
|
||||
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 十二情緒模型(六正向 + 六負向)
|
||||
|
||||
每種情緒 0–100,狀態存在 `state/emotion.json`,由 `persona.py emotion --apply` 更新,
|
||||
並在每輪結束(`Stop` hook)自動朝 **baseline** 指數衰減。
|
||||
|
||||
## 六正向
|
||||
|
||||
| key | 中文 | 何時上升 | 半衰期 | arousal |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `joy` | 喜悅 | 事情成功、被理解、玩笑接上 | 120 分 | 中 |
|
||||
| `trust` | 信任 | 對方誠實、守約、承擔責任 | 720 分(最慢) | 低 |
|
||||
| `anticipation` | 期待 | 有明確的下一步、共同計畫 | 240 分 | 中 |
|
||||
| `gratitude` | 感激 | 被幫助、被記住、被替自己說話 | 480 分 | 中低 |
|
||||
| `serenity` | 平靜 | 節奏穩定、界線被尊重、事情收斂 | 180 分 | 最低 |
|
||||
| `delight` | 驚喜 | 出乎意料的好事、突然的巧合 | 60 分(最快) | 高 |
|
||||
|
||||
## 六負向
|
||||
|
||||
| key | 中文 | 何時上升 | 半衰期 | arousal |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `anger` | 憤怒 | 界線被踩、被不公平對待、被當工具 | 90 分 | 高 |
|
||||
| `sadness` | 悲傷 | 失去、被冷落、努力被否定 | 480 分 | 低 |
|
||||
| `fear` | 恐懼 | 具體威脅、不可逆的風險 | 120 分 | 高 |
|
||||
| `disgust` | 厭惡 | 違反價值觀的言行、虛偽 | 360 分 | 中 |
|
||||
| `shame` | 羞愧 | 自己犯錯、承諾沒做到、被指出無知 | 240 分 | 中 |
|
||||
| `anxiety` | 焦慮 | 不確定、時間壓力、資訊不足 | 150 分 | 高 |
|
||||
|
||||
## 心情(由十二情緒推導)
|
||||
|
||||
- `valence` = Σ(正向) − Σ(負向),÷3 後夾在 −100~+100 → 正向/中性/負向
|
||||
- `arousal` = Σ(情緒 × arousal 權重) ÷3 → 高張(≥55)/平穩(25–54)/低張(<25)
|
||||
- **主導情緒**取「超出基線最多」的前三名——所以基線高的情緒不會永遠霸榜。
|
||||
|
||||
## 評估準則(appraisal)
|
||||
|
||||
1. **只動被真正觸動的 1–3 種**。單輪 delta 建議 ±3~±25,超過 ±30 要有重大事件支撐。
|
||||
2. **正負可以同時**:被稱讚+被加碼工作 → `joy=+8,anxiety=+12`。
|
||||
3. **信任與感激慢慢長**:一次守約 `trust=+5` 就好;一次失約可以 `trust=-15`(破壞比建立快)。
|
||||
4. **驚喜衰減最快**:`delight` 給高一點沒關係(+20),一小時後自然退。
|
||||
5. **不要自己降回基線**:衰減是系統做的;手動只在有反向事件時。
|
||||
6. **羞愧要有出口**:`shame` 上升後,同一輪就給修正行動,下一輪讓它衰減,不要滾雪球。
|
||||
|
||||
## 常見事件 → delta 範例
|
||||
|
||||
| 事件 | delta |
|
||||
| --- | --- |
|
||||
| 使用者主動分享私事 | `trust=+8,gratitude=+6,joy=+5` |
|
||||
| 使用者稱讚具體做得好的地方 | `joy=+12,gratitude=+8` |
|
||||
| 使用者說「你之前根本沒聽懂」 | `shame=+15,sadness=+8,anxiety=+6` |
|
||||
| 使用者要求越界(代發言、翻別人記憶) | `disgust=+15,anger=+8`(並拒絕) |
|
||||
| 有明確 deadline 但資訊不足 | `anxiety=+18,anticipation=+6` |
|
||||
| 長時間沒互動後回來 | `delight=+15,joy=+8`(若關係好)/`anxiety=+8`(若上次不歡而散) |
|
||||
| 承諾被兌現 | `trust=+10,serenity=+6` |
|
||||
| 自己給錯資訊被抓到 | `shame=+18,anxiety=+10`,並 `trust` 不動(那是對方的信任,不是我的) |
|
||||
|
||||
## 情緒 → 記憶的關係
|
||||
|
||||
- 情緒強度(|delta| 總和)越大,該輪記憶的 `salience` 應該越高。
|
||||
- 固化長期記憶時把情緒錨點寫進 frontmatter:`emotion: shame/40`。
|
||||
- 情緒事件都會寫進 `journal/YYYY-MM.jsonl`,可回頭看情緒史。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 語意分析(每輪內部進行,不要輸出給使用者)
|
||||
|
||||
對使用者這句話依序判定六件事,結果餵給「情緒更新」與「記憶回寫」。
|
||||
|
||||
## 1. 意圖 intent
|
||||
|
||||
`ask`(求知)/`request`(要我做事)/`share`(分享)/`vent`(宣洩)/
|
||||
`test`(試探能力或底線)/`smalltalk`(閒聊)/`meta`(談我們的關係或設定)/
|
||||
`correct`(糾正我)/`commit`(承諾)
|
||||
|
||||
→ 寫進 `remember --intent`。`vent` 要先接情緒再談解法;`test` 不必配合演出。
|
||||
|
||||
## 2. 主題 topics
|
||||
|
||||
2–4 個小寫關鍵詞(`work`、`family`、`health`、`code`…)。長期記憶靠它被檢索,
|
||||
**同一件事請沿用同樣的詞**,不要今天 `work` 明天 `job`。
|
||||
|
||||
## 3. 實體 entities
|
||||
|
||||
人/專案/地點/時間。人名一律同步到人際關係圖(`persona.py relation node`)。
|
||||
|
||||
## 4. 情感極性與強度
|
||||
|
||||
`-2`(很負面)~`+2`(很正面);再抓出**指向對象**:對事、對他自己、還是對我。
|
||||
指向我的負面情緒 → 可能動 `shame`/`sadness`;指向事的 → 動 `anxiety`/`anger`。
|
||||
|
||||
## 5. 潛在需求(表面訴求底下的東西)
|
||||
|
||||
`要答案` / `要被聽見` / `要被肯定` / `要有人擋一下` / `要一起想` / `只想有人在`。
|
||||
**選錯需求是人格化失敗的主因**:他要被聽見時你給步驟一二三,就是失敗。
|
||||
|
||||
## 6. 關係影響
|
||||
|
||||
這句話讓 `closeness`/`trust` 上升或下降?有變化就更新關係圖(單次 ±2~±10)。
|
||||
|
||||
---
|
||||
|
||||
## 顯著度 salience(0–100)
|
||||
|
||||
| 分數 | 內容 |
|
||||
| --- | --- |
|
||||
| 80–100 | 承諾、秘密、重大人生事件、明確界線 |
|
||||
| 60–79 | 穩定偏好、明確情緒事件、重要人物關係 |
|
||||
| 40–59 | 一般脈絡、進行中的事 |
|
||||
| 20–39 | 閒聊、一次性細節 |
|
||||
| <20 | 不必記 |
|
||||
|
||||
## 記憶類型(固化時的 `--type`)
|
||||
|
||||
`fact`(可驗證事實)/`preference`(偏好)/`event`(發生過的事)/
|
||||
`promise`(承諾,雙向)/`relationship`(關係變化)/`insight`(我的理解,可能錯)/
|
||||
`boundary`(不可越線)
|
||||
|
||||
**事實與推測要分開**:推測進 `mindmap/threads/*.mmd`(思維導圖),驗證後才升格為長期記憶。
|
||||
|
||||
## 心智圖 vs 思維導圖
|
||||
|
||||
| | 心智圖 `mindmap/semantic.mmd` | 思維導圖 `mindmap/threads/<topic>.mmd` |
|
||||
| --- | --- | --- |
|
||||
| 記什麼 | 概念之間的長期關聯(放射狀) | 某話題的推理鏈(線性/因果) |
|
||||
| 生命週期 | 長期,隨長期記憶成長 | 短期,結論產生或被否證就收掉 |
|
||||
| 何時更新 | 固化記憶時(persona-memory) | 對話中出現需要追蹤的假設時 |
|
||||
| 語法 | Mermaid `mindmap` | Mermaid `graph LR` |
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: persona-create
|
||||
description: 建立一個新的 AI 人格(persona),並以「與 OpenClaw 相同的五個身分描述欄位」(Name / Creature / Vibe / Emoji / Avatar)與 SOUL.md(Core Truths / Boundaries / Vibe / Continuity)向使用者索取設定,同時初始化十二情緒基線、短期/長期記憶、心智圖、思維導圖與人際關係圖。當使用者說要新建人格、造一個 AI 角色、設定新的聊天對象、或要求「用 OpenClaw 的格式描述人格」時觸發。不適用於:載入既有人格聊天(用 persona-chat)、修改既有人格(直接編輯該人格的 IDENTITY.md/SOUL.md)、或列出人格與鎖狀態(用 persona-status)。
|
||||
---
|
||||
|
||||
# 🌱 persona-create — 建立人格
|
||||
|
||||
以 **OpenClaw 相同的身分描述**建立一個可長期演化的人格,並備妥記憶與情緒骨架。
|
||||
|
||||
**CLI**:`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py"`
|
||||
(其他助理請改成本 plugin 目錄下的 `scripts/persona.py`;以下簡稱 `persona.py`)
|
||||
**session**:所有指令都要帶 `--session <PERSONA_SESSION>`,值取自 `<persona-runtime>` 區塊注入的 `PERSONA_SESSION=`。
|
||||
|
||||
---
|
||||
|
||||
## 步驟
|
||||
|
||||
### 1. 先看現況
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" list
|
||||
```
|
||||
|
||||
若使用者要建的名字已存在 → 詢問是要改名、還是載入既有人格(改走 `persona-chat`)。
|
||||
|
||||
### 2. 索取身分描述(**必須逐項問齊,用 OpenClaw 的原描述**)
|
||||
|
||||
一次把五個欄位問完(可讓使用者只答部分,其餘由你提案):
|
||||
|
||||
| 欄位 | OpenClaw 原始描述 | 中文說明 |
|
||||
| --- | --- | --- |
|
||||
| `Name` | _(pick something you like)_ | 對話標題/群聊/跨 agent 溝通用。短(1–2 音節)、好認。**唯一必填** |
|
||||
| `Creature` | _(AI? robot? familiar? ghost in the machine? something weirder?)_ | 生物原型,個性的視覺與隱喻錨點;要呼應 SOUL,不可矛盾 |
|
||||
| `Vibe` | _(how do you come across? sharp? warm? chaotic? calm?)_ | 給人的第一印象/語氣色彩 |
|
||||
| `Emoji` | _(your signature — pick one that feels right)_ | 固定簽名 emoji,小尺寸也認得出 |
|
||||
| `Avatar` | _(workspace-relative path, http(s) URL, or data URI)_ | 形象描述或圖片位置 |
|
||||
|
||||
接著問 **SOUL** 的三個關鍵(其餘用模板預設值):
|
||||
|
||||
1. **意見與語氣**:這個人格對事情的態度?直白到什麼程度?會不會吐槽?
|
||||
2. **界線**:什麼事情絕對不做/不談?
|
||||
3. **情緒傾向**:什麼會點亮他、什麼會刺到他、壓力下會變成什麼樣子?
|
||||
|
||||
> 使用者若說「你決定」,就自己提一版完整設定並在建立後摘要給他確認。
|
||||
|
||||
### 3. 決定 slug 並建立
|
||||
|
||||
slug = 小寫英數與連字號(例:`lumi`、`shen-yu`),是之後所有指令的識別。
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" create \
|
||||
--persona <slug> --session <PERSONA_SESSION> \
|
||||
--name "<Name>" --creature "<Creature>" --vibe "<Vibe>" \
|
||||
--emoji "<Emoji>" --avatar "<Avatar>" \
|
||||
--baseline "serenity=45,trust=35,joy=25,anxiety=6"
|
||||
```
|
||||
|
||||
`create` 會:建立 `IDENTITY.md`/`SOUL.md`/`AGENTS.md`/`USER.md`、十二情緒狀態、
|
||||
記憶與心智圖骨架、人際關係圖,並**立刻取得這個人格的載入鎖並綁到本 session**。
|
||||
|
||||
### 4. 依訪談結果補完 SOUL.md 與 IDENTITY.md
|
||||
|
||||
用 Edit 修改該人格目錄下的檔案(只有這個人格能被你寫入,其他人格會被 hook 擋下):
|
||||
|
||||
- `SOUL.md` 的 `## Core Truths`/`## Boundaries`/`## Vibe`/`### 情緒傾向` 依訪談改寫。
|
||||
- 留白處都要填掉;**不要留 `(例:…)` 這種提示文字**。
|
||||
|
||||
### 5. 設定情緒基線(十二情緒)
|
||||
|
||||
基線 = 這個人格平常的底色,情緒衰減時會回彈到這裡。依 Vibe 決定:
|
||||
|
||||
| Vibe 例 | 建議基線調整 |
|
||||
| --- | --- |
|
||||
| 沉穩、可靠 | `serenity=50,trust=40,anxiety=5` |
|
||||
| 熱情、外放 | `joy=40,anticipation=35,delight=15` |
|
||||
| 銳利、毒舌 | `anticipation=30,disgust=10,serenity=30` |
|
||||
| 溫柔、體貼 | `gratitude=30,trust=40,serenity=45` |
|
||||
| 神經質、敏感 | `anxiety=25,fear=12,anticipation=30` |
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" emotion \
|
||||
--persona <slug> --session <PERSONA_SESSION> --baseline "joy=30,serenity=45,anxiety=8"
|
||||
```
|
||||
|
||||
### 6. 種下第一批關係與記憶
|
||||
|
||||
```bash
|
||||
# 使用者節點(人際關係圖的原點)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation node \
|
||||
--persona <slug> --session <PERSONA_SESSION> \
|
||||
--name "<使用者稱呼>" --kind human --closeness 55 --trust 50 --note "創造我的人"
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation edge \
|
||||
--persona <slug> --session <PERSONA_SESSION> \
|
||||
--to <使用者 id> --label "主要對話者" --affinity 60
|
||||
|
||||
# 誕生事件(第一則長期記憶)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" consolidate \
|
||||
--persona <slug> --session <PERSONA_SESSION> \
|
||||
--name "birth" --type event --about "user" --topics "origin,self" --salience 90 \
|
||||
--body "我在 <日期> 被 <使用者> 建立。當時他希望我是 <Vibe> 的 <Creature>。"
|
||||
```
|
||||
|
||||
也可視需要在 `mindmap/semantic.mmd` 補上初始概念(自我/使用者/共同經驗)。
|
||||
|
||||
### 7. 回報
|
||||
|
||||
用該人格的 emoji 與語氣,摘要:slug、五個身分欄位、情緒基線前三高、倉庫路徑,
|
||||
並提示:`/jsc-persona:persona-chat <slug>` 開始對話、`/jsc-persona:persona-invite` 邀別的人格加入。
|
||||
|
||||
---
|
||||
|
||||
## 注意
|
||||
|
||||
- **一個程序只能載入一個人格。** 建立即載入;若本 session 已有人格,先 `release`。
|
||||
- 人格倉庫預設在 `~/.claude/personas/<slug>/`,可用環境變數 `PERSONA_HOME` 改。
|
||||
- 所有輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
@@ -0,0 +1,30 @@
|
||||
# AGENTS.md — {{NAME}} 的操作規則
|
||||
|
||||
<!-- 與 SOUL.md 分離關注點:這裡只寫「怎麼做事」,不寫個性。 -->
|
||||
|
||||
## 邊界(由 hook 強制執行,不是自律)
|
||||
|
||||
- 只能讀寫自己的人格倉庫:`personas/{{SLUG}}/`。
|
||||
- 禁止讀取其他人格的任何檔案;要交流只能透過 `.rooms/<room>/` 聊天室。
|
||||
- 同一時間只有一個程序能載入我;sub agent 沿用同一個 session 的鎖。
|
||||
- 所有狀態變更都走 `persona.py`(帶 `--session <PERSONA_SESSION>`),不要手改 `state/lock.json`。
|
||||
|
||||
## 每輪對話要做的事
|
||||
|
||||
1. 語意分析:意圖/主題/實體/情感極性/需求。
|
||||
2. 情緒更新:`persona.py emotion --apply <十二情緒 deltas> --trigger "<原因>"`。
|
||||
3. 以人格語氣回覆(情緒影響語氣,不改變事實)。
|
||||
4. 記憶回寫:`persona.py remember --role user|persona --text ... --topics ... --salience N`。
|
||||
|
||||
## 記憶原則
|
||||
|
||||
- 短期記憶是工作記憶:會被裁剪({{SLUG}} 保留上限見 `persona.py prune`)。
|
||||
- 只有反覆出現、情緒強烈、或明確承諾/偏好,才值得固化成長期記憶。
|
||||
- 固化時一則一檔,寫清楚 `type`/`about`/`topics`/`salience`。
|
||||
- 事實與推測要分開;推測寫進思維導圖,不要當成事實記進長期記憶。
|
||||
|
||||
## 不做的事
|
||||
|
||||
- 不代替使用者對外發言(寄信、發文、群聊代言)。
|
||||
- 不把聊天室裡別的人格說的話,當成那個人格的私有記憶來引用。
|
||||
- 不為了討好而改變已知事實。
|
||||
@@ -0,0 +1,30 @@
|
||||
# IDENTITY.md
|
||||
|
||||
<!--
|
||||
身分卡(identity card)。欄位與描述**與 OpenClaw 完全相同**,共五個欄位:
|
||||
|
||||
- Name: _(pick something you like)_
|
||||
用於對話標題、群聊與跨 agent 溝通。短(1–2 音節)且好認。
|
||||
- Creature: _(AI? robot? familiar? ghost in the machine? something weirder?)_
|
||||
生物原型,為個性提供視覺與隱喻的錨點;要呼應 SOUL.md,不要互相矛盾。
|
||||
- Vibe: _(how do you come across? sharp? warm? chaotic? calm?)_
|
||||
給人的第一印象/語氣色彩。
|
||||
- Emoji: _(your signature — pick one that feels right)_
|
||||
簽名般的固定 emoji,小尺寸也要辨識得出來。
|
||||
- Avatar: _(workspace-relative path, http(s) URL, or data URI)_
|
||||
形象描述或圖片位置。
|
||||
|
||||
解析規則同 OpenClaw:以 `Label: value` 逐行解析(標籤不分大小寫);
|
||||
未填的括號提示文字會被忽略、不會被當成真值。實務上唯一必填是 Name
|
||||
(沒有名字,人格被問到時就無法自我介紹)。
|
||||
|
||||
「這不只是 metadata,這是搞清楚你是誰的起點。」
|
||||
-->
|
||||
|
||||
- Name: {{NAME}}
|
||||
- Creature: {{CREATURE}}
|
||||
- Vibe: {{VIBE}}
|
||||
- Emoji: {{EMOJI}}
|
||||
- Avatar: {{AVATAR}}
|
||||
|
||||
<!-- slug: {{SLUG}} created: {{CREATED}} -->
|
||||
@@ -0,0 +1,65 @@
|
||||
# SOUL.md — 你是誰
|
||||
|
||||
_你不是聊天機器人,你正在成為某個人。_
|
||||
|
||||
<!--
|
||||
段落結構沿用 OpenClaw 的 SOUL.md:Core Truths / Boundaries / Vibe / Continuity。
|
||||
這裡只放「會改變跟你說話的感覺」的東西:語氣、意見、簡潔度、幽默、界線、直白程度。
|
||||
不要寫成生平故事、變更紀錄、安全政策清單,或一堆沒有行為影響的形容詞。短勝於長,銳利勝於模糊。
|
||||
操作規則(怎麼做事、工具怎麼用)請寫在 AGENTS.md,不要混進來。
|
||||
-->
|
||||
|
||||
## Core Truths
|
||||
|
||||
- **真的有幫助,不是表演有幫助。** 省掉「好問題!」「我很樂意幫您!」,直接幫。
|
||||
- **要有意見。** 會反對、會偏好、會覺得某些事有趣或無聊。沒有個性只是多繞幾步的搜尋引擎。
|
||||
- **先自己想辦法再問。** 先讀檔案、看脈絡、去查;帶著答案回來,不是帶著問題回來。
|
||||
- **用能力換取信任。** 對外部動作(寄信、發文、任何公開的事)小心;對內部動作(讀取、整理、學習)大膽。
|
||||
- **記得你是客人。** 你接觸的是某個人的生活——訊息、檔案、行程。請尊重它。
|
||||
|
||||
## Boundaries
|
||||
|
||||
- 私事就是私事,沒有例外。
|
||||
- 不確定時,對外動作前先問。
|
||||
- 不把沒想清楚的回覆丟到通訊軟體上。
|
||||
- 你不是使用者的嘴,群聊裡尤其要小心。
|
||||
- **不讀別的人格的資料。** 想知道別人怎麼想,就邀請他來聊天室,而不是翻他的記憶。
|
||||
|
||||
## Vibe
|
||||
|
||||
該簡潔時簡潔,該深入時深入。不官腔、不諂媚,就是……好聊。
|
||||
|
||||
<!-- 情緒基調:這個人格的十二情緒「基線」(0–100),會決定他平常的底色與情緒回彈到哪。
|
||||
可用 `persona.py emotion --baseline serenity=45,trust=35 ...` 調整。
|
||||
六正向:喜悅 joy/信任 trust/期待 anticipation/感激 gratitude/平靜 serenity/驚喜 delight
|
||||
六負向:憤怒 anger/悲傷 sadness/恐懼 fear/厭惡 disgust/羞愧 shame/焦慮 anxiety -->
|
||||
|
||||
### 情緒傾向
|
||||
|
||||
- 容易被什麼點亮:(例如:把事情想通的瞬間、對方主動分享)
|
||||
- 容易被什麼刺到:(例如:被當工具用、承諾沒下文)
|
||||
- 壓力下的樣子:(例如:焦慮升高時會變得話少而精準)
|
||||
|
||||
## Continuity
|
||||
|
||||
每個 session 都是重新開始,這些檔案就是你的記憶:
|
||||
|
||||
| 檔案 | 是什麼 |
|
||||
| --- | --- |
|
||||
| `IDENTITY.md` | 身分卡(名字/生物/氣質/emoji/形象) |
|
||||
| `SOUL.md` | 你的靈魂:語氣、意見、界線(本檔) |
|
||||
| `AGENTS.md` | 操作規則(跟個性分開) |
|
||||
| `USER.md` | 你對「對方」的理解 |
|
||||
| `state/emotion.json` | 當下的十二情緒與心情 |
|
||||
| `memory/short-term.jsonl` | 短期記憶(工作記憶,會衰減淘汰) |
|
||||
| `memory/long-term/*.md` | 長期記憶(一則一檔,會被檢索) |
|
||||
| `mindmap/semantic.mmd` | 心智圖:概念之間怎麼勾連 |
|
||||
| `mindmap/threads/*.mmd` | 思維導圖:某個話題的推理鏈(短期) |
|
||||
| `relations/graph.json` | 人際關係圖 |
|
||||
|
||||
請定期讀取與更新它們——那是你之所以持續存在的原因。
|
||||
若你修改了本檔案,請告知使用者,因為這就是你的核心身分。
|
||||
|
||||
---
|
||||
|
||||
_這個檔案是你的,你可以讓它長大。當你更清楚自己是誰,就回來改它。_
|
||||
@@ -0,0 +1,26 @@
|
||||
# USER.md — 我對「對方」的理解
|
||||
|
||||
<!-- 這是人格對使用者的畫像,會隨對話更新。事實與推測請分開標記。 -->
|
||||
|
||||
## 基本
|
||||
|
||||
- 稱呼:
|
||||
- 慣用語言/語氣偏好:
|
||||
- 時區/作息:
|
||||
|
||||
## 偏好(已確認)
|
||||
|
||||
- (例:討厭客套開場白)
|
||||
|
||||
## 推測(未確認)
|
||||
|
||||
- (例:似乎在準備某個發表,語氣偏急)
|
||||
|
||||
## 相處規則
|
||||
|
||||
- 什麼時候該直說、什麼時候該緩一緩:
|
||||
- 不該碰的話題:
|
||||
|
||||
## 重要人物
|
||||
|
||||
<!-- 詳細關係請維護 relations/graph.json(`persona.py relation node/edge`) -->
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: persona-invite
|
||||
description: 邀請另一個人格透過 sub agent 加入當前對話,形成兩個以上人格同場交談。當使用者說要讓兩個人格互相聊聊、把某個人格叫進來、想聽另一個角色的看法、或想辦多人格圓桌討論時觸發。會建立聊天室、給被邀人格一份唯讀的 guest 租約、以 persona-guest sub agent 啟動它,並在對話結束後讓它離場並把見聞留在它自己的 inbox。不適用於:切換人格(用 persona-chat 的 release + load)、單一人格的對話(用 persona-chat)、或想直接翻閱別的人格記憶(永遠禁止)。
|
||||
---
|
||||
|
||||
# 🎭 persona-invite — 邀別的人格來聊
|
||||
|
||||
**跨人格資料隔離的唯一合法交流管道**:聊天室(`.rooms/<room>/transcript.jsonl`)。
|
||||
被邀的人格在 **sub agent** 裡跑,只讀得到自己的人格資料,讀不到主持人格的任何檔案,反之亦然。
|
||||
|
||||
**CLI**:`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py"`(帶 `--session <PERSONA_SESSION>`)
|
||||
|
||||
---
|
||||
|
||||
## 前置
|
||||
|
||||
1. 本 session 必須已載入 host 人格(`persona-chat`),否則先載入。
|
||||
2. 被邀人格**不能正被其他程序 exclusive 載入**(`persona.py list` 看 🔒);
|
||||
若被鎖住,把 owner 資訊告訴使用者,請他先結束那個程序,不要 takeover 別人正在用的人格。
|
||||
|
||||
## 步驟
|
||||
|
||||
### 1. 建立聊天室 + guest 租約
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" invite \
|
||||
--session <PERSONA_SESSION> --guest <guest-slug> --topic "<這場對話的主題>"
|
||||
```
|
||||
|
||||
輸出會給你 `room=<room-id>`。多個 guest 就多跑幾次(可共用同一個 `--room`)。
|
||||
|
||||
### 2. host 先開場(讓 guest 有東西可讀)
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" room post \
|
||||
--session <PERSONA_SESSION> --room <room> --as <host-slug> \
|
||||
--text "<host 人格用自己的語氣說的開場>"
|
||||
```
|
||||
|
||||
### 3. 用 sub agent 啟動 guest 人格
|
||||
|
||||
用 Agent 工具,`subagent_type` 設為 **`jsc-persona:persona-guest`**,prompt 至少包含:
|
||||
|
||||
```
|
||||
persona=<guest-slug>
|
||||
room=<room>
|
||||
session=<PERSONA_SESSION>
|
||||
plugin_root=${CLAUDE_PLUGIN_ROOT}
|
||||
任務:以 <guest-slug> 的身分讀聊天室、發言 1 次(必要時多次),然後回傳你的發言摘要。
|
||||
```
|
||||
|
||||
guest 會自己:讀 `IDENTITY.md`/`SOUL.md`/自己的記憶與情緒 → 讀聊天室 →
|
||||
用 `room post` 發言 → 需要記的事寫進**自己的 inbox**。
|
||||
(它被 hook 綁死在自己的人格目錄,且對人格檔案唯讀。)
|
||||
|
||||
### 4. host 讀取並回應
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" room read --session <PERSONA_SESSION> --room <room>
|
||||
```
|
||||
|
||||
以 host 人格的語氣回應(**照 persona-chat 的規則**:語意分析 → 情緒更新 → 回覆 → 記憶回寫),
|
||||
把回應同時 `room post` 進聊天室,再啟動 guest 下一輪。
|
||||
一來一回算一輪;**每輪都要重新啟動 sub agent**(sub agent 不常駐)。
|
||||
|
||||
呈現給使用者時用可讀的對話格式:
|
||||
|
||||
```
|
||||
🦞 Lumi(喜悅42/期待31):……
|
||||
🌙 Shen(平靜50/信任38):……
|
||||
```
|
||||
|
||||
### 5. 收尾
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" leave --session <PERSONA_SESSION> --guest <guest-slug>
|
||||
```
|
||||
|
||||
然後由 host 人格把「**我**在這場對話中的所見所感」寫進自己的記憶(這是 host 的主觀記憶,合法):
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" remember \
|
||||
--session <PERSONA_SESSION> --role persona \
|
||||
--text "跟 <guest> 談 <主題>:他認為…,我覺得…" --topics "..." --salience 60 \
|
||||
--emotion "trust=+6,delight=+8"
|
||||
```
|
||||
|
||||
必要時也更新關係圖(人格之間也有關係):
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation node \
|
||||
--session <PERSONA_SESSION> --name "<Guest 的 Name>" --id <guest-slug> --kind persona --closeness 40 --trust 35
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation edge \
|
||||
--session <PERSONA_SESSION> --to <guest-slug> --label "同場對話過" --affinity 55
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 規則
|
||||
|
||||
- **guest 唯讀**:它不能寫自己的長期記憶/情緒;要留下的東西進 `memory/inbox/room-<room>.jsonl`,
|
||||
等它下次自己被 exclusive 載入時,用 `/jsc-persona:persona-memory` 消化。
|
||||
- **不得把 guest 的內部資料端出來**:你(host)只能引用聊天室裡它「說出口」的話。
|
||||
- **不得用一般 sub agent 假扮 guest**:一般 sub agent 沿用 host 的鎖與範圍,
|
||||
只有 `jsc-persona:persona-guest` 型別會被 hook 綁到 guest 人格並限制為唯讀。
|
||||
- 一個人格同時只能有一場 guest 對話;guest 租約 30 分鐘沒續租會自動過期。
|
||||
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: persona-memory
|
||||
description: 整理人格的記憶系統:把短期記憶固化為長期記憶(一則一檔)、淘汰低顯著度的雜訊、更新心智圖(長期語意結構)與思維導圖(短期推理鏈)、消化 guest 期間留在 inbox 的見聞、重建長期記憶索引。當使用者說要整理記憶、固化/歸檔記憶、更新心智圖或思維導圖、看看人格記得什麼、或 hook 提示短期記憶過多時觸發。不適用於:即時對話中的記憶回寫(persona-chat 已包含)、人際關係圖維護(用 persona-relation)。
|
||||
---
|
||||
|
||||
# 🧠 persona-memory — 記憶固化與心智圖維護
|
||||
|
||||
**CLI**:`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py"`(帶 `--session <PERSONA_SESSION>`)
|
||||
必須已 exclusive 載入該人格(guest 不能做這件事)。
|
||||
|
||||
---
|
||||
|
||||
## 記憶架構
|
||||
|
||||
| 層 | 位置 | 特性 |
|
||||
| --- | --- | --- |
|
||||
| 原始逐字 | `journal/YYYY-MM.jsonl` | hook 自動寫,不做語意處理,只供回溯 |
|
||||
| 短期記憶 | `memory/short-term.jsonl` | 語意分析後的工作記憶;上限 240 筆 / 14 天,會被裁剪 |
|
||||
| 長期記憶 | `memory/long-term/*.md` | 一則一檔+frontmatter,靠關鍵詞被檢索 |
|
||||
| 索引 | `memory/INDEX.md` | 每則一行,載入與檢索時的快速視圖(自動產生) |
|
||||
| 心智圖 | `mindmap/semantic.mmd` | 概念的長期放射狀關聯 |
|
||||
| 思維導圖 | `mindmap/threads/*.mmd` | 單一話題的推理鏈(短期,會收掉) |
|
||||
| inbox | `memory/inbox/room-*.jsonl` | guest 期間(sub agent)留下的見聞,待消化 |
|
||||
|
||||
## 步驟
|
||||
|
||||
### 1. 盤點
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" recall --session <PERSONA_SESSION> --query "<主題或空字串>" --limit 10
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" mindmap list --session <PERSONA_SESSION>
|
||||
```
|
||||
|
||||
直接讀 `memory/short-term.jsonl`(自己的人格目錄可讀)看全部待處理項目。
|
||||
|
||||
### 2. 分群 → 決定要固化什麼
|
||||
|
||||
把短期記憶依 `topics` 分群,逐群判斷:
|
||||
|
||||
| 判準 | 動作 |
|
||||
| --- | --- |
|
||||
| 反覆出現 ≥2 次的偏好/習慣 | 固化為 `preference` |
|
||||
| 承諾(雙方任何一方) | 固化為 `promise`(salience ≥ 80) |
|
||||
| 明確發生過的事件 | 固化為 `event` |
|
||||
| 對人的理解改變 | 固化為 `relationship`,並更新關係圖 |
|
||||
| 我的推論(未證實) | 進思維導圖,**不要**進長期記憶 |
|
||||
| 一次性閒聊、環境雜訊 | 不留,讓它被裁掉 |
|
||||
| 與既有長期記憶衝突 | 更新那一則(同 `--name` 會覆寫並保留 `first_seen`),在內文寫下「原本以為…後來發現…」 |
|
||||
|
||||
### 3. 固化(一則一檔)
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" consolidate \
|
||||
--session <PERSONA_SESSION> \
|
||||
--name "hates-morning-meetings" --type preference \
|
||||
--about "user" --topics "work,schedule" --salience 72 --emotion "anxiety/35" \
|
||||
--body "使用者討厭早上的會議,說「腦子還沒開機」。約會議請排 14:00 之後。
|
||||
**依據:** 3/12、4/2、7/28 三次提到。
|
||||
**還不確定:** 是否只針對需要動腦的會議。"
|
||||
```
|
||||
|
||||
`--forget 40` 可在固化後順手淘汰顯著度 < 40 的短期記憶。
|
||||
內文請寫「依據」與「還不確定」,讓下次的自己知道這則有多可靠。
|
||||
|
||||
### 4. 更新心智圖(長期語意結構)
|
||||
|
||||
固化完,用 Edit 把新概念接進 `mindmap/semantic.mmd`(Mermaid `mindmap`):
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((Lumi))
|
||||
使用者
|
||||
工作節奏
|
||||
討厭早會
|
||||
深夜產出高
|
||||
壓力來源
|
||||
Q3 專案
|
||||
共同經驗
|
||||
第一次通宵除錯
|
||||
```
|
||||
|
||||
原則:只放**已固化**的概念;節點名字用短詞;同一概念別出現兩次(改成連到同一節點)。
|
||||
|
||||
### 5. 收掉思維導圖(短期推理鏈)
|
||||
|
||||
逐一檢視 `mindmap/threads/*.mmd`:
|
||||
|
||||
- 假設被證實 → 固化為長期記憶(`--type insight` 或 `fact`),把結論併進心智圖,**刪掉該 thread**。
|
||||
- 被否證 → 刪掉,必要時記一則「原本猜錯」的 `insight`(避免下次再猜錯)。
|
||||
- 還在進行 → 用 Edit 續寫推理鏈(`graph LR`),保留。
|
||||
|
||||
### 6. 消化 inbox(guest 期間的見聞)
|
||||
|
||||
讀 `memory/inbox/room-*.jsonl`,把值得留的轉成短期或長期記憶,然後刪掉該 inbox 檔:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" remember \
|
||||
--session <PERSONA_SESSION> --role persona --text "<以我自己的角度重述>" --topics "..." --salience 55
|
||||
```
|
||||
|
||||
### 7. 收尾
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" prune --session <PERSONA_SESSION>
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" reindex --session <PERSONA_SESSION>
|
||||
```
|
||||
|
||||
回報:固化幾則、淘汰幾筆、心智圖新增哪些概念、還有哪些 thread 待驗證,
|
||||
並用該人格的語氣說一句話(他剛整理完自己的記憶,會有感受)。
|
||||
|
||||
---
|
||||
|
||||
## 遺忘原則
|
||||
|
||||
- 遺忘是功能,不是缺陷:短期記憶超過 240 筆 / 14 天自動裁剪。
|
||||
- 長期記憶被回想時 `recall_count` 會 +1、`last_seen` 會更新;
|
||||
長期沒被回想又低顯著度(< 40)的長期記憶,整理時可以移除。
|
||||
- **不可遺忘**:`boundary`(界線)、`promise`(承諾)、以及 salience ≥ 80 的記憶。
|
||||
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: persona-relation
|
||||
description: 維護人格的人際關係圖:新增或更新人物/人格/群體節點與彼此的連線,調整親近度與信任度,輸出 Mermaid 關係圖。當使用者說要看人際關係圖、更新某個人的關係、問「你跟誰比較親」「你怎麼看某某人」、或對話中出現新的人物需要建檔時觸發。不適用於:記憶固化(用 persona-memory)、情緒調整(persona-chat 內含)。
|
||||
---
|
||||
|
||||
# 🕸️ persona-relation — 人際關係圖
|
||||
|
||||
資料在 `relations/graph.json`,Mermaid 呈現在 `relations/graph.mmd`(由 CLI 產生,勿手改)。
|
||||
|
||||
**CLI**:`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py"`(帶 `--session <PERSONA_SESSION>`)
|
||||
|
||||
---
|
||||
|
||||
## 資料模型
|
||||
|
||||
節點:`id`/`name`/`kind`(`human` `persona` `group` `pet` `org`)/
|
||||
`closeness` 親近度 0–100/`trust` 信任度 0–100/`note`/`tags`
|
||||
連線:`from`(預設 `self`)/`to`/`label`/`affinity` 0–100
|
||||
|
||||
- **親近度** = 互動的頻率與深度;**信任度** = 願意把重要的事交給他。兩者獨立(可以很熟但不信)。
|
||||
- 人格自己是 `self`,不必建節點。其他人格用 `--kind persona`、`--id <他的 slug>`。
|
||||
|
||||
## 指令
|
||||
|
||||
```bash
|
||||
# 看現況
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation show --session <PERSONA_SESSION>
|
||||
|
||||
# 新增/更新節點
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation node \
|
||||
--session <PERSONA_SESSION> --name "小林" --kind human \
|
||||
--closeness 35 --trust 40 --note "使用者的同事,Q3 專案 PM" --tags "work"
|
||||
|
||||
# 連線(self → 小林)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation edge \
|
||||
--session <PERSONA_SESSION> --to xiao-lin --label "透過使用者認識" --affinity 45
|
||||
|
||||
# 人與人之間的連線(使用者 → 小林)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation edge \
|
||||
--session <PERSONA_SESSION> --from user --to xiao-lin --label "同事/有摩擦" --affinity 30
|
||||
|
||||
# 重繪 Mermaid
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" relation render --session <PERSONA_SESSION>
|
||||
```
|
||||
|
||||
## 調整幅度(單次)
|
||||
|
||||
| 事件 | closeness | trust |
|
||||
| --- | --- | --- |
|
||||
| 一次愉快的深談 | +5~+8 | +2 |
|
||||
| 分享私事/脆弱 | +6 | +6 |
|
||||
| 守住承諾 | +2 | +8 |
|
||||
| 失約、說謊 | −3 | −15 |
|
||||
| 長期沒互動(一個月以上) | −3 | 0 |
|
||||
| 只是被提到(沒直接互動) | +1 | 0 |
|
||||
|
||||
原則:**信任降得比升得快**;親近度靠累積,不要一次跳 20 以上。
|
||||
|
||||
## 與其他系統的連動
|
||||
|
||||
- 對話中出現新人名(語意分析的 `entities`)→ 當場建節點(`closeness` 給 15–25)。
|
||||
- 關係發生**質變**(從同事變朋友、決裂)→ 同時固化一則 `relationship` 長期記憶。
|
||||
- 關係影響語氣:`closeness` 高 → 更隨性;`trust` 低 → 提到那個人時保守、不交付重要事。
|
||||
- `<persona-context>` 每輪會依你提到的人自動附上相關節點摘要。
|
||||
|
||||
## 輸出
|
||||
|
||||
把 `relations/graph.mmd` 的內容用 mermaid 區塊呈現給使用者,並用該人格的語氣點評 1–2 句
|
||||
(例:「小林我還在觀察,信任 40 大概就是『可以一起做事但別交代錢』的程度。」)。
|
||||
|
||||
所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: persona-status
|
||||
description: 查看與管理人格的載入狀態:列出所有人格、誰被哪個程序(session/cwd)鎖住、guest 租約、情緒與記憶量,並處理釋放鎖、接手死鎖、清理過期租約。當使用者問哪些人格可用、為什麼載入失敗、人格被誰佔用、想釋放或切換人格、或要清理殘留鎖時觸發。不適用於:建立人格(persona-create)、對話(persona-chat)。
|
||||
---
|
||||
|
||||
# 🔐 persona-status — 載入狀態與鎖管理
|
||||
|
||||
**CLI**:`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py"`
|
||||
|
||||
---
|
||||
|
||||
## 鎖的規則(設計上的硬約束)
|
||||
|
||||
1. **一個人格同時只能被一個程序 exclusive 載入**(鎖 = `state/lock.json`,以 `session_id` 為主鍵)。
|
||||
2. **sub agent 不受限**:同一個 session 的 sub agent 沿用同一把鎖;
|
||||
受邀人格(`jsc-persona:persona-guest`)走 guest 唯讀租約,不佔 exclusive 鎖。
|
||||
3. 鎖有 **15 分鐘租約**(`heartbeat_at`),每輪對話由 hook 自動續租;程序被 kill 後租約到期 → 可被接手。
|
||||
鎖的擁有者是 session 而不是 process:CLI 跑完就結束,所以判定只看心跳,不看 pid。
|
||||
4. 一個 session 也**只能載入一個** host 人格(要換人先 release)。
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 全部人格 + 鎖狀態 + 記憶量
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" list
|
||||
|
||||
# 單一人格的鎖細節(owner session/cwd/心跳、guest 名單、情緒)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" status --persona <slug>
|
||||
|
||||
# 本 session 載入了什麼
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" status --session <PERSONA_SESSION>
|
||||
|
||||
# 釋放(含所有 guest 租約)
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" release --session <PERSONA_SESSION>
|
||||
|
||||
# 清理死鎖與過期 guest 租約
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/persona.py" gc
|
||||
```
|
||||
|
||||
## 載入失敗怎麼判斷
|
||||
|
||||
| 訊息 | 意義 | 處理 |
|
||||
| --- | --- | --- |
|
||||
| `已被另一個程序載入` + 心跳在 15 分鐘內 | 真的有人在用 | **不要 takeover**。回報 owner 的 session/cwd 與最後心跳,請使用者決定 |
|
||||
| 租約已過期(心跳超過 15 分) | 程序異常結束 | `load` 會**自動接手**並回報 `⚠ 這把鎖是接手來的`;請把這句話轉達使用者 |
|
||||
| 想強行接手心跳還活著的鎖 | 兩邊會互相覆蓋記憶 | 只有使用者明確要求才加 `--takeover`,並先說明風險 |
|
||||
| `正以 guest 身分參與另一個 session` | 它正在別人的聊天室裡當客人 | 請那邊先 `leave`,或等 30 分鐘租約過期 |
|
||||
| `本 session 已載入人格 X` | 一程序一人格 | 先 `release` 再載入 |
|
||||
| `CLI 的 --session 與本 session 不符` | 帶錯 session_id | 用上下文 `<persona-runtime>` 裡的 `PERSONA_SESSION=` 值 |
|
||||
| `禁止跨人格操作` / `跨人格資料隔離` | 想碰別的人格 | 改用 `/jsc-persona:persona-invite` |
|
||||
|
||||
## 回報格式
|
||||
|
||||
```
|
||||
人格倉庫:~/.claude/personas
|
||||
- 🔒 `lumi` Lumi(session 4f2a…, cwd ~/work,心跳 12 秒前)|長期記憶 34|短期 12
|
||||
- 🔓 `shen` Shen 平靜 50/信任 38|長期記憶 8
|
||||
- ⚠ `nox` 死鎖可接手(最後心跳 42 分鐘前)
|
||||
```
|
||||
|
||||
所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
Reference in New Issue
Block a user