jiantw83
|
e8d994df78
|
docs(persona): remove legacy marketplace namespace
|
2026-07-31 17:17:53 +00:00 |
|
jiantw83
|
6ea5076205
|
docs(persona-chat): clarify auto-hook boundary
|
2026-07-31 17:00:01 +00:00 |
|
 jiantw83andClaude Opus 5
|
34b1dc7942
|
fix(marketplace): marketplace 名由 jsc-plugins 改為 persona(與 template 撞名)
兩份 marketplace.json 原本都叫 jsc-plugins,與 jsc-template 的 marketplace 同名
——兩個 plugin 同時安裝時會撞名。template 已在 731a494 改為 template(=它的 repo
名),本 repo 跟著改為 persona,也與 code/doc/generic「marketplace 名 = repo 名」
的慣例一致。plugin 名維持 jsc-persona 不動。
README 的安裝 token、marketplace update/upgrade/remove 指令、目錄樹註解與
「marketplace 名是 …」那句一併更新(Claude/Codex/Copilot 三節各一組)。
依 spec-plugin-version,安裝識別鍵是 <plugin 名>@<marketplace 名>,改名等於換識別
鍵,舊安裝不能用 update 遷移,必須先移除再安裝,中間 skills 會短暫消失。照
template 9b2e2bb 的做法在安裝章節開頭補上三家各自的升級路徑,並交代要清掉
settings.json 的 enabledPlugins 舊鍵、人格倉庫(PERSONA_HOME)不在 plugin 目錄裡
所以情緒與記憶不受影響。
版號維持 0.1.0 不動:spec 的「更名重置版號」指的是 plugin 更名(name 欄位改變),
本次三份 plugin.json 的 name 與 version 都沒動,marketplace.json 也沒有版本欄位;
template 當時的 marketplace 改名(731a494)同樣沒有重置,照常從 0.0.2 走。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 14:43:58 +00:00 |
|
 jiantw83andClaude Opus 5
|
ed80158d99
|
chore(plugin 版本): 三家 manifest 升版 0.1.0
master 現行 0.0.9,patch 已到 9,依 spec-plugin-version 進位 minor 並把
patch 歸零(0.0.9 → 0.1.0),非 0.0.10。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 14:38:39 +00:00 |
|
 jiantw83andClaude Opus 5
|
bc571cf37f
|
fix(data): 四處情緒/sync 狀態的讀改寫還沒進鎖,等於繞過了上次的修正
B4 加了 updateJson/updateEmotion,但這幾個呼叫點還是「loadEmotion → 改 →
writeJson」的裸讀改寫,鎖形同不存在:
- sleep 的 emotion-decay、remember --emotion(每輪對話的熱路徑)、
Stop hook 與 SessionEnd hook 的時間衰減,四處都改走 pl.updateEmotion()。
衰減與 delta 的算法一個字都沒動,只是把讀與寫收進同一把鎖裡。
- sync.json 同理:背景 sync push 跟前景指令會同時寫它,兩邊撞上時後寫的會把
pushed_at/overwrites 整段蓋掉——覆蓋紀錄就這樣安靜地消失。新增
updateSyncState()(帶鎖),六處 loadSyncState + saveSyncState 成對使用全部
改過去;補預設欄位的邏輯抽成 normalizeSyncState() 給兩邊共用。
saveSyncState 沒有呼叫端了,直接移掉,免得下次又有人拿它裸寫。
sync.json 的欄位格式與 overwrites 留 10 筆的行為都沒變。
selftest 448 項(+1):新增「並行 8 次 remember --emotion 跟循序結果一樣」,
把對話熱路徑的情緒寫入也蓋進競態測試——原本只蓋到 emotion --apply。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 14:35:06 +00:00 |
|
jiantw83
|
fa4aa8b787
|
merge(gitea): 同步漏檔/push 衝突回報/clone 新功能/錯誤訊息
衝突:scripts/selftest.mjs 檔尾兩份都各自 append 一個測試區塊
(guard 的「注入區塊不可被人格檔案逸出(S6)」與 gitea 的「㉒ 從 Gitea
匯入本機還沒有的人格」)。兩塊都保留,各自收好自己的 `}`。
|
2026-07-31 14:29:10 +00:00 |
|
jiantw83
|
9b5de053e3
|
merge(data): 並行鎖/consolidate 保護/情緒校驗/speechLint 誤殺修正
|
2026-07-31 14:27:26 +00:00 |
|
jiantw83
|
0a12321df6
|
merge(guard): 資安三項修正(S1 sleeper pin/S2 Glob-Grep 破口/S6 注入區與 room 偽造)
|
2026-07-31 14:27:20 +00:00 |
|
 jiantw83andClaude Opus 5
