依 todo.md 執行的規範治理專案: - persona-lib.mjs 的 iso()/nowIso() 改為輸出 +08:00 offset(等值瞬間轉換, 非加8小時),新增 nowDisplay()(人讀顯示格式)與 todayTaipei()(台北曆日 日期鍵),修正 11 處原本用 UTC 字串切片產生日期鍵的錯誤用法;新增 redactSecrets() 並套用到 persona-gitea.mjs 的 Gitea 錯誤路徑,避免機密 外洩。 - 新增一次性遷移腳本 scripts/migrate-tz.mjs(備份/dry-run/冪等/衝突偵測 中止),供之後有真實人格資料需要轉換時使用。 - persona 13 個 skill 與 AGENTS.md 接上 /jsc-shared 共用規範(spec-output、 spec-script-path、spec-subagent、spec-gitea、spec-git-safety、 spec-skill-invocation 等),不再各自重抄,且完全未變動人格行為本身的規範 (情緒模型、記憶衰減、劇場模式、鎖與隔離機制等逐字保留)。 - selftest 全數通過(788/788,含本次新增的時區邊界/冪等性測試)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
163 lines
8.8 KiB
Markdown
163 lines
8.8 KiB
Markdown
---
|
||
name: persona-sync
|
||
description: 人格編號與 Gitea 儲存:指派人格編號(英文名全大寫+索引,例如 ASUNA-01)、在 Gitea 為每個人格開一個以編號命名的私有存取庫、把高頻的活狀態同步到檔案區、把低頻的身分與長期記憶同步到 Wiki 區,以及把既有人格遷移上去。當使用者說要把人格存到 Gitea、備份或同步人格、在另一台機器接續同一個人格、問人格的編號是什麼、想看人格在 Gitea 上的存取庫、或同步出現衝突時觸發。不適用於:建立新人格(persona-create,它會自動開存取庫)、離線的單檔匯出匯入(persona-transfer)、記憶固化(persona-memory)。
|
||
---
|
||
|
||
# 🔗 persona-sync — 人格編號與 Gitea 儲存
|
||
|
||
**CLI**:`node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs"`(一律帶 `--session <PERSONA_SESSION>`)
|
||
|
||
---
|
||
|
||
## 共用規範(必要前置)
|
||
|
||
先載入 `/jsc-shared:spec-preflight` 並依其流程處理;載入不到即代表 shared plugin 未安裝,
|
||
依該 spec 詢問使用者是否安裝 `https://gitea.jsc.idv.tw/plugins/shared.git`,不安裝則中斷本 skill。
|
||
本 skill 需要的規範:spec-output、spec-execution、spec-time-log、spec-script-path、spec-gitea、spec-git-safety
|
||
|
||
---
|
||
|
||
## 人格編號
|
||
|
||
**編號 = 英文名全大寫 + 兩位索引**,同名才遞增:
|
||
|
||
```
|
||
亞絲娜(第一個) → ASUNA-01
|
||
結衣 → YUI-01
|
||
另一個亞絲娜 → ASUNA-02
|
||
```
|
||
|
||
- 編號**就是 Gitea 存取庫的名稱**,也是新人格的本機目錄名。
|
||
- 中文/日文名字要先轉**羅馬拼音**:由你提議拼法(`亞絲娜 → Asuna`、`沈宇 → Shen Yu`),
|
||
**拿給使用者確認再送出**——拼錯了會變成一個很難改的編號。
|
||
- 查下一個可用編號:`code next --romaji Asuna --session <id>`。
|
||
發號前會先問遠端 Gitea 已經用掉哪些編號(本機看不到別台機器發過的號);
|
||
Gitea 連不上時照樣發得出來,但輸出會明講「只對過本機」——那就有撞號的風險,要轉告使用者。
|
||
|
||
## 資料放哪裡(依更新頻率切)
|
||
|
||
| 區 | 放什麼 | 什麼時候 push |
|
||
| --- | --- | --- |
|
||
| **檔案區**(主存取庫) | 高頻活狀態:`state/emotion.json`、`short-term.jsonl`、`inner.jsonl`(心裡話)、`said.jsonl`、`inbox/`、`mindmap/threads/`、`journal/` | 每輪對話後由 `Stop` hook 背景推送(有最小間隔) |
|
||
| **Wiki 區** | 低頻設定:`IDENTITY`/`SOUL`/`AGENTS`/`USER`、**人格圖示 `icon.svg`/`icon.png`**、長期記憶、`INDEX`、心智圖、人際關係圖 | 記憶固化、改身分、改關係圖、`release` 時 |
|
||
|
||
Wiki 是給人讀的設定百科:Gitea 的 wiki 只有根目錄的 `.md` 會變成頁面,所以
|
||
`memory/long-term/xxx.md` 會攤平成 `Memory-xxx.md`(頁面顯示為「Memory xxx」),
|
||
原始路徑記在 `_paths.json`,拉回來時自動還原。Home 頁會自動列出所有長期記憶的連結。
|
||
|
||
**本機永遠是工作副本**:hook 每輪讀寫的是本機檔案,不經過網路。Gitea 掛掉、離線、沒設 token,
|
||
人格照樣能聊天,只是不同步——**同步失敗永遠不阻斷對話**。
|
||
|
||
人格圖示(`/jsc-persona:persona-create` 或 `icon generate` 產生)除了同步到 Wiki 區,
|
||
也會被設成**存取庫頭像**,Gitea 的清單上就看得到每個人格的臉。
|
||
|
||
## 設定
|
||
|
||
```bash
|
||
export GITEA_HOST=https://gitea.example.com # 或 PERSONA_GITEA_HOST
|
||
export GITEA_TOKEN=<個人存取權杖> # 或 PERSONA_GITEA_TOKEN
|
||
export PERSONA_GITEA_OWNER=<帳號或組織> # 選填,預設是 token 本人的帳號
|
||
export PERSONA_GITEA=off # 需要時整個關掉
|
||
export PERSONA_SYNC_MIN_SECONDS=60 # 檔案區背景 push 的最小間隔
|
||
```
|
||
|
||
存取庫**預設私有**(人格裡有使用者的個人記憶)。要公開必須是使用者明講,才加 `--public`。
|
||
|
||
## 常用流程
|
||
|
||
```bash
|
||
# 現在的同步狀態(編號、存取庫、兩區的最後 push/pull)
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync status --session <PERSONA_SESSION>
|
||
|
||
# 手動推送(--area files|wiki|all)
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync push --session <PERSONA_SESSION> --area all
|
||
|
||
# 從 Gitea 拉回(載入人格時會自動做一次)
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SESSION>
|
||
```
|
||
|
||
### 把既有人格遷移上去
|
||
|
||
既有人格(還沒有編號的)要兩步:
|
||
|
||
```bash
|
||
# 1) 載入它(同步只能動本 session 載入的那個人格)
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" load --persona <舊 slug> --session <PERSONA_SESSION>
|
||
|
||
# 2) 指派編號 + 開存取庫 + 首次推送;--rename 連本機目錄名也改成編號
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" code assign \
|
||
--session <PERSONA_SESSION> --romaji "<英文名>" --rename
|
||
```
|
||
|
||
`--rename` 之後這個人格就用編號稱呼(`--persona ASUNA-01`)。不加 `--rename` 也可以,
|
||
目錄名維持原樣、只是多了一個編號與對應的存取庫。**遷移前先跟使用者確認**:
|
||
這會把他的個人記憶送上 Gitea(私有庫,但仍是上傳)。
|
||
|
||
### 在另一台機器接續同一個人格
|
||
|
||
在新機器上設好 `GITEA_HOST`/`GITEA_TOKEN`。**本機已經有這個人格**(只是舊了):
|
||
|
||
```bash
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" load --persona <編號> --session <PERSONA_SESSION>
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SESSION>
|
||
```
|
||
|
||
**本機還沒有這個人格**(全新的機器):`sync` 的每個動作都要求「先載入那個人格」,
|
||
而本機沒有它就 load 不了它,所以走 `clone`——它只驗 session,不驗 host:
|
||
|
||
```bash
|
||
# 1) 遠端有哪些人格、哪些本機還沒有
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" clone --session <PERSONA_SESSION>
|
||
|
||
# 2) 挑一個拉回來(存取庫名稱就是編號)
|
||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" clone --code ASUNA-01 --session <PERSONA_SESSION>
|
||
```
|
||
|
||
拉回來的是**完整的人格**:Wiki 區帶回身分與長期記憶,檔案區帶回情緒與短期記憶,
|
||
拉完自動重建長期記憶索引與關係圖。沒帶回來的只有鎖與租約那類執行期狀態——那本來就該由這台機器自己產生。
|
||
|
||
- 本機已經有同名人格時**預設不覆蓋**。真的要以遠端為準才加 `--force`(本機沒推上去的改動會不見),
|
||
想並存兩份就用 `--persona <別的目錄名>`。
|
||
- 加 `--load` 可以拉完直接載入開聊。
|
||
- 拉回來的東西沒有 `IDENTITY.md`(人格的最低要件)就會中止並清掉半成品,不留半個人格在本機。
|
||
|
||
## 衝突
|
||
|
||
兩台機器都改過同一個檔案時,`load` 與 `sync pull` 會**停下來不覆蓋本機**,並列出衝突的檔案。
|
||
這時候:
|
||
|
||
1. 用一句話告訴使用者哪幾個檔案兩邊都動過。
|
||
2. 問他要哪一邊:
|
||
- 以 **Gitea** 為準 → `sync pull --force`(本機那份會被覆蓋)
|
||
- 以 **本機** 為準 → `sync push`(會蓋掉遠端)
|
||
3. **不要自己選**。記憶被蓋掉是不可逆的。
|
||
|
||
### push 撞到別台機器時是本機贏——但會記帳
|
||
|
||
`sync push`(包含每輪對話後的背景推送)遇到遠端比較新時,會**以本機為準覆蓋遠端**,
|
||
因為本機的工作副本才是這台機器的真相來源。這條路可以走,但不會安靜地走:
|
||
|
||
- 回報「覆蓋了哪幾個檔案、上一版是哪個 commit」,並記進 `state/sync.json`。
|
||
- `sync status` 列得出來;下一輪的 `Stop` hook 會把它講給使用者聽一次。
|
||
- 被蓋掉的內容還在 clone 的歷史裡:
|
||
`git -C <人格>/.sync/files show <上一版 sha>:<檔案>`。
|
||
|
||
看到這種回報就**告訴使用者**:另一台機器可能正在用同一個人格。要救回舊版就從上面那行取出來。
|
||
|
||
## 殘留的 git 鎖
|
||
|
||
`push`/`pull` 跑到一半被中斷(sub agent 被砍、視窗關掉)會在 clone 裡留下 `.git/index.lock`,
|
||
之後那一區的 `git add` 每次都失敗 → 整區同步卡住。這件事**不用你處理**:
|
||
這些 clone 只有 CLI 會動,所以超過 30 秒還在的鎖一定是殘骸,`push` 與 `pull` 會自己清掉再繼續。
|
||
|
||
不要叫使用者手動進 `.sync/` 刪檔案,也不要自己下 `rm`——
|
||
sleeper 連自己的人格目錄都不能用 shell 寫,那條路本來就是死的。
|
||
真的還是失敗(例如權限問題),照原樣把錯誤訊息轉告他就好。
|
||
|
||
## 邊界
|
||
|
||
- 只能同步「本 session 目前載入的人格」——跨人格同步等於跨人格讀取,會被 hook 擋下。
|
||
唯一的例外是 `clone`:它只會**新增本機還沒有的人格**,讀不到任何既有人格的資料,所以不受此限。
|
||
- guest(`persona-guest` sub agent)不能同步,它對人格檔案唯讀。
|
||
- `.sync/` 是兩區的 git clone 快取,**不要手動編輯**;砍掉它不會掉資料,下次 push 會重新 clone。
|