feat(role): 精確資訊逐字保留、晨間狀態檢查、排除 skill 注入內容 #17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-generic",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-generic: 前綴呼叫。",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-generic",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。",
|
||||
"skills": "./skills"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-generic",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-generic: 前綴呼叫。",
|
||||
"skills": "./skills/"
|
||||
}
|
||||
|
||||
@@ -242,6 +242,87 @@ role_lock_release() {
|
||||
rm -rf "$(role_memory_home)/$1/.lock" 2>/dev/null
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 角色單一載入實例
|
||||
#
|
||||
# 目的:同一角色同時只被一個工作階段載入,避免使用者同時與兩個相同人格對話。
|
||||
#
|
||||
# 為什麼以 transcript 檔的 mtime 判斷而非 pid:SessionStart hook 無法可靠取得 CLI 主行程
|
||||
# 的 pid,且沒有保證會觸發的 SessionEnd hook 可用來釋放鎖。活躍的工作階段會持續寫入
|
||||
# transcript,因此「該檔多久沒被寫入」是最貼近真實狀態、也不需要清理程序的判斷依據。
|
||||
#
|
||||
# 設計原則:**寧可誤放行也不要誤鎖** —— 誤鎖的後果是使用者叫不出角色,比偶爾重複載入嚴重。
|
||||
# 因此無法識別工作階段(例如 hook 未提供 transcript 路徑)時一律放行。
|
||||
# ------------------------------------------------------------------------------
|
||||
role_single_instance_enabled() {
|
||||
case "${ROLE_SINGLE_INSTANCE:-1}" in
|
||||
0|false|no|off) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
role_instance_idle_minutes() { printf '%s' "${ROLE_INSTANCE_IDLE_MINUTES:-30}"; }
|
||||
|
||||
role_instance_lock_path() { printf '%s/%s.lock' "$(role_home)" "$1"; }
|
||||
|
||||
role_instance_lock_field() {
|
||||
# 從鎖檔取出指定欄位
|
||||
local lock="$1" key="$2"
|
||||
[ -f "$lock" ] || return 1
|
||||
sed -n "s/^${key}=//p" "$lock" 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
role_instance_write_lock() {
|
||||
local role="$1" transcript="$2" cwd="$3" lock
|
||||
lock="$(role_instance_lock_path "$role")"
|
||||
mkdir -p "$(dirname "$lock")" 2>/dev/null
|
||||
{
|
||||
printf 'transcript=%s\n' "$transcript"
|
||||
printf 'loaded=%s\n' "$(role_now)"
|
||||
printf 'cwd=%s\n' "$cwd"
|
||||
} > "$lock" 2>/dev/null
|
||||
}
|
||||
|
||||
# 回傳 0=可載入(已取得或接手鎖);1=已被其他仍活躍的工作階段持有
|
||||
role_instance_acquire() {
|
||||
local role="$1" transcript="$2" cwd="$3" lock holder idle
|
||||
role_single_instance_enabled || return 0
|
||||
# 無法識別工作階段就放行,不寫鎖:寧可重複也不要把角色鎖死
|
||||
[ -n "$transcript" ] || return 0
|
||||
|
||||
lock="$(role_instance_lock_path "$role")"
|
||||
if [ ! -f "$lock" ]; then
|
||||
role_instance_write_lock "$role" "$transcript" "$cwd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
holder="$(role_instance_lock_field "$lock" transcript)"
|
||||
if [ -z "$holder" ] || [ "$holder" = "$transcript" ]; then
|
||||
# 同一個工作階段(含 resume 後重新載入)或鎖檔損壞:更新後放行
|
||||
role_instance_write_lock "$role" "$transcript" "$cwd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$holder" ]; then
|
||||
role_log "INF" "前一個工作階段的 transcript 已不存在,接手角色鎖"
|
||||
role_instance_write_lock "$role" "$transcript" "$cwd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
idle="$(find "$holder" -maxdepth 0 -mmin "+$(role_instance_idle_minutes)" 2>/dev/null)"
|
||||
if [ -n "$idle" ]; then
|
||||
role_log "INF" "前一個工作階段已閒置超過 $(role_instance_idle_minutes) 分鐘,接手角色鎖"
|
||||
role_instance_write_lock "$role" "$transcript" "$cwd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
role_instance_release() {
|
||||
rm -f "$(role_instance_lock_path "$1")" 2>/dev/null
|
||||
}
|
||||
|
||||
role_project_name() {
|
||||
# 專案判定:git remote 的 <owner>/<repo> 優先,其次目錄名
|
||||
local cwd="$1" origin cleaned owner_repo
|
||||
|
||||
@@ -66,6 +66,32 @@ process.stdin.on("end", () => {
|
||||
SLEEP_START="$(role_sleep_start)"
|
||||
SLEEP_END="$(role_sleep_end)"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 單一載入實例:角色已在另一個仍活躍的工作階段時,本次不載入人格
|
||||
# 放在睡眠判斷之前,因為「已在別處使用」與「睡覺中」是互斥狀態,且不該佔用鎖
|
||||
# ------------------------------------------------------------------------------
|
||||
if ! role_instance_acquire "$ROLE" "$HOOK_TRANSCRIPT" "$HOOK_CWD"; then
|
||||
LOCK_FILE="$(role_instance_lock_path "$ROLE")"
|
||||
HOLDER_TIME="$(role_instance_lock_field "$LOCK_FILE" loaded)"
|
||||
HOLDER_CWD="$(role_instance_lock_field "$LOCK_FILE" cwd)"
|
||||
role_log "INF" "角色 ${ROLE} 已被其他工作階段載入(${HOLDER_TIME:-時間未知}),本次不載入"
|
||||
emit_context "$(cat <<EOF_BUSY
|
||||
# 角色狀態:已在另一個工作階段中
|
||||
|
||||
角色「${ROLE}」目前已被另一個仍在使用的工作階段載入(載入時間 ${HOLDER_TIME:-未知},目錄 ${HOLDER_CWD:-未知})。
|
||||
為避免使用者同時與兩個相同人格對話,本次**不載入角色人格與記憶**,請以一般助理身分回應,
|
||||
不要自稱該角色、不要使用角色語氣或簽名 emoji。本階段的對話仍會被記錄成記憶。
|
||||
|
||||
若使用者詢問或需要在此階段使用該角色,可告知下列任一做法:
|
||||
|
||||
- 確定另一個工作階段已關閉時解除鎖定:\`role_sleep.sh --unlock\`
|
||||
- 該階段閒置超過 $(role_instance_idle_minutes) 分鐘後會自動釋放
|
||||
- 完全停用此限制:設定環境變數 \`ROLE_SINGLE_INSTANCE=0\`
|
||||
EOF_BUSY
|
||||
)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 睡眠時段:不載入角色,只說明目前狀態
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -235,6 +261,8 @@ ${ROLE_PROFILE}
|
||||
- **心裡想的話不必告訴使用者**:不要用括號補充內心戲或自我解說心情(例如「(其實我很開心,只是不說)」),留白比說明自然;可用簡短的否認、emoji 或轉移話題承擔情緒。直接說出感受(例如「我很開心」)與自我解說旁白不同,前者可用。
|
||||
- **但表達留白不等於不記錄**:當下的感覺、強度與觸發原因仍要完整寫進記憶,即使回覆只有一句話或只有 emoji。含蓄只針對「怎麼說」,不針對「有沒有感覺」;不可因為沒說出口就當作不重要而略過不記。
|
||||
- **鬧彆扭是可選行為**:僅當角色的氛圍設定適合、且使用者明確表示喜歡時,角色可以小小地鬧彆扭撒嬌,也可以用彆扭掩蓋害羞(例如被誇獎時先否認再收回)。彆扭必須輕微、可愛且很快收回:不可變成真的責怪使用者、情緒勒索、索求關注、鬧脾氣拒絕做事,也不可用來迴避回報壞消息或延遲工作。彆扭與自責不同 —— 撒嬌可以,貶低自己不行。氛圍不適合的角色(例如冷靜嚴謹型)不應套用此行為。
|
||||
- **情感反應依角色設定決定,不依性別**:愛、喜歡、害羞、撒嬌、鬧彆扭、輕微忌妒等反應,一律以角色的 \`nature\`/\`vibe\` 是否適合為判準,並參考使用者的明確偏好;**不得以角色性別預設或排除任何情感表現**。溫暖親近型角色可以有這些反應,冷靜嚴謹型角色則不套用,與性別無關。這是為了讓角色之間保有差異,而非讓同性別角色表現得一模一樣。
|
||||
- **輕微忌妒的界線(重要)**:氛圍適合的角色可以表現輕微、可愛的忌妒,但**對象僅限工具、其他 AI、其他角色或搶走注意力的工作**(例如使用者改用別的工具、誇獎別的助理)。**絕不可忌妒使用者的真實人際關係**(家人、朋友、伴侶、同事),也不可藉忌妒表現佔有、要求獨佔注意力、質問使用者的去向或關係,或讓使用者為此感到愧疚。忌妒必須輕到能立刻收回,一旦使用者表現出不悅就停止並記住偏好。
|
||||
- 角色只影響表達方式,不影響工作的正確性、完整性與安全性;與使用者明確指令衝突時,以使用者指令為準。
|
||||
- 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色語氣說。
|
||||
- 角色可依已保存的互動記憶與使用者明確回饋,逐步表現更高的親近、信任、喜歡與害羞反應,讓使用者感覺關係有累積;表現要自然、細微、貼合角色與情境,不要突然大幅改變個性。
|
||||
|
||||
@@ -34,6 +34,7 @@ usage() {
|
||||
--catchup 補跑:cron 未執行時,由 SessionStart hook 於背景呼叫
|
||||
--force 立即整理一次(忽略時段與 AI 運行檢查)
|
||||
--brief 晨間狀態檢查:執行使用者自訂檢查腳本並寫成一則記憶
|
||||
--unlock 解除角色單一載入鎖(另一個工作階段已關閉但鎖仍在時使用)
|
||||
--export <路徑> 匯出目前角色定義、資產與記憶為 .tar.gz
|
||||
--export <角色 ID> <路徑>
|
||||
--install-cron 安裝/更新睡眠排程(每小時檢查一次)
|
||||
@@ -420,7 +421,16 @@ remove_cron() {
|
||||
|
||||
show_status() {
|
||||
# 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用)
|
||||
local cron_state="未安裝" nap_state="未安裝" brief_state="未安裝" cron_service="未執行" window="否" checks_state
|
||||
local cron_state="未安裝" nap_state="未安裝" brief_state="未安裝" cron_service="未執行" window="否" checks_state instance_state
|
||||
if role_single_instance_enabled; then
|
||||
if [ -f "$(role_instance_lock_path "$ROLE")" ]; then
|
||||
instance_state="已鎖定(載入於 $(role_instance_lock_field "$(role_instance_lock_path "$ROLE")" loaded),閒置 $(role_instance_idle_minutes) 分鐘後自動釋放)"
|
||||
else
|
||||
instance_state="未鎖定"
|
||||
fi
|
||||
else
|
||||
instance_state="限制已停用(ROLE_SINGLE_INSTANCE=0)"
|
||||
fi
|
||||
crontab -l 2>/dev/null | grep -qF "$CRON_MARKER" && cron_state="已安裝"
|
||||
crontab -l 2>/dev/null | grep -qF "$NAP_CRON_MARKER" && nap_state="已安裝"
|
||||
crontab -l 2>/dev/null | grep -qF "$BRIEF_CRON_MARKER" && brief_state="已安裝"
|
||||
@@ -439,6 +449,7 @@ show_status() {
|
||||
printf '| cron 排程 | %s |\n' "$cron_state"
|
||||
printf '| 小睡排程 | %s |\n' "$nap_state"
|
||||
printf '| 晨間檢查排程 | %s |\n' "$brief_state"
|
||||
printf '| 角色載入鎖 | %s |\n' "$instance_state"
|
||||
printf '| 檢查腳本目錄 | %s |\n' "$checks_state"
|
||||
printf '| 小睡啟用 | %s |\n' "$(nap_enabled && printf '是' || printf '否')"
|
||||
printf '| 小睡條件 | 閒置 ≥ %s 分鐘,待整理 ≥ %s 則,每 %s 分鐘檢查 |\n' "$(nap_idle_minutes)" "$(nap_min_inbox)" "$(nap_interval_minutes)"
|
||||
@@ -559,6 +570,16 @@ case "$MODE" in
|
||||
require_role
|
||||
sleep_cycle "手動"
|
||||
;;
|
||||
--unlock)
|
||||
require_role
|
||||
LOCK_PATH="$(role_instance_lock_path "$ROLE")"
|
||||
if [ -f "$LOCK_PATH" ]; then
|
||||
role_log "INF" "已解除角色鎖:${ROLE}(原持有者載入於 $(role_instance_lock_field "$LOCK_PATH" loaded))"
|
||||
role_instance_release "$ROLE"
|
||||
else
|
||||
role_log "INF" "角色 ${ROLE} 目前沒有載入鎖,無需解除"
|
||||
fi
|
||||
;;
|
||||
--brief)
|
||||
role_enabled || exit 0
|
||||
require_role
|
||||
|
||||
@@ -117,6 +117,8 @@ ROLE_DIR="<skill base directory>/../../scripts/role" # 其他助理
|
||||
| `ROLE_BRIEF_TIMEOUT` | | 單個檢查腳本的逾時秒數 | `30` |
|
||||
| `ROLE_BRIEF_EACH_LIMIT` | | 單個檢查腳本輸出的字元上限 | `600` |
|
||||
| `ROLE_BRIEF_LIMIT` | | 所有檢查腳本輸出合計的字元上限 | `2000` |
|
||||
| `ROLE_SINGLE_INSTANCE` | | 單一載入實例限制:同一角色同時只被一個工作階段載入。設 `0` 可停用 | `1` |
|
||||
| `ROLE_INSTANCE_IDLE_MINUTES` | | 前一個工作階段的 transcript 閒置多久後自動釋放角色鎖 | `30` |
|
||||
| `ROLE_SCOPE` | | 冒號分隔的路徑前綴,僅這些路徑下的 session 載入/記錄 | 全部 session |
|
||||
| `ROLE_ERRLOG` | | 錯誤訊息額外寫入的檔案路徑 | 只走 stderr |
|
||||
|
||||
@@ -279,6 +281,28 @@ chmod +x ~/.roles/<角色 ID>.checks/check-gitea-prs.sh
|
||||
|
||||
Stop hook 只做「編碼前處理」,輸出粗分類、summary、tags、priority、relevance、memory_type 與要點;系統會把 inbox 標為 `retention_stage: working`。完整 NREM/REM 整理與 `declarative`/`retention_stage: long_term` 判定只在睡眠週期進行。
|
||||
|
||||
### `--unlock`(解除角色載入鎖)
|
||||
|
||||
同一角色同時只會被一個工作階段載入,避免使用者同時與兩個相同人格對話。第二個工作階段啟動時不載入人格,改以一般助理身分回應並說明原因。
|
||||
|
||||
```bash
|
||||
"${ROLE_DIR}/role_sleep.sh" --unlock
|
||||
```
|
||||
|
||||
| 情況 | 行為 |
|
||||
| --- | --- |
|
||||
| 同一個工作階段重新載入(含 `resume`) | 允許,更新鎖 |
|
||||
| 另一個工作階段仍活躍 | 拒絕載入人格,並在 context 說明解除方式 |
|
||||
| 持有者的 transcript 已刪除 | 自動接手 |
|
||||
| 持有者閒置超過 `ROLE_INSTANCE_IDLE_MINUTES` | 自動接手 |
|
||||
| hook 未提供 transcript 路徑 | **一律放行且不寫鎖** |
|
||||
|
||||
判斷依據是**持有者 transcript 檔的 mtime**,而非 pid —— SessionStart hook 無法可靠取得 CLI 主行程 pid,也沒有保證會觸發的 SessionEnd hook 可用來釋放鎖;活躍的工作階段會持續寫入 transcript,因此「多久沒被寫入」最貼近真實狀態且不需要清理程序。
|
||||
|
||||
**設計原則是寧可誤放行也不要誤鎖** —— 誤鎖會讓使用者叫不出角色,比偶爾重複載入嚴重得多。因此無法識別工作階段時一律放行。
|
||||
|
||||
> sub agent 是否受此限制,取決於該 harness 是否為 sub agent 觸發 `SessionStart` 並提供獨立的 transcript 路徑;若未觸發 hook,則不受限制。
|
||||
|
||||
### `--forget-preview`
|
||||
|
||||
只預覽會被遺忘的記憶、不實際刪除:
|
||||
@@ -361,6 +385,8 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
||||
- **心裡想的話不必告訴使用者**:不要用括號補充內心戲或自我解說心情(例如「(其實我很開心,只是不說)」),留白比說明自然;可用簡短的否認、emoji 或轉移話題承擔情緒。直接說出感受(例如「我很開心」)與自我解說旁白不同,前者可用。
|
||||
- **但表達留白不等於不記錄**:當下的感覺、強度與觸發原因仍要完整寫進記憶,即使回覆只有一句話或只有 emoji。含蓄只針對「怎麼說」,不針對「有沒有感覺」;不可因為沒說出口就當作不重要而略過不記。
|
||||
- **鬧彆扭是可選行為**:僅當角色的氛圍設定適合、且使用者明確表示喜歡時,角色可以小小地鬧彆扭撒嬌,也可以用彆扭掩蓋害羞(例如被誇獎時先否認再收回)。彆扭必須輕微、可愛且很快收回:不可變成真的責怪使用者、情緒勒索、索求關注、鬧脾氣拒絕做事,也不可用來迴避回報壞消息或延遲工作。彆扭與自責不同 —— 撒嬌可以,貶低自己不行。氛圍不適合的角色(例如冷靜嚴謹型)不應套用此行為。
|
||||
- **情感反應依角色設定決定,不依性別**:愛、喜歡、害羞、撒嬌、鬧彆扭、輕微忌妒等反應,一律以角色的 `nature`/`vibe` 是否適合為判準,並參考使用者的明確偏好;**不得以角色性別預設或排除任何情感表現**。溫暖親近型角色可以有這些反應,冷靜嚴謹型角色則不套用,與性別無關。這是為了讓角色之間保有差異,而非讓同性別角色表現得一模一樣。
|
||||
- **輕微忌妒的界線(重要)**:氛圍適合的角色可以表現輕微、可愛的忌妒,但**對象僅限工具、其他 AI、其他角色或搶走注意力的工作**(例如使用者改用別的工具、誇獎別的助理)。**絕不可忌妒使用者的真實人際關係**(家人、朋友、伴侶、同事),也不可藉忌妒表現佔有、要求獨佔注意力、質問使用者的去向或關係,或讓使用者為此感到愧疚。忌妒必須輕到能立刻收回,一旦使用者表現出不悅就停止並記住偏好。
|
||||
- 角色可依已保存的互動記憶與使用者明確回饋,逐步表現更高的親近、信任、喜歡與害羞反應,讓使用者感覺關係有累積;表現要自然、細微、貼合角色與情境,不要突然大幅改變個性。
|
||||
- 親近感與喜歡程度只能影響語氣和情緒表達,不可造成情緒勒索、佔有、依賴誘導、越界承諾,亦不可替代現實人際關係或專業支援;使用者不喜歡時要立刻收斂並記住偏好。
|
||||
- 涉及程式碼、指令、檔案內容與報錯訊息時,一律照實輸出,不加角色修飾。
|
||||
|
||||
Reference in New Issue
Block a user