|
ca4becbaba
|
fix(gitea): clone 失敗訊息不要被 git 的多行 stderr 截斷成半個字
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:38:17 +00:00 |
|
 jiantw83andClaude Opus 5
|
2b4b49d478
|
feat(gitea): clone —— 從 Gitea 匯入一個本機還沒有的人格
底層本來就走得通(`pullArea` 的 restore 會把本機缺少的檔案全部補進來),
擋住的是上層的雞生蛋:`sync` 先走 `requireOwner`,而 `requireOwner` 第一件事
就是「本機沒有這個人格就 die」。本機沒有它 → load 不了它 → sync pull 被擋 →
永遠拉不回來。所以照 `import` 的模式另開一個只驗 session、不驗 host 的入口。
* `clone --code <編號>`:兩區都拉回來(Wiki 區給身分與長期記憶,檔案區給活狀態),
然後補上 `pullArea` 不管的那幾件事——驗 IDENTITY.md(`validateBundle` 明文的
人格最低要件,Gitea 這條路上原本不存在)、補寫 config.code/來歷、
`rebuildIndex()`、`renderRelations()`。拉回來不成人格就中止並清掉半成品。
* `clone`(不帶 --code):列出遠端有哪些人格、哪些本機還沒有。
整個 codebase 原本沒有任何「列出 owner 底下的存取庫」的呼叫,新增
`listRemotePersonas()`:分頁打 `GET /user/repos`(他人/組織走 `/users/<owner>/repos`),
用編號格式過濾——存取庫名稱就是人格編號,所以那份清單就是遠端的人格清單。
* 本機已有同名人格時**預設不覆蓋**;`--force` 才蓋(沿用 `import` 的兩道保護:
不得覆寫別人、不得覆寫正被其他程序載入的人格),`--persona` 可並存兩份。
* 加進 `OWNER_EXEMPT_SUBCOMMANDS`,否則已載入其他人格時會被 hook deny。
* 編號衝突:`nextCode()` 只掃本機,換機器會重複發號。新增
`nextCodeAcrossMachines()`,發號前先問遠端已經用掉哪些編號;Gitea 連不上
就退回本機答案並在輸出明講「只對過本機」。`create` 與 `code assign/next` 都改用它。
* 順手修正 `ensureRepo` 的建庫路由:`me` 取自 `resolveOwner()`,而它在有
`PERSONA_GITEA_OWNER` 時只會把那個值原封不動還回來,於是組織永遠走成
`/user/repos`(建到 token 本人底下)。改用不受該環境變數影響的 `giteaLogin()`。
測試:selftest 新增第 ㉒ 區,用 file:// 的裸倉庫當「假的 Gitea」跑完整往返
(推兩區 → 刪掉本機人格 → clone 回來 → 驗身分/長期記憶/活狀態/索引/關係圖),
並涵蓋前兩個修正(子資料夾與非 ASCII 檔名真的進了存取庫、push 覆蓋遠端的回報
與 Stop hook 只吵一次)。345 → 373 項全過。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:35:29 +00:00 |
|
 jiantw83andClaude Opus 5
