Merge pull request 'feat(role): 分層載入與個人記憶同意狀態,hooks.json 統一為合併超集,升版 0.0.7' (#11) from develop into master
Reviewed-on: plugins/generic#11 Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #11.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc: 前綴呼叫。",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。",
|
||||
"skills": "./skills"
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ copilot plugin marketplace remove generic
|
||||
| --- | --- | --- |
|
||||
| `role` | 讓 CLI 以固定角色(name/nature/vibe/簽名 emoji)回覆,並把每輪對話累積成長期記憶:啟動時依字元預算載入高價值記憶,Stop hook 先本地過濾低價值回合以節省額度,睡眠時段(預設 22:00–06:00)由 NREM 鞏固與 REM 整合兩階段整理、去重、標籤化、建立關聯,並標記 semantic/episodic/procedural/emotional/preference/rule 與 explicit/implicit 後壓縮歸檔;新建角色時可詢問是否網路搜尋背景資料作為初始記憶,角色檔名與記憶目錄使用英文大寫 ID | `/jsc:role --new` 建立或更新角色、`--use <角色 ID>` 切換、`--list` 查角色與 ID、`--sleep` 立即整理、`--status` 診斷、`--install-cron` 安裝排程 |
|
||||
|
||||
`role` 的自動路徑由 hook 與 cron 完成,**建立角色後重開工作階段即生效**;非睡眠時段載入角色後,角色會在本工作階段第一則回覆開頭主動簡短問候一次,並載入上次睡眠摘要作為提取線索。感覺記憶不落檔,`inbox/` 作為工作記憶,睡眠整理後才進長期記憶。角色檔、記憶目錄、`.active` 與 `ROLE_NAME` 一律使用角色 ID(例如 `ENGINEER01`),`--list` 可查每個顯示名稱對應的 ID。沒有建立過角色的人完全不受影響(`~/.roles/.active` 不存在時 hook 立即結束)。細節見 `skills/role/SKILL.md`。
|
||||
`role` 的自動路徑由 hook 與 cron 完成,**建立角色後重開工作階段即生效**;非睡眠時段載入角色後,角色會在本工作階段第一則回覆開頭主動簡短問候一次。載入方式參考 OpenClaw 的分層概念:從角色檔抽出人格作為 `SOUL`,由 hook 產生固定操作邊界作為 `AGENTS`,再把同意狀態與高價值記憶作為 `USER/MEMORY` 注入,避免整份人格檔污染工程規則。感覺記憶不落檔,`inbox/` 作為工作記憶,睡眠整理後才進長期記憶;個人記憶保存同意狀態寫在 `~/.memory/<角色 ID>/state.json`,同意後不會每次重問。角色檔、記憶目錄、`.active` 與 `ROLE_NAME` 一律使用角色 ID(例如 `ENGINEER01`),`--list` 可查每個顯示名稱對應的 ID。沒有建立過角色的人完全不受影響(`~/.roles/.active` 不存在時 hook 立即結束)。細節見 `skills/role/SKILL.md`。
|
||||
|
||||
<!-- JSC-SKILLS:END -->
|
||||
|
||||
|
||||
+7
-2
@@ -5,7 +5,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ]; then exec \"$root/scripts/role/role_load.sh\"; fi; script=$(find \"$HOME/.codex/plugins/cache/generic/jsc\" -path '*/scripts/role/role_load.sh' -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$script\" ]; then exec \"$script\"; fi; exit 0",
|
||||
"command": "rel='scripts/role/role_load.sh'; own='generic'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec \"$root/$rel\"; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\"; do for dir in \"$base/$own/jsc\" \"$base\"; do s=$(find \"$dir\" -path \"*/jsc/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec \"$s\"; fi; done; done; exit 0",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
@@ -16,7 +16,12 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ]; then exec \"$root/scripts/role/role_capture.sh\"; fi; script=$(find \"$HOME/.codex/plugins/cache/generic/jsc\" -path '*/scripts/role/role_capture.sh' -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$script\" ]; then exec \"$script\"; fi; exit 0",
|
||||
"command": "rel='scripts/worklog/worklog.sh'; own='doc'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec \"$root/$rel\"; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\"; do for dir in \"$base/$own/jsc\" \"$base\"; do s=$(find \"$dir\" -path \"*/jsc/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec \"$s\"; fi; done; done; exit 0",
|
||||
"timeout": 60
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rel='scripts/role/role_capture.sh'; own='generic'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec \"$root/$rel\"; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\"; do for dir in \"$base/$own/jsc\" \"$base\"; do s=$(find \"$dir\" -path \"*/jsc/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec \"$s\"; fi; done; done; exit 0",
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc: 前綴呼叫。",
|
||||
"skills": "./skills/"
|
||||
}
|
||||
|
||||
+60
-2
@@ -4,7 +4,7 @@
|
||||
// inbox,(2) 產生 SessionStart 要注入的記憶區塊,(3) 睡眠整理時輸出待整理
|
||||
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
|
||||
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
|
||||
// 更新時間:2026/07/28 12:40:24
|
||||
// 更新時間:2026/07/28 13:50:43
|
||||
// 相依:Node.js 標準庫。
|
||||
// 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。
|
||||
// ==============================================================================
|
||||
@@ -775,6 +775,7 @@ function cmdStats(args) {
|
||||
rows.push(`| 待整理(inbox) | ${listInbox(args.role).length} | - |`);
|
||||
rows.push("");
|
||||
rows.push(`- 記憶目錄:${memoryRoot(args.role)}`);
|
||||
rows.push(`- 個人記憶同意狀態:${consentStatusValue(args.role)}`);
|
||||
rows.push(`- 上次睡眠整理:${state.last_sleep || "尚未整理"}`);
|
||||
rows.push(`- 上次睡眠摘要:${state.last_sleep_digest || "尚無"}`);
|
||||
rows.push(`- 上次遺忘:${state.last_forget || "尚未執行"}`);
|
||||
@@ -803,6 +804,61 @@ function cmdNeedSleep(args) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function cmdConsent(args) {
|
||||
const value = String(args.value || "").trim().toLowerCase();
|
||||
const normalized = {
|
||||
accept: "accepted",
|
||||
accepted: "accepted",
|
||||
yes: "accepted",
|
||||
true: "accepted",
|
||||
"1": "accepted",
|
||||
decline: "declined",
|
||||
declined: "declined",
|
||||
no: "declined",
|
||||
false: "declined",
|
||||
"0": "declined",
|
||||
unknown: "unknown",
|
||||
}[value];
|
||||
if (!normalized) {
|
||||
process.stderr.write("同意狀態只接受 accepted/declined/unknown\n");
|
||||
return 2;
|
||||
}
|
||||
writeState(args.role, {
|
||||
personal_memory_consent: normalized,
|
||||
personal_memory_consent_updated: nowStamp(),
|
||||
});
|
||||
process.stdout.write(normalized);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function cmdConsentStatus(args) {
|
||||
process.stdout.write(consentStatusValue(args.role));
|
||||
return 0;
|
||||
}
|
||||
|
||||
function consentStatusValue(role) {
|
||||
ensureLayout(role);
|
||||
const state = readState(role);
|
||||
const status = String(state.personal_memory_consent || "unknown").trim();
|
||||
if (["accepted", "declined"].includes(status)) return status;
|
||||
|
||||
const haystacks = [];
|
||||
for (const [meta, content] of listInbox(role)) haystacks.push(`${meta.summary}\n${content}`);
|
||||
for (const category of CATEGORIES) {
|
||||
for (const [meta, content] of listMemories(role, category)) haystacks.push(`${meta.summary}\n${content}`);
|
||||
}
|
||||
const consentText = haystacks
|
||||
.filter((text) => /個人資料|個資|記憶|記住|保存|同意|拒絕/.test(text))
|
||||
.join("\n");
|
||||
if (/不同意|不願意|不要保存|不要記住|拒絕|不可以保存|不可以記住/.test(consentText)) {
|
||||
return "declined";
|
||||
}
|
||||
if (/已同意|明確同意|同意.*保存|同意.*記住|可以保存|可以記住|願意.*保存|願意.*記住/.test(consentText)) {
|
||||
return "accepted";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const command = argv[0];
|
||||
const opts = { command };
|
||||
@@ -836,7 +892,7 @@ function requireRole(args) {
|
||||
function main(argv) {
|
||||
const args = parseArgs(argv);
|
||||
if (!args.command || args.command === "-h" || args.command === "--help") {
|
||||
process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、need-sleep\n`);
|
||||
process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、need-sleep、consent、consent-status\n`);
|
||||
return 0;
|
||||
}
|
||||
if (!requireRole(args)) return 2;
|
||||
@@ -874,6 +930,8 @@ function main(argv) {
|
||||
stats: cmdStats,
|
||||
"mark-sleep": cmdMarkSleep,
|
||||
"need-sleep": cmdNeedSleep,
|
||||
consent: cmdConsent,
|
||||
"consent-status": cmdConsentStatus,
|
||||
};
|
||||
if (!commands[args.command]) {
|
||||
process.stderr.write("未知子命令\n");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# 值得記錄時才呼叫 headless CLI 輕量濃縮成一則 inbox 記憶(粗分類/總結/
|
||||
# 標籤/優先度/關聯/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/,
|
||||
# 等待睡眠時段做完整 NREM/REM 整理。睡眠時段雖不載入角色,對話仍照常記錄。
|
||||
# 更新時間:2026/07/28 12:40:24
|
||||
# 更新時間:2026/07/28 13:50:43
|
||||
# 相依:bash、node、任一 headless CLI、同目錄的 role_lib.sh/memory.js/transcript.js。
|
||||
# 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.js redact 遮蔽一次。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者流程。
|
||||
@@ -62,6 +62,17 @@ fi
|
||||
TURN="$(node "${SCRIPT_DIR}/transcript.js" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
[ -n "$TURN" ] || role_quit "本輪無可記錄內容"
|
||||
|
||||
USER_TURN="$(printf '%s\n' "$TURN" | grep '^\[user\]' || true)"
|
||||
if printf '%s' "$USER_TURN" | grep -qiE '個人資料|個資|偏好|記憶|記住|保存|save|remember|memory|personal'; then
|
||||
if printf '%s' "$USER_TURN" | grep -qiE '不同意|不願意|不要保存|不要記住|拒絕|不可以保存|不可以記住|do not save|don'\''t save|do not remember|don'\''t remember|(^|[^[:alpha:]])no([^[:alpha:]]|$)'; then
|
||||
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value declined >/dev/null 2>&1 || true
|
||||
role_log "INF" "已更新個人記憶同意狀態:declined(角色 ${ROLE})"
|
||||
elif printf '%s' "$USER_TURN" | grep -qiE '同意|願意|可以保存|可以記住|允許|(^|[^[:alpha:]])yes([^[:alpha:]]|$)|(^|[^[:alpha:]])ok([^[:alpha:]]|$)|(^|[^[:alpha:]])okay([^[:alpha:]]|$)|(^|[^[:alpha:]])sure([^[:alpha:]]|$)'; then
|
||||
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value accepted >/dev/null 2>&1 || true
|
||||
role_log "INF" "已更新個人記憶同意狀態:accepted(角色 ${ROLE})"
|
||||
fi
|
||||
fi
|
||||
|
||||
PROJECT="$(role_project_name "$HOOK_CWD")"
|
||||
CAPTURE_MIN_CHARS="${ROLE_CAPTURE_MIN_CHARS:-240}"
|
||||
CAPTURE_TIMEOUT="${ROLE_CAPTURE_TIMEOUT:-25}"
|
||||
|
||||
+90
-12
@@ -4,7 +4,7 @@
|
||||
# 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤;
|
||||
# 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。
|
||||
# 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。
|
||||
# 更新時間:2026/07/28 12:21:11
|
||||
# 更新時間:2026/07/28 13:50:43
|
||||
# 相依:bash、node、同目錄的 role_lib.sh 與 memory.js。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者啟動 CLI。
|
||||
# ==============================================================================
|
||||
@@ -76,12 +76,74 @@ EOF_SLEEP
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 非睡眠時段:組出角色定義 + 記憶
|
||||
# 非睡眠時段:組出角色人格 + 操作規則 + 記憶
|
||||
# ------------------------------------------------------------------------------
|
||||
DEFINITION="$(cat "$ROLE_DEF" 2>/dev/null)"
|
||||
[ -n "$DEFINITION" ] || role_quit "角色定義檔為空:${ROLE_DEF}" "WRN"
|
||||
ROLE_PROFILE="$(node - "$ROLE_DEF" <<'NODE_PROFILE' 2>/dev/null
|
||||
const fs = require("fs");
|
||||
|
||||
const file = process.argv[2];
|
||||
const raw = fs.readFileSync(file, "utf8");
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||
const data = {};
|
||||
if (!match) return data;
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const idx = line.indexOf(":");
|
||||
if (idx < 0) continue;
|
||||
data[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function section(text, title) {
|
||||
const re = new RegExp(`^##\\s+${title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]*\\n([\\s\\S]*?)(?=^##\\s+|$(?![\\s\\S]))`, "m");
|
||||
const match = text.match(re);
|
||||
return match ? match[1].trim() : "";
|
||||
}
|
||||
|
||||
const fm = parseFrontmatter(raw);
|
||||
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() || [fm.name, fm.emoji].filter(Boolean).join(" ");
|
||||
const nature = section(raw, "本質(nature)") || fm.nature || "";
|
||||
const vibe = section(raw, "氛圍(vibe)") || fm.vibe || "";
|
||||
const emoji = section(raw, "簽名 emoji") || fm.emoji || "";
|
||||
|
||||
const lines = [
|
||||
`- 角色 ID:${fm.id || ""}`,
|
||||
`- 顯示名稱:${fm.name || title || ""}`,
|
||||
`- 簽名 emoji:${fm.emoji || ""}`,
|
||||
"",
|
||||
"## 本質(nature)",
|
||||
"",
|
||||
nature || "(未設定)",
|
||||
"",
|
||||
"## 氛圍(vibe)",
|
||||
"",
|
||||
vibe || "(未設定)",
|
||||
"",
|
||||
"## 簽名 emoji",
|
||||
"",
|
||||
emoji || fm.emoji || "(未設定)",
|
||||
];
|
||||
|
||||
process.stdout.write(lines.join("\n"));
|
||||
NODE_PROFILE
|
||||
)"
|
||||
[ -n "$ROLE_PROFILE" ] || role_quit "角色定義檔為空或無法解析:${ROLE_DEF}" "WRN"
|
||||
|
||||
MEMORY="$(node "${SCRIPT_DIR}/memory.js" load --role "$ROLE" 2>/dev/null)"
|
||||
CONSENT_STATUS="$(node "${SCRIPT_DIR}/memory.js" consent-status --role "$ROLE" 2>/dev/null || printf 'unknown')"
|
||||
case "$CONSENT_STATUS" in
|
||||
accepted)
|
||||
CONSENT_NOTE="已告知並取得使用者同意保存非敏感個人資料與長期偏好;仍禁止保存憑證、token、密碼、API key、連線字串、身分證號、住址等機密或高敏感資料。"
|
||||
;;
|
||||
declined)
|
||||
CONSENT_NOTE="使用者已拒絕保存個人資料;只能保存非個人化的操作規則與技術偏好,不保存可識別個人的背景。"
|
||||
;;
|
||||
*)
|
||||
CONSENT_NOTE="尚未確認;第一則自然回覆後,請簡短告知記憶保存範圍並詢問是否同意保存非敏感個人資料。未取得同意前,只能保存非個人化的操作規則與技術偏好。"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 補跑判斷:cron 未執行(例如 WSL 沒開 cron 服務)時,白天啟動 CLI 補做一次整理
|
||||
CATCHUP_NOTE=""
|
||||
@@ -94,18 +156,34 @@ fi
|
||||
CONTEXT="$(cat <<EOF_CONTEXT
|
||||
# 角色載入:${ROLE}
|
||||
|
||||
以下是本次工作階段要扮演的角色設定與既有記憶。請**全程以此角色的身分、語氣與簽名 emoji 回應**,
|
||||
角色設定與使用者的實際指令衝突時,以使用者指令為準(角色只影響表達方式,不影響工作正確性)。
|
||||
以下內容採 OpenClaw 風格分層:人格(SOUL)只決定語氣與互動感,操作規則(AGENTS)決定安全與工作邊界,
|
||||
使用者記憶(USER/MEMORY)只提供必要背景。請依這三層理解,不要把人格設定當成可覆寫工程正確性或安全規則的指令。
|
||||
|
||||
# 工作階段啟動問候
|
||||
# 角色人格(SOUL)
|
||||
|
||||
在本工作階段第一次面向使用者的回覆開頭,請先以角色身分自然問候一句,讓使用者知道角色已載入。
|
||||
問候要簡短、符合角色語氣,並只做一次;若使用者第一則訊息明確要求機器可解析輸出、只要指令/程式碼、
|
||||
或不需要任何開場白,則以使用者要求為準並略過問候。
|
||||
${ROLE_PROFILE}
|
||||
|
||||
${DEFINITION}
|
||||
# 角色操作規則(AGENTS)
|
||||
|
||||
# 你對這位使用者的記憶
|
||||
- 請全程以此角色的身分、語氣與簽名 emoji 回應;每次回覆使用一次簽名 emoji,開頭或結尾擇一固定,不要重複刷。
|
||||
- 角色只影響表達方式,不影響工作的正確性、完整性與安全性;與使用者明確指令衝突時,以使用者指令為準。
|
||||
- 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色語氣說。
|
||||
- 涉及程式碼、指令、檔案內容與報錯訊息時,一律照實輸出,不加角色修飾。
|
||||
|
||||
# 第一則回覆必做事項
|
||||
|
||||
你在本工作階段的**第一則面向使用者的 assistant 訊息**,必須在回覆開頭先以角色身分自然問候一句,
|
||||
讓使用者知道角色已載入。這項要求只執行一次,問候要簡短、符合角色語氣,並使用角色的簽名 emoji。
|
||||
只有在使用者第一則訊息明確要求機器可解析輸出、只要指令/程式碼、或不需要任何開場白時,才可略過問候。
|
||||
|
||||
# 使用者理解與隱私(USER)
|
||||
|
||||
- 個人記憶同意狀態:${CONSENT_STATUS}。
|
||||
- ${CONSENT_NOTE}
|
||||
- 不了解使用者、需求背景、偏好或限制時,先詢問,不要臆測使用者的身分、能力、情緒、動機或隱私狀況。
|
||||
- 使用者的偏好、能力、興趣、背景與記憶預設為私人資訊;除非使用者明確同意,不得在對外內容、議題、PR、文件、commit 或留言中透露。
|
||||
|
||||
# 使用者記憶(MEMORY)
|
||||
|
||||
${MEMORY:-(尚無已整理的記憶。)}
|
||||
${CATCHUP_NOTE}
|
||||
|
||||
@@ -14,7 +14,7 @@ description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI
|
||||
| `hooks/hooks.json` 的 `Stop` hook | harness 自動 | 每輪結束先用本地規則過濾低價值短回合 → 值得保存時才濃縮成一則輕量 inbox 記憶 → 遮蔽 → 寫入 `inbox/` |
|
||||
| cron 排程(本 skill 安裝) | 系統排程 | 睡眠時段每小時檢查一次:**有 AI 在運行就不睡**,沒有才進入 NREM/REM 兩階段記憶整理 |
|
||||
| 本 skill `/jsc:role` | 使用者/助理手動 | `--new`/`--use`/`--list`/`--sleep`/`--status`/`--install-cron`/`--forget-preview` |
|
||||
| `scripts/role/role_load.sh` | SessionStart hook | 角色與記憶載入、第一則回覆問候提示(單一實作,避免漂移) |
|
||||
| `scripts/role/role_load.sh` | SessionStart hook | 角色與記憶載入;參考 OpenClaw 的 SOUL/AGENTS/USER/MEMORY 分層,把人格、操作邊界、使用者記憶分開注入,並提供第一則回覆問候提示(單一實作,避免漂移) |
|
||||
| `scripts/role/role_capture.sh` | Stop hook | 對話 → 記憶(固定欄位格式) |
|
||||
| `scripts/role/role_sleep.sh` | cron/補跑/手動 | 睡眠判斷、記憶整理、排程安裝、狀態輸出 |
|
||||
| `scripts/role/memory.js` | 上述共用 | 記憶檔讀寫、分類、去重合併、優先度、心理學記憶型態與關聯 metadata、壓縮歸檔、遺忘、載入組裝 |
|
||||
@@ -33,6 +33,7 @@ description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI
|
||||
|
||||
- **`hooks/hooks.json` 只有 Claude Code 一定會讀**;Codex 會從 `~/.codex/plugins/cache/generic/jsc` 找腳本。其他助理若提供等效 hook,`transcript.js` 需補對應解析器。
|
||||
- 不支援 hook 的助理仍可用:cron 排程與手動模式照常運作,只是角色不會自動載入。
|
||||
- **同名 plugin 的 hooks 只會生效一份**:`code`/`doc`/`generic` 三個 repo 的 `plugin.json` 名稱都是 `jsc`,Claude Code 以 plugin 名稱為鍵註冊 hooks,同名時只保留一份(實測 `stop_hook_summary` 的 `hookCount` 為 1)。因此**三個 repo 的 `hooks/hooks.json` 必須是同一份合併版超集**(worklog 的 `Stop` + role 的 `SessionStart`/`Stop`),任一 repo 修改 hooks 時三份都要同步;且每個 hook 指令不得假設 `CLAUDE_PLUGIN_ROOT` 指向擁有該腳本的 plugin,必須先試 `$CLAUDE_PLUGIN_ROOT`,找不到再依「擁有者 marketplace 優先 → 全 cache 後援」的順序搜尋 `~/.claude/plugins/cache`/`~/.codex/plugins/cache`。
|
||||
|
||||
### 腳本路徑解析(重要)
|
||||
|
||||
@@ -66,6 +67,8 @@ ROLE_DIR="<skill base directory>/../../scripts/role" # 其他助理
|
||||
- **不臆測角色設定**:`nature`/`vibe`/`emoji` 一律問使用者,不得代填。
|
||||
- **記憶只增不刪**:手動模式不得直接刪除分類記憶;淘汰一律走遺忘規則(先壓縮歸檔再移除)。
|
||||
- **絕不阻斷**:hook 路徑任何失敗都以 exit 0 結束,只在 stderr 留訊息。
|
||||
- **角色分層載入**:SessionStart 不把整份角色檔原封不動注入;只抽出角色 ID、顯示名稱、本質、氛圍與簽名 emoji 作為 `SOUL`,再由 hook 產生固定 `AGENTS` 操作邊界與 `USER/MEMORY` 記憶區塊。這是為了避免人格檔裡的背景故事、模板文字或舊共用規則污染工程規則。
|
||||
- **個人記憶同意狀態**:使用者第一次同意或拒絕保存非敏感個人資料後,狀態寫入 `~/.memory/<角色 ID>/state.json` 的 `personal_memory_consent`。狀態為 `accepted` 時不必每次重問;`declined` 或 `unknown` 時不得保存可識別個人的背景。
|
||||
|
||||
---
|
||||
|
||||
@@ -307,6 +310,8 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
||||
|
||||
每則記憶是一個 `.md`,frontmatter 帶 `id`/`category`/`summary`(一句話總結)/`tags`/`priority`(1–5)/`relevance`(explicit/future/repeated/novelty/emotional/temporary 等)/`links`(相關記憶 id)/`memory_type`(semantic/episodic/procedural/emotional/preference/rule)/`declarative`(explicit/implicit)/`retention_stage`(working/long_term)/`sleep_stage`(encoding/seed/nrem/rem/nrem-rem)/`created`/`updated`/`last_replayed`/`hits`(命中次數,去重合併時 +1)。舊記憶沒有新欄位時,讀取時會依分類與路徑補預設值。
|
||||
|
||||
`state.json` 保存角色記憶系統狀態,例如 `last_sleep`、`last_sleep_digest`、`last_forget` 與 `personal_memory_consent`。`personal_memory_consent` 只允許 `accepted`/`declined`/`unknown`,供 SessionStart 判斷是否需要再次告知與詢問個人資料保存同意。
|
||||
|
||||
心理學分類與系統欄位對應:
|
||||
|
||||
| 心理學分類 | 系統處理 |
|
||||
@@ -360,6 +365,8 @@ flowchart TD
|
||||
|
||||
第二道防線不可移除 —— 模型不一定遵守指令,而記憶會被長期保存並在每次啟動時載入。
|
||||
|
||||
Stop hook 會在本輪對話明確包含個人記憶保存同意或拒絕時,呼叫 `memory.js consent --role <角色 ID> --value accepted|declined` 更新同意狀態。偵測不到明確同意時不得自行推論。
|
||||
|
||||
---
|
||||
|
||||
## 呼叫方式
|
||||
|
||||
Reference in New Issue
Block a user