feat(role): 精確資訊逐字保留、晨間狀態檢查、排除 skill 注入內容

三件事都源自 2026/07/28-29 實際踩到的問題。

一、整理與濃縮提示詞加「精確資訊逐字保留」規則
原本 role_sleep.sh 只要求「壓縮成 5 行、不超過 400 字」,沒有任何規則保護精確資訊,
導致一則含檔案路徑與網址的記憶被整理成純情感摘要,路徑與網址全部遺失且無法復原。
- role_sleep.sh 新增第 19 條:檔案路徑、網址、指令、環境變數名稱、版本號、識別碼、檔名
  一律逐字保留,不得摘要改寫;必要時可超過字數上限;憑證與個資的禁令仍優先
- role_capture.sh 第 8 條同步強化,並釐清與第 7 條「不要整段抄程式碼」的界線

二、新增 --brief 晨間狀態檢查
睡眠時段結束的整點執行使用者自訂檢查腳本,把有變化的結果寫成一則 daily 記憶,
讓角色當天第一次互動就能主動回報(例如 PR 還沒合併、CI 失敗),不必等使用者開口才查。
- 刻意不內建任何檢查邏輯,不假設使用者用 Gitea/GitHub:檢查內容放 ~/.roles/<角色>.checks/*.sh
- 目錄不存在時完全不動作,也不安裝排程條目,對沒設定的人零影響
- 只執行有 +x 的 *.sh;無執行權限記警告並略過
- 沒有輸出就不寫記憶(靜默即代表一切正常,不打擾使用者)
- 每個腳本受 ROLE_BRIEF_TIMEOUT 限制,輸出受 ROLE_BRIEF_EACH_LIMIT/ROLE_BRIEF_LIMIT 截斷
- 腳本輸出視為外部資料,寫入前一律經 transcript.js redact 遮蔽
- install_cron/remove_cron/show_status 一併支援;附 examples/check-gitea-prs.sh 範例

三、對話交接排除 skill 等注入內容
實測發現載入一個 skill 會插入一筆 isMeta 的 user 訊息(長度可達兩萬字元),
被誤認為使用者發言,既吃光字元預算也讓輪數計算失真(29 輪虛胖,實際 26 輪)。
- transcript.js 新增 isMetaEntry():以 isMeta 與 sourceToolUseID 判斷注入內容
- isRealUserMessage() 與 dialogLines() 皆排除,連帶讓 Stop hook 的本輪抽取更準確

驗證:晨間檢查涵蓋有輸出/無輸出/無執行權限/逾時/含機密五種腳本,
確認遮蔽生效、逾時腳本未納入、無權限腳本未執行、全部無輸出時不寫記憶、
ROLE_BRIEF_ENABLED=0 可停用;對話交接確認 skill 內容已排除且 8 則全為真實使用者發言。

版號 0.0.3 → 0.0.4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-29 09:05:22 +08:00
co-authored by Claude Opus 5
parent 48dc9ea74a
commit e10865ced7
8 changed files with 264 additions and 14 deletions
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# ==============================================================================
# 用途:晨間狀態檢查範例 —— 列出 Gitea 上仍開啟中的 PR,讓角色早上能主動提醒。
# 更新時間:2026/07/29 09:05:00
# 相依:bash、curl、node(不使用 jq)。
#
# 安裝:複製到 ~/.roles/<角色 ID>.checks/ 並加上執行權限,然後重跑 --install-cron
# cp check-gitea-prs.sh ~/.roles/YUI01.checks/
# chmod +x ~/.roles/YUI01.checks/check-gitea-prs.sh
#
# 設定(環境變數,需寫進 crontab 或 ~/.roles 的環境,cron 沒有互動 shell 的變數):
# GITEA_HOST Gitea 站台,例如 https://gitea.example.com
# GITEA_TOKEN 存取權杖(本腳本不會輸出它;晨間檢查寫入記憶前仍會再遮蔽一次)
# GITEA_REPOS 逗號分隔的 owner/repo 清單,例如 plugins/generic,plugins/code
#
# 慣例:**沒有需要回報的事情就不要輸出任何內容**。晨間檢查只在有輸出時才寫記憶,
# 靜默即代表「一切正常,不必打擾使用者」。
# ==============================================================================
set -u
HOST="${GITEA_HOST:-}"
TOKEN="${GITEA_TOKEN:-}"
REPOS="${GITEA_REPOS:-}"
# 設定不全就安靜結束:晨間檢查不該因為沒設定而每天產生雜訊
[ -n "$HOST" ] && [ -n "$TOKEN" ] && [ -n "$REPOS" ] || exit 0
command -v curl >/dev/null 2>&1 || exit 0
command -v node >/dev/null 2>&1 || exit 0
lines=""
IFS=','
for repo in $REPOS; do
repo="$(printf '%s' "$repo" | tr -d '[:space:]')"
[ -n "$repo" ] || continue
body="$(curl -sS --max-time 15 \
-H "Authorization: token ${TOKEN}" \
"${HOST}/api/v1/repos/${repo}/pulls?state=open&limit=20" 2>/dev/null)" || continue
[ -n "$body" ] || continue
summary="$(printf '%s' "$body" | node -e '
let raw = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { raw += chunk; });
process.stdin.on("end", () => {
let data;
try { data = JSON.parse(raw); } catch { return; }
if (!Array.isArray(data) || !data.length) return;
const repo = process.argv[1];
for (const pr of data) {
// mergeable 為 false 通常代表有衝突或未過檢查,值得在早上提醒
const blocked = pr.mergeable === false ? ",有衝突或未過檢查" : "";
console.log(`${repo} PR #${pr.number}${pr.title}${pr.head?.ref ?? "?"} → ${pr.base?.ref ?? "?"}${blocked}`);
}
});
' "$repo" 2>/dev/null)"
[ -n "$summary" ] && lines="${lines}${summary}
"
done
unset IFS
# 有開啟中的 PR 才輸出;全部合併完畢就靜默
if [ -n "$lines" ]; then
printf '尚未合併的 PR\n%s' "$lines"
fi