|
5d27051293
|
fix(gitea): push 撞到別台機器時是本機贏,但不能靜靜地贏
`pushArea` 遇到 non-fast-forward 時會 `git reset --hard origin/<branch>`、
把本機工作副本重新疊上去再推一次,然後回 `{ok:true, changed:true}`——
沒有任何衝突訊號。而 Stop hook 每一輪都在背景 push,所以兩台機器同時聊同一個
人格時,對方的 emotion.json/short-term.jsonl 會被靜默取代,誰都不知道。
「本機工作副本是這台機器的真相來源」這個設計選擇保留,但那條路徑現在要記帳:
* 算出「對方在分歧後改過、而我們正要蓋掉」的檔案交集,連同覆蓋前的遠端 sha
一起回傳 `overwrote`,並寫進 state/sync.json(留最近 10 筆)。
* `sync push` 一律往 stderr 寫一行警告(--quiet 也寫,背景 push 才有痕跡),
正常輸出與 `sync status` 都列得出「蓋掉幾個檔案、上一版是誰」。
* 背景 push 是 detached、輸出丟掉的,所以由 Stop hook 認領未回報的紀錄,
講給使用者聽一次(劇場模式也照講——那是資料被蓋掉)。
* commit 訊息也寫進「覆蓋 N 個檔案,上一版 <sha>」,被蓋掉的內容仍可用
`git -C <人格>/.sync/<區> show <sha>:<檔案>` 取回。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:34:29 +00:00 |
|
 jiantw83andClaude Opus 5
|
b937c9ad7a
|
fix(gitea): 同步會漏掉子資料夾與非 ASCII 檔名的檔案
兩個各自獨立、但都是「檔案在本機有、在存取庫沒有/拉不回來」的靜默失真:
1. `listAreaFiles` 只收資料夾第一層的 `entry.isFile()`,所以
`mindmap/threads/archive/`(睡眠時把太久沒動的思維導圖收進去的地方)
從來沒有被同步過。改成遞迴。
2. `git ls-files` / `status --porcelain` / `diff --name-only` 預設會把
非 ASCII 路徑引號跳脫成 `"Memory-\350\267\250…"`。長期記憶的檔名正好是中文的,
於是這些檔名跟工作副本比對不上:pull 時被當成「clone 裡沒有的東西」整批略過
(拉回一個沒有長期記憶的人格),本機刪掉的也不會從遠端消失。
一律改用 `-z`,並把三處解析收斂成 `diffPaths()` / `statusPaths()`。
順便修掉 porcelain 第一筆因為整體 trim 而少一個字元的解析錯誤。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:32:49 +00:00 |
|
jiantw83
|
186de10179
|
fix(security): 人格檔案不能逸出注入區塊、room 台詞不能偽造成系統訊息(S6)
注入到上下文的東西夾在 persona-runtime / persona-context / persona-ops 三種區塊
中間,而夾進去的內容有**不可信來源**:persona-anime 從 Fandom 抓設定寫進
IDENTITY/AGENTS、`sync pull` 從另一台機器拉、`import` 吃外部 bundle、
guest 的 room 台詞是別的人格寫的。原本這些地方**沒有任何跳脫**:
- AGENTS.md 裡放一行結束標記 → opsBrief 的區塊提早關閉,後面的內容跑到區塊外,
連外層的 runtime 區塊都能一起關掉。
- IDENTITY.md 的 `Vibe:` 欄位值同理,經 identityBrief 進 turnContext。
- room 台詞塞換行 → roomScript 是一行一句「emoji 名字(情緒):內容」,
於是可以偽造成別人的台詞或系統訊息。
修法:
- 新增 `stripInjectionMarkers()`:把 `<persona-…` 的 `<` 換成全形。內容還讀得懂
(人格自己寫的說明不會被吃掉),但它不再是一個標籤。
- 新增 `injectSafeLine()`:中和標記 + 換行壓成空白(比照短期記憶的作法)。
- 一個收口勝過十幾個防點:`turnContext()` 與 SessionStart 的 runtime 區塊都改成
**組完之後對整個內文**做一次,再補上真正的標記;只有 turnContext/opsBrief
這種自己已處理過、帶合法巢狀標記的整塊原樣保留。
- 讀出來就中和的:`opsBrief()` 的 AGENTS.md 全文(先截斷再中和,長度上限才算得準)、
`identityFields()` 的欄位值(identityBrief/roomScript/roomDisplayName 全吃這一份)、
`relationsBrief()` 的人名與備註。
- room:`roomPost()` 在**寫入端**就把 text/emotion/barge_in 壓成一行,
`roomScript()` 與 `room read` 在**顯示端**再壓一次(舊逐字稿是原文寫進去的)。
測試:新增 14 項——AGENTS.md 與 IDENTITY 欄位的逸出、turnContext 與 SessionStart
的區塊只被關閉一次、短期記憶與關係圖人名走同一個收口、room 台詞的換行偽造與
標記逸出、舊逐字稿的顯示端防線、顯示名不夾帶標記。
反向驗證:把兩個中和函式改成 identity,這 14 項全數失敗。
368 → 383 項全過。
|
2026-07-31 09:31:41 +00:00 |
|
 jiantw83andClaude Opus 5
