feat(hooks): SessionStart 注入人格的 AGENTS.md 操作規則

新增 pl.opsBrief(slug):把該人格的 AGENTS.md 全文包成 <persona-ops> 區塊,
由 SessionStart hook 在「接續人格」與「預設人格自動載入」兩條分支各注入一次。

刻意不放進 turnContext——操作規則是低頻的「怎麼做事」,每輪重貼只是燒 context。
超過 OPS_BRIEF_MAX_CHARS(6000)會截斷並指路回原檔;檔案不存在時回空字串。

這讓「這個人格自己的工具箱」可以寫在 AGENTS.md 裡跟著人格走(AGENTS.md 本來
就在 sync 的 wiki 白名單內),不必落在機器本機的 ~/.claude/agents。

selftest 補 5 條:全文注入、指路原檔、不進 turnContext、檔案不存在回空、
超長截斷。全套 345 項通過。
This commit is contained in:
2026-07-31 08:34:38 +00:00
parent 0527a3cad0
commit 721fc0ece8
3 changed files with 70 additions and 0 deletions
+32
View File
@@ -1733,6 +1733,9 @@ export function sentenceCount(text) {
export const MAX_SENTENCE_CHARS = 45;
// AGENTS.md 開機注入的上限:超過就截斷並指路回原檔,不要一份規則吃掉整個 context。
export const OPS_BRIEF_MAX_CHARS = 6000;
// 講完就算了:這些開頭都是在解釋自己剛才講的話,真的在講話的人不會這樣。
export const EXPLAINER_OPENERS = [
"我的意思是", "我是想說", "換句話說", "換個說法", "也就是說", "換言之",
@@ -3082,6 +3085,35 @@ export function identityBrief(slug) {
return order.filter((k) => fields[k]).map((k) => `${k}: ${fields[k]}`).join("");
}
/**
* SessionStart 注入的操作規則:AGENTS.md 全文。
*
* 只在開機注入一次(不進 turnContext)——它是低頻的「怎麼做事」,
* 每輪重貼只是把 context 燒掉。內容太長就截斷並指路回原檔。
*/
export function opsBrief(slug) {
let text;
try {
text = fs.readFileSync(path.join(personaDir(slug), "AGENTS.md"), "utf8").trim();
} catch {
return "";
}
if (!text) return "";
const file = path.join(personaDir(slug), "AGENTS.md");
let body = text;
if (body.length > OPS_BRIEF_MAX_CHARS) {
body = body.slice(0, OPS_BRIEF_MAX_CHARS) + `\n\n(後略;全文見 ${file}`;
}
return [
"<persona-ops>",
`以下是 \`${slug}\` 的操作規則(${file}):這輪開機注入一次,之後不再重複貼。`,
"要重看用 `persona.mjs show --what agents --session <id>`;改了就直接編輯那個檔案。",
"",
body,
"</persona-ops>",
].join("\n");
}
/** UserPromptSubmit 注入的人格上下文:身分 + 情緒 + 短期記憶 + 相關長期記憶 + 關係。 */
export function turnContext(slug, sessionId, prompt = "") {
const state = decayEmotion(loadEmotion(slug));
+25
View File
@@ -81,6 +81,31 @@ check("IDENTITY 五欄位可解析", pl.identityBrief("alpha").includes("Name: A
check("SOUL/AGENTS/USER 都有產生",
["SOUL.md", "AGENTS.md", "USER.md"].every((f) => fs.existsSync(path.join(pl.personaDir("alpha"), f))));
// 操作規則(AGENTS.md)只在開機注入一次:人格自己的工具箱寫在裡面就會跟著人格走。
check("opsBrief 帶回 AGENTS.md 全文", (() => {
const ops = pl.opsBrief("alpha");
return ops.includes("<persona-ops>") && ops.includes("</persona-ops>") && ops.includes("每輪對話要做的事");
})(), pl.opsBrief("alpha").slice(0, 200));
check("opsBrief 指得出原檔路徑", pl.opsBrief("alpha").includes(path.join(pl.personaDir("alpha"), "AGENTS.md")));
check("opsBrief 不進 turnContext(開機一次就好)",
!pl.turnContext("alpha", S_HOST, "在嗎").includes("<persona-ops>"));
check("AGENTS.md 不存在時 opsBrief 回空字串", (() => {
const file = path.join(pl.personaDir("omega"), "AGENTS.md");
const backup = fs.readFileSync(file, "utf8");
fs.rmSync(file);
const empty = pl.opsBrief("omega");
fs.writeFileSync(file, backup);
return empty === "";
})());
check("opsBrief 超長會截斷並指路", (() => {
const file = path.join(pl.personaDir("omega"), "AGENTS.md");
const backup = fs.readFileSync(file, "utf8");
fs.writeFileSync(file, "劍".repeat(pl.OPS_BRIEF_MAX_CHARS + 500));
const ops = pl.opsBrief("omega");
fs.writeFileSync(file, backup);
return ops.includes("(後略;全文見") && ops.length < pl.OPS_BRIEF_MAX_CHARS + 500;
})());
console.log("② 人格鎖:一個人格只能被一個程序載入");
check("建立時即取得鎖", pl.lockStatus("alpha").locked);
check("同 session 重入成功", cli(["load", "--persona", "alpha", "--session", S_HOST]).status === 0);