#!/usr/bin/env node // SessionStart:清理死鎖、續租,並把 PERSONA_SESSION 與人格清單注入上下文。 // // session_id 只有 hook 拿得到,所以這裡把它注入上下文;後續 skill 呼叫 CLI 時 // 必須帶 `--session <這個值>`,guard hook 會驗證,程序無法冒用別人的身分。 // // 人格「由使用者呼叫才載入」:這裡只列出可用人格,不自動載入沒綁過的人格。 import { readEvent, respond } from "./_hook.mjs"; import * as pl from "../scripts/persona-lib.mjs"; const event = readEvent(); if (!event) process.exit(0); const sessionId = event.session_id || "unknown"; const source = event.source || "startup"; const cwd = event.cwd || null; pl.gcRuntime(); const data = pl.loadSession(sessionId); const host = data.host; // 使用者可以指定「預設人格」(`persona.mjs default --persona `):那是他明示的選擇, // 不是我們自己挑的,所以只有在他設過的時候才自動載入;沒設定就維持原本的「等他指定」。 const wanted = host ? null : pl.defaultPersona(); const lines = [ "", `PERSONA_SESSION=${sessionId}`, `人格倉庫:${pl.personaHome()}`, "規則:", ` 1. 呼叫 persona.mjs 一律帶 \`--session ${sessionId}\`(值不符會被 hook 拒絕)。`, wanted ? " 2. 使用者已設定**預設人格**(見下方):那是他明示的選擇,直接以它的身分對話;除此之外不要自己挑人格附身。" : " 2. 人格**只有使用者叫你載入時才載入**(/jsc-persona:persona-chat ),不要自己挑一個人格附身。", " 3. 一個程序只能載入一個人格;要與別的人格對話請用 /jsc-persona:persona-invite。", " 4. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。", ]; /** * 把 AGENTS.md(操作規則)注入一次。 * * 放這裡而不是 turnContext:它是低頻的「怎麼做事」,開機讀一次就夠, * 每輪重貼只是浪費 context。人格自己的工具箱(例如六把劍)寫在裡面就會跟著人格走。 */ function pushOps(slug) { const ops = pl.opsBrief(slug); if (ops) lines.push(ops); } /** 列出可用人格,讓使用者挑(沒有預設人格、或預設人格載入失敗時用)。 */ function listAvailable() { const personas = pl.listPersonas(); if (!personas.length) { lines.push("尚無任何人格。可用 /jsc-persona:persona-create 或 /jsc-persona:persona-anime(動漫作品+角色名)建立。"); return; } lines.push("可用人格(等使用者指定):"); for (const slug of personas) { const status = pl.lockStatus(slug); lines.push(` - ${status.locked ? "🔒" : status.stale ? "⚠" : "🔓"} \`${slug}\` ${pl.identityBrief(slug)}`); } lines.push("載入方式:/jsc-persona:persona-chat "); } if (host && pl.personaExists(host)) { // resume / compact / clear:把鎖續租回來,並帶回人格狀態 try { pl.acquireLock(host, sessionId, { cwd }); lines.push(`已接續人格 \`${host}\`(session 恢復:${source})。`); lines.push(pl.turnContext(host, sessionId)); pushOps(host); } catch (err) { lines.push(`⚠ 無法接續人格 \`${host}\`:${err.message}`); } } else if (wanted && !pl.personaExists(wanted)) { lines.push(`⚠ 預設人格 \`${wanted}\` 不存在(設定於 ${pl.homeSettingsPath()})。請告訴使用者,並問他要改設定還是改載別的。`); listAvailable(); } else if (wanted) { // 使用者設過預設人格 → 自動載入。拿不到鎖時只回報,**不**自作主張 --takeover。 try { pl.acquireLock(wanted, sessionId, { cwd }); pl.bindHost(sessionId, wanted, { cwd }); pl.pruneShortTerm(wanted); pl.rebuildIndex(wanted); lines.push(`已自動載入預設人格 \`${wanted}\`(使用者設定,來源:${process.env.PERSONA_DEFAULT ? "PERSONA_DEFAULT" : pl.homeSettingsPath()})。`); lines.push("請照 /jsc-persona:persona-chat 的每輪流程走(語意分析→情緒→回想→3 句內回覆→記憶回寫)。"); lines.push("提醒:這次是本機自動載入,沒有從 Gitea 拉最新狀態;若可能在別台機器動過,先 `sync pull`。"); lines.push(pl.turnContext(wanted, sessionId)); pushOps(wanted); } catch (err) { lines.push(`⚠ 預設人格 \`${wanted}\` 自動載入失敗:${err.message}`); lines.push("不要自作主張 `--takeover`——先把 owner 與最後心跳告訴使用者,讓他決定。"); listAvailable(); } } else { lines.push("尚未載入人格。"); listAvailable(); lines.push("想每次開機就自動載入某個人格:`persona.mjs default --persona --session `。"); } lines.push(""); respond({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: lines.join("\n"), }, suppressOutput: true, });