|
197ad3416f
|
fix(speech): 修掉三個誤殺——書名號、變體選擇子 emoji、引述算進句長
三個都是「該放行卻被擋」,而且都只有幾行:
* speechBody() 的「提及 ≠ 使用」豁免清單漏了《》:
「我在看《說到底》這本書」被判 preach,「他推薦我看《質量與信息》」被判中國用語。
* EMOJI_RE 把 \u{FE0F} 寫在字元類裡,`❤️`(U+2764 + VS16)被算成兩個 emoji 而誤擋;
ZWJ 家族(👨👩👦)算三個、膚色(👍🏽)算兩個。改成「底字 + 變體/膚色 + ZWJ 續接」算一個。
* 句長檢查用的是 sentence 而不是 body,引號內容與 `code` 都算進字數——
引述使用者原話必被擋,等於不准引述。
「質量」「信息」這類語境相依的中國用語誤判沒有動(那要語意判斷,成本不對)。
selftest +11:SNF 放行 7 條(書名號兩種、三種 emoji 組合、引述長原話與長 code),
SF 仍擋 4 條(書名號之外真的在說教、引號之外自己講的長句、兩個 emoji、
一個 emoji 加一個帶變體選擇子的)——豁免只針對提及與引述,不是整句放行。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:29:53 +00:00 |
|
 jiantw83andClaude Opus 5
|
94a86c9703
|
fix(emotion): loadEmotion 驗證檔案內容,衰減一律 clamp 且半衰期必須為正
emotion.json 是外部輸入(import/sync pull/手改都會進到這裡),但 loadEmotion()
用 `??=` 只補缺、不驗合法性,decayEmotion() 完全不 clamp。只要一份不合法的檔案:
* baseline.joy = 5000 → decay 後 joy = 4378.79,mood 卡在 valence 100 / arousal 100
* levels.anger = "很生氣" → decay 後 anger = NaN,注入的上下文印出 valence NaN
* half_life_minutes.joy ≤ 0 → factor 恆為 0,那個情緒從此累積不起來(連三次 +30 結果不變)
CLI 的 --baseline 與 --apply 本來就有保護,破口純粹在檔案入口,所以擋在 loadEmotion():
levels/baseline clamp 到 0–100、非有限數字退回預設、half_life 非正數退回該情緒的預設值。
decayEmotion/decayEmotionBy 抽出共用的 applyDecay(),同樣重驗一次並 clamp 結果——
它們是 export 的,呼叫端有可能餵手寫或匯入來的狀態,不能假設一定經過 loadEmotion()。
selftest +9:baseline 超界被 clamp、非數字不變 NaN、上下文不印 NaN、
四種壞掉的半衰期都退回預設、壞半衰期下情緒仍累積得起來、decayEmotionBy 同樣擋得住。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:26:47 +00:00 |
|
 jiantw83andClaude Opus 5
|
1d5b049152
|
fix(consolidate): --forget 套上短期記憶的保護;長期記憶撞名不再靜默覆蓋
兩個資料遺失路徑:
① `--forget N` 無條件刪掉 salience < N 的全部短期記憶,完全不套 shortTermProtected()
(salience ≥ 80/intent=commit/24 小時內)。prune 有這層保護,這裡沒有。
實測 `--forget 200` 把一筆 intent=commit、salience 95、剛寫入的承諾一起刪掉,
而 R4 明文寫「不可遺忘」。改走新的 forgetShortTerm(),跟 prune 同一層保護,
並回報「有幾筆低於門檻但受保護、沒有刪」。
② slugify() 把標點吃掉:「我的貓」「我的貓?」「我的貓!!」全都落在 `我的貓.md`,
writeText 直接覆蓋,只保留舊檔的 first_seen/recall_count,內文靜默被換掉。
front matter 新增 `title:`(原本的 --name),撞名時 die 並給一個沒被占用的檔名
(`--name 我的貓-2`);真的要覆蓋同一則就加 --force。
selftest +11:--forget 不刪承諾/界線/今天的紀錄但該刪的照刪、撞名被擋、
舊內文原封不動、訊息給得出替代檔名、同名再寫是更新、title 有寫入、--force 仍可覆蓋。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:23:35 +00:00 |
|
jiantw83
|
7549c0b69f
|
fix(guard): 補上 Glob/Grep 的兩個自然破口,並誠實標示 guard 的定位(S2)
`extractPaths()` 只解析「含 / 且展開後包含 home 或 personas」的 token,
於是兩種**模型最自然會寫出來的列舉方式**整路穿過去:
1. `PATH_TOOL_FIELDS.Glob` 只列 `path`,所以樣式欄位不被檢查——
`Glob { pattern: "<home>/*/IDENTITY.md" }`(不給 `path`)可以掃出全部人格的身分檔。
新增 `PATTERN_TOOL_FIELDS`(`Glob.pattern`、`Grep.glob`),且**相對於 `path` 解析**
(沒給 `path` 才相對 cwd),因為樣式的基準點跟路徑欄位不同。
`Grep.pattern` 是正規表示式、不是路徑,故意不收,免得誤攔 `a/b/c` 這種樣式。
2. `Grep`/`Glob` 沒給 `path` 時 `extractPaths` 回空陣列 → guard 不表態,
於是 cwd 站在 `~/.claude/personas` 或別人的人格底下直接 `Grep` 就整批穿過去。
改成把 hook event 的 `cwd` 當預設目標。
其餘的直譯器逃逸(`node -e`、`python3 -c`、逐段 `cd`、引號切割 token)**不用正則補**:
Bash 圖靈完備,追指令字串永遠落後一步,每加一條正則就多一批誤攔正常指令的風險。
改為把文件的措辭修正成誠實的定位——
- README 新增〈guard 擋得住什麼、擋不住什麼〉:明說 guard 是**防漂移的護欄,
對正常寫法一律 deny,不是對抗性沙箱**,並逐條列出擋得住與擋不住的形式,
以及「真要對抗性隔離請走 OS 層」。
- 拿掉會誤導的字眼:`PreToolUse` 那列的「唯一強制點」、guard.mjs 檔頭的
「唯一的強制執行點」、「不能被繞過的關鍵」。
測試:新增 13 項——樣式欄位指向全倉庫/別的人格/自己、樣式相對 path 解析、
`Grep.pattern` 不誤判成路徑、不給 path 時 cwd 在別人格/倉庫根/自己人格/
普通專案的四種情形、給了 path 就不看 cwd,以及兩項文件措辭的迴歸檢查。
355 → 368 項全過。
|
2026-07-31 09:23:28 +00:00 |
|
 jiantw83andClaude Opus 5
|
d9f865d7a4
|
fix(persona-lib): 檔案層互斥鎖,read-modify-rewrite 不再吃掉並行的 append
short-term.jsonl / said.jsonl 的裁切是「整檔讀進來 → 過濾 → writeText 覆蓋」,
中間沒有任何鎖。而同一個 session 的 sub agent 與主程序共用同一把人格鎖(設計如此),
所以兩邊真的會同時寫——實測背景 prune 進行中 append 30 筆 salience 95 的承諾,
會被吃掉 1~9 筆,正是 shortTermProtected() 明文要保護的那一類。
emotion.json 更嚴重:並行 8 次 `--apply joy=+5`,循序得 50.24,並行只得 38.91(增量遺失 45%)。
加一把用 `fs.openSync(path, "wx")` sentinel 做的檔案鎖:
* withFileLock(file, fn):拿不到就退讓重試,超過 15 秒的殘留鎖視為死鎖並接手,
真的等不到就直接做(寧可冒一次競態,也不要因為殘留鎖檔讓人格從此寫不進東西)。
sentinel 放 .runtime/locks/,不落在人格目錄裡,不會被同步上去。
* rewriteJsonl(file, transform):整檔改寫的唯一入口,讀與寫都在鎖裡。
* updateJson / updateEmotion:JSON 檔的 read-modify-write 同樣進鎖。
* appendJsonl 也拿同一把鎖,否則 append 仍會落在別人的讀與寫之間被覆蓋掉。
改用新入口的:pruneShortTermDetail、trimSaid、trimJsonl、turnContext 的情緒衰減、
persona.mjs 的 `emotion --apply`(讀→衰減→套用→寫回整段在鎖裡)。
selftest +5:並行 append vs prune 一筆不掉、並行 emotion 與循序同值、
withFileLock 互斥(12 程序各加一次=12)、過期鎖檔可接手、鎖檔不落在人格目錄。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 09:19:39 +00:00 |
|
jiantw83
|
b1e77b01a1
|
fix(security): sleeper 旗標不再是任意人格的萬用鑰匙(S1)
問題:`requireOwner()` 看到 sleeper 旗標就直接 return,把驗證整個外包給
PreToolUse hook;而 hook 認得出這支 CLI 靠的是檔名正則。把 scripts/ 複製出去、
CLI 改個名字,hook 全程回 pass,任何程序都能用那個旗標對**任意人格**
remember/recall/emotion --apply——等於完整讀寫權。
修法:CLI 自己驗,證據取自 hook 唯一寫得下、程序偽造不了的東西——
session 檔(`.runtime/sessions/<id>.json`)裡的 sleeper pin。pin 由 hook 依
`event.agent_type` 寫入,沒經過 hook 的程序拿不到。
- `pinAgent()` 多記角色(`{ persona, role, pinned_at }`),`pinOf()` 相容舊的純字串格式;
舊格式沒有角色,一律不算 sleeper 授權,hook 下次 first-touch 時會補上。
- 新增 `sleeperPins(sessionId, slug)`:本 session 中 pin 在該人格上的 sleeper。
- `requireSleeperPin()` 進 `sleeperAccess()`(requireOwner/requireMember 都會經過):
沒有對應的 sleeper pin 就 die,連 sleeper 租約都不會留下。
有帶 `--agent-id` 時額外要求它就是那個被 pin 的 agent。
- 連帶更新讀 pins 的三處:resolveScope、`leave` 的 guest pin 清理、SubagentStop hook。
persona-sleeper sub agent 的正常流程不受影響:hook 的 first-touch pinning 在它
第一個指令就寫好 pin,之後每個收尾指令都驗得過。
測試:新增 10 項(實際把 scripts/ 複製成別的檔名重現攻擊)——改名後 hook 確實
不表態、無 pin 時 remember/recall/emotion 都被 CLI 擋下且不留租約、pin 在別的
人格上不能跨過去、guest pin 與舊格式 pin 都不算授權、真有 pin 時照常放行。
345 → 355 項全過。
|
2026-07-31 09:19:20 +00:00 |
|