therookie 0.4.14 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/update.js +9 -0
- package/src/lib/deploy-artifacts.js +9 -0
- package/src/lib/merge-hooks.js +1 -1
- package/templates/hook/bin/recall-episode-metric.sh +18 -5
- package/templates/hook/bin/recall-push.sh +114 -0
- package/templates/hook/settings.partial.json +2 -1
- package/version.json +1 -1
package/package.json
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -143,6 +143,13 @@ export async function runUpdate(flags) {
|
|
|
143
143
|
)
|
|
144
144
|
chmodSync(join(pretoolBinDir, 'recall-episode-metric.sh'), 0o755)
|
|
145
145
|
|
|
146
|
+
// spec 2026-08-08 무조건 회상 푸시 — UserPromptSubmit 매 발화 무조건 질의 + 임계 push 주입.
|
|
147
|
+
cpSync(
|
|
148
|
+
join(TEMPLATES, 'hook', 'bin', 'recall-push.sh'),
|
|
149
|
+
join(pretoolBinDir, 'recall-push.sh')
|
|
150
|
+
)
|
|
151
|
+
chmodSync(join(pretoolBinDir, 'recall-push.sh'), 0o755)
|
|
152
|
+
|
|
146
153
|
const libDir = join(binDir, 'lib')
|
|
147
154
|
mkdirSync(libDir, { recursive: true })
|
|
148
155
|
cpSync(join(TEMPLATES, 'hook', 'lib', 'queue.sh'), join(libDir, 'queue.sh'))
|
|
@@ -176,6 +183,7 @@ export async function runUpdate(flags) {
|
|
|
176
183
|
skillGateCmd: claudeHookCommand(join(binDir, 'user-prompt-skill-gate.sh')),
|
|
177
184
|
workRecallGateCmd: claudeHookCommand(join(pretoolBinDir, 'work-recall-gate.sh')),
|
|
178
185
|
recallEpisodeMetricCmd: claudeHookCommand(join(pretoolBinDir, 'recall-episode-metric.sh')),
|
|
186
|
+
recallPushCmd: claudeHookCommand(join(pretoolBinDir, 'recall-push.sh')),
|
|
179
187
|
})
|
|
180
188
|
|
|
181
189
|
// v6 P0b — 글로벌 SKILL.md 갱신 (backup-and-overwrite). 같은 본문이면 no-op.
|
|
@@ -223,6 +231,7 @@ async function mergeSettings(settingsPath, partialPath, subs) {
|
|
|
223
231
|
.replaceAll('__GUARD_DESTRUCTIVE_DB_CMD__', escapeJsonStringValue(subs.guardCmd))
|
|
224
232
|
.replaceAll('__USER_PROMPT_SKILL_GATE_CMD__', escapeJsonStringValue(subs.skillGateCmd))
|
|
225
233
|
.replaceAll('__WORK_RECALL_GATE_CMD__', escapeJsonStringValue(subs.workRecallGateCmd))
|
|
234
|
+
.replaceAll('__RECALL_PUSH_CMD__', escapeJsonStringValue(subs.recallPushCmd))
|
|
226
235
|
const partial = JSON.parse(partialRaw)
|
|
227
236
|
|
|
228
237
|
let current = {}
|
|
@@ -73,6 +73,13 @@ export function installSharedHookRuntime({ scope, home, cwd } = {}) {
|
|
|
73
73
|
)
|
|
74
74
|
chmodSync(join(pretoolBinDir, 'recall-episode-metric.sh'), 0o755)
|
|
75
75
|
|
|
76
|
+
// spec 2026-08-08 무조건 회상 푸시 — UserPromptSubmit 매 발화 무조건 질의 + 임계 push 주입.
|
|
77
|
+
cpSync(
|
|
78
|
+
join(TEMPLATES, 'hook', 'bin', 'recall-push.sh'),
|
|
79
|
+
join(pretoolBinDir, 'recall-push.sh')
|
|
80
|
+
)
|
|
81
|
+
chmodSync(join(pretoolBinDir, 'recall-push.sh'), 0o755)
|
|
82
|
+
|
|
76
83
|
// spec post-v5-decisions §17 — hook offline queue 라이브러리 배포
|
|
77
84
|
const libDir = join(binDir, 'lib')
|
|
78
85
|
mkdirSync(libDir, { recursive: true })
|
|
@@ -129,6 +136,7 @@ export async function applyClaudeSetup({ scope, cfg, home, cwd } = {}) {
|
|
|
129
136
|
skillGateCmd: claudeHookCommand(join(binDir, 'user-prompt-skill-gate.sh')),
|
|
130
137
|
workRecallGateCmd: claudeHookCommand(join(pretoolBinDir, 'work-recall-gate.sh')),
|
|
131
138
|
recallEpisodeMetricCmd: claudeHookCommand(join(pretoolBinDir, 'recall-episode-metric.sh')),
|
|
139
|
+
recallPushCmd: claudeHookCommand(join(pretoolBinDir, 'recall-push.sh')),
|
|
132
140
|
})
|
|
133
141
|
|
|
134
142
|
// §18 U-3 — 클라이언트 manifest 저장. 다음 hook turn 부터 미스매치 경고 비교 가능.
|
|
@@ -187,6 +195,7 @@ async function mergeSettings(settingsPath, partialPath, subs) {
|
|
|
187
195
|
.replaceAll('__GUARD_DESTRUCTIVE_DB_CMD__', escapeJsonStringValue(subs.guardCmd))
|
|
188
196
|
.replaceAll('__USER_PROMPT_SKILL_GATE_CMD__', escapeJsonStringValue(subs.skillGateCmd))
|
|
189
197
|
.replaceAll('__WORK_RECALL_GATE_CMD__', escapeJsonStringValue(subs.workRecallGateCmd))
|
|
198
|
+
.replaceAll('__RECALL_PUSH_CMD__', escapeJsonStringValue(subs.recallPushCmd))
|
|
190
199
|
const partial = JSON.parse(partialRaw)
|
|
191
200
|
|
|
192
201
|
let current = {}
|
package/src/lib/merge-hooks.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// refresh-context.sh 포함 — 과거 UserPromptSubmit 로 실배포됐던 legacy hook.
|
|
9
9
|
// 마커 소실 구버전 설정에서 이를 보존하면 매 turn 대용량 주입 회귀가 남는다(codex R3-P1).
|
|
10
10
|
export const ROOKIE_HOOK_SCRIPTS =
|
|
11
|
-
/[\\/]\.rookie[\\/]bin[\\/](?:bin[\\/])?(?:flush-session|session-start|user-prompt-skill-gate|guard-destructive-db|pretool-md-read|posttool-recall-nudge|work-recall-gate|recall-episode-metric|sweep-recovery|refresh-context)\.sh\b/
|
|
11
|
+
/[\\/]\.rookie[\\/]bin[\\/](?:bin[\\/])?(?:flush-session|session-start|user-prompt-skill-gate|guard-destructive-db|pretool-md-read|posttool-recall-nudge|work-recall-gate|recall-episode-metric|recall-push|sweep-recovery|refresh-context)\.sh\b/
|
|
12
12
|
|
|
13
13
|
export function isRookieEntry(entry) {
|
|
14
14
|
if (!entry || typeof entry !== 'object') return false
|
|
@@ -185,10 +185,14 @@ try {
|
|
|
185
185
|
cur={
|
|
186
186
|
episode_seq: seq++,
|
|
187
187
|
episode_start_byte_offset: lineOff,
|
|
188
|
-
parser_version:
|
|
188
|
+
parser_version: 3,
|
|
189
189
|
is_human_prompt: true,
|
|
190
190
|
recall_signal: sig, recall_called: false, verified_solution_served: false,
|
|
191
191
|
external_search: false, amnesia_next_prompt: false, correction_next_prompt: false,
|
|
192
|
+
// spec 2026-08-06 A.5b — external_search 는 이제 **웹 도구만** 센다.
|
|
193
|
+
// external_search_after_recall: 회상 이후의 외부검색만(순서 있는 신호).
|
|
194
|
+
// bash_http_seen: 느슨한 Bash HTTP 관측 — 진단 전용, headline 금지.
|
|
195
|
+
external_search_after_recall: false, bash_http_seen: false,
|
|
192
196
|
first_action_after_recall: null, failure_classes: [],
|
|
193
197
|
_boundary: firstBoundaryAfter(lineOff),
|
|
194
198
|
_recallSeen: false,
|
|
@@ -214,12 +218,21 @@ try {
|
|
|
214
218
|
let action="other";
|
|
215
219
|
if(/recall/i.test(name) || /api\/rookie\/recall/.test(cmd)) action="recall";
|
|
216
220
|
else if(/^(WebSearch|WebFetch)$/.test(name)) action="external_search";
|
|
217
|
-
// 외부 Bash HTTP(curl/wget 로 밖에서 답 찾기)도 외부검색 — recall/내부 API·로컬 dev 는 제외(codex P1·P2).
|
|
218
|
-
else if(name==="Bash" && /\b(curl|wget)\b/.test(cmd) && /https?:\/\//.test(cmd) && !/api\/rookie/.test(cmd) && !/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(cmd)) action="external_search";
|
|
219
221
|
else if(/^(Edit|Write|NotebookEdit)$/.test(name)) action="edit";
|
|
220
222
|
else if(name==="AskUserQuestion") action="ask";
|
|
223
|
+
// spec 2026-08-06 A.5b B-1 — Bash 는 외부검색으로 세지 않는다. 명령문 전체에서 curl|wget 을
|
|
224
|
+
// 찾던 옛 판정이 heredoc 본문·경로에 그 단어가 든 **로컬 스크립트**까지 외부검색으로 셌다
|
|
225
|
+
// (§7 원문 대조: 강 3건 중 2건이 이 오탐). 정확히 세려면 셸 문법을 다시 구현해야 해서 철회했다.
|
|
226
|
+
// 느슨한 관측은 버리지 않고 **진단 전용 신호로 격리**한다.
|
|
227
|
+
const bashHttp = name==="Bash" && /\b(curl|wget)\b/.test(cmd) && /https?:\/\//.test(cmd)
|
|
228
|
+
&& !/api\/rookie/.test(cmd) && !/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(cmd)
|
|
229
|
+
if(bashHttp) cur.bash_http_seen=true;
|
|
221
230
|
if(action==="recall"){ cur.recall_called=true; cur._recallSeen=true; }
|
|
222
|
-
if(action==="external_search"){
|
|
231
|
+
if(action==="external_search"){
|
|
232
|
+
cur.external_search=true;
|
|
233
|
+
// B-2 — 첫 행동 기록 여부와 **무관하게** 누적한다(회상 이후의 모든 외부검색).
|
|
234
|
+
if(cur._recallSeen) cur.external_search_after_recall=true;
|
|
235
|
+
}
|
|
223
236
|
// recall 이후의 첫 실질 행동을 1회만 기록.
|
|
224
237
|
if(cur._recallSeen && action!=="recall" && cur.first_action_after_recall===null){
|
|
225
238
|
cur.first_action_after_recall=action;
|
|
@@ -259,7 +272,7 @@ try {
|
|
|
259
272
|
// 앞으로 밀려(불안정) 무엇이 왜 빠졌는지 사후 확인이 불가능해진다.
|
|
260
273
|
let activatedAt=null;
|
|
261
274
|
try{
|
|
262
|
-
const ap=path.join(bndDir,".
|
|
275
|
+
const ap=path.join(bndDir,".v3-activated");
|
|
263
276
|
const readAct=()=>{ try{ const v=JSON.parse(fs.readFileSync(ap,"utf8")).activated_at; return typeof v==="string"?v:null; }catch{ return null; } };
|
|
264
277
|
activatedAt=readAct();
|
|
265
278
|
if(!activatedAt){
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# UserPromptSubmit hook — 무조건 회상 푸시 (unconditional recall push).
|
|
3
|
+
# 설계: docs/superpowers/specs/2026-08-08-unconditional-recall-push-design.md
|
|
4
|
+
#
|
|
5
|
+
# - 발동 조건 없음(E1 결정 제거): 매 사용자 발화를 그대로 recall 질의로 던진다. 빈 발화만 skip —
|
|
6
|
+
# 길이 하한·낱말 필터는 결정의 재도입이라 두지 않는다(§1.1).
|
|
7
|
+
# - hit(THRESH 이상 상위 K)만 additionalContext 로 그 턴의 생성 컨텍스트에 주입(E2). 무-hit → 0바이트.
|
|
8
|
+
# - THRESH 기본 0.0165 = rrf k=60 단일 신호 최대치 1/61 바로 위 — "FTS·벡터 두 신호가 합의한
|
|
9
|
+
# 결과만 주입"이라는 기계적 의미(worklog §96 분포 실측). 모델 판단·어휘 패턴 매칭이 아니다.
|
|
10
|
+
# - 세션 dedup: 같은 기억 id 는 세션당 1회. /clear·compact 는 skill-gate <key>.epoch 값 변화로
|
|
11
|
+
# 감지해 리셋한다(컨텍스트가 비워졌으니 재주입이 옳다).
|
|
12
|
+
# - fail-soft: 타임아웃(늦으면 그 턴 무주입)·네트워크·파싱 실패 전부 조용히 무주입 종료.
|
|
13
|
+
# 아무것도 막지 않는다 — deny 0, 기존 user-prompt-skill-gate.sh 불변.
|
|
14
|
+
set -u
|
|
15
|
+
|
|
16
|
+
# self-reference 방지: internal LLM 호출(claude -p)에서 발사된 hook 은 즉시 종료.
|
|
17
|
+
[ "${ROOKIE_INTERNAL_INVOCATION:-}" = "1" ] && exit 0
|
|
18
|
+
command -v node >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat 2>/dev/null) || exit 0
|
|
21
|
+
[ -z "$INPUT" ] && exit 0
|
|
22
|
+
|
|
23
|
+
# config 해석 (env > 프로젝트-로컬 > 전역) — B6. 배포 위치는 ~/.rookie/bin/bin/ 이라 ../lib.
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
if [ -f "$SCRIPT_DIR/../lib/config-resolve.sh" ]; then
|
|
26
|
+
# shellcheck source=/dev/null
|
|
27
|
+
. "$SCRIPT_DIR/../lib/config-resolve.sh"
|
|
28
|
+
elif [ -f "$HOME/.rookie/bin/lib/config-resolve.sh" ]; then
|
|
29
|
+
# shellcheck source=/dev/null
|
|
30
|
+
. "$HOME/.rookie/bin/lib/config-resolve.sh"
|
|
31
|
+
fi
|
|
32
|
+
command -v rookie_resolve_credentials >/dev/null 2>&1 || exit 0
|
|
33
|
+
rookie_resolve_credentials "$PWD" || exit 0
|
|
34
|
+
[ -n "$ROOKIE_RESOLVED_TOKEN" ] || exit 0
|
|
35
|
+
|
|
36
|
+
PUSH_DIR="${ROOKIE_RECALL_PUSH_DIR:-$HOME/.rookie/recall-push}"
|
|
37
|
+
GATE_DIR="${ROOKIE_SKILL_GATE_DIR:-$HOME/.rookie/skill-gate}"
|
|
38
|
+
HOST_VAL="$(hostname 2>/dev/null || echo "")"
|
|
39
|
+
|
|
40
|
+
# payload 는 stdin 으로(ARG_MAX 회피). 질의·필터·dedup·주입 결정 전부 node 한 덩어리 —
|
|
41
|
+
# code 안에 작은따옴표 미사용(bash single-quote 보호).
|
|
42
|
+
printf '%s' "$INPUT" | \
|
|
43
|
+
ROOKIE_PUSH_DIR="$PUSH_DIR" ROOKIE_GATE_DIR="$GATE_DIR" \
|
|
44
|
+
ROOKIE_EP="$ROOKIE_RESOLVED_ENDPOINT" ROOKIE_TK="$ROOKIE_RESOLVED_TOKEN" \
|
|
45
|
+
ROOKIE_HOST="$HOST_VAL" ROOKIE_CWD="$PWD" node -e '
|
|
46
|
+
let d="";
|
|
47
|
+
process.stdin.on("data",c=>d+=c).on("end",async()=>{
|
|
48
|
+
try{
|
|
49
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");
|
|
50
|
+
const p=JSON.parse(d);
|
|
51
|
+
const sid=String(p.session_id||p.sessionId||"");
|
|
52
|
+
let prompt=String(p.prompt||"").trim();
|
|
53
|
+
if(!prompt) return; // 빈 발화 = 질의 불성립(유일한 기계 조건)
|
|
54
|
+
prompt=prompt.slice(0,500); // RecallQuerySchema q max(500) — 초과는 400 무주입(R1 P1)
|
|
55
|
+
const THRESH=Number(process.env.ROOKIE_RECALL_PUSH_THRESH||"0.0165");
|
|
56
|
+
const K=parseInt(process.env.ROOKIE_RECALL_PUSH_TOPK||"3",10);
|
|
57
|
+
const CAP=parseInt(process.env.ROOKIE_RECALL_PUSH_CAP||"1200",10);
|
|
58
|
+
const TIMEOUT=parseInt(process.env.ROOKIE_RECALL_PUSH_TIMEOUT_MS||"3500",10);
|
|
59
|
+
|
|
60
|
+
// 세션 dedup 상태 — epoch(skill-gate 기록) 가 바뀌었으면 /clear·compact 경계로 보고 리셋.
|
|
61
|
+
const key=sid?crypto.createHash("sha256").update(sid).digest("hex"):"";
|
|
62
|
+
let epoch="";
|
|
63
|
+
if(key){ try{ epoch=String(fs.readFileSync(path.join(process.env.ROOKIE_GATE_DIR,key+".epoch"),"utf8")).trim(); }catch{} }
|
|
64
|
+
const stFile=key?path.join(process.env.ROOKIE_PUSH_DIR,key+".injected"):null;
|
|
65
|
+
let st={epoch,ids:[]};
|
|
66
|
+
if(stFile){ try{ const j=JSON.parse(fs.readFileSync(stFile,"utf8")); if(j&&j.epoch===epoch&&Array.isArray(j.ids)) st=j; }catch{} }
|
|
67
|
+
const seen=new Set(st.ids);
|
|
68
|
+
|
|
69
|
+
// 무조건 질의 — scope=all(현 구현상 project boost 없음, R1 P2).
|
|
70
|
+
// x-rookie-hostname + repo_path 동시 전달 = workspace 격리 기억 가시성 계약(R2 P2, fail-closed).
|
|
71
|
+
const u=new URL("/api/rookie/recall",process.env.ROOKIE_EP);
|
|
72
|
+
u.searchParams.set("q",prompt);
|
|
73
|
+
u.searchParams.set("scope","all");
|
|
74
|
+
u.searchParams.set("limit","5");
|
|
75
|
+
if(process.env.ROOKIE_CWD) u.searchParams.set("repo_path",process.env.ROOKIE_CWD);
|
|
76
|
+
const res=await fetch(u,{
|
|
77
|
+
headers:{Authorization:"Bearer "+process.env.ROOKIE_TK,"x-rookie-hostname":process.env.ROOKIE_HOST||""},
|
|
78
|
+
signal:AbortSignal.timeout(TIMEOUT)
|
|
79
|
+
});
|
|
80
|
+
if(!res.ok) return;
|
|
81
|
+
const body=await res.json();
|
|
82
|
+
const results=Array.isArray(body&&body.results)?body.results:[];
|
|
83
|
+
const cands=results
|
|
84
|
+
.filter(r=>r&&typeof r.relevance==="number"&&r.relevance>=THRESH&&r.id&&typeof r.summary==="string"&&r.summary.trim()&&!seen.has(r.id))
|
|
85
|
+
.sort((a,b)=>b.relevance-a.relevance)
|
|
86
|
+
.slice(0,K);
|
|
87
|
+
if(!cands.length) return; // 무-hit(또는 전부 기주입) → 0바이트
|
|
88
|
+
|
|
89
|
+
// 주입 문구는 지시가 아니라 자료다(§3.2) — 요약 + 시각 + 출처 프로젝트(전역이면 명시).
|
|
90
|
+
const MARK=["①","②","③","④","⑤"];
|
|
91
|
+
let msg="🧠 관련 기억:";
|
|
92
|
+
const used=[];
|
|
93
|
+
for(let i=0;i<cands.length;i++){
|
|
94
|
+
const r=cands[i];
|
|
95
|
+
const when=r.memory_at?String(r.memory_at).slice(0,10):"시각 미상";
|
|
96
|
+
const src=r.project_name?String(r.project_name):"전역";
|
|
97
|
+
const summary=String(r.summary).replace(/\s+/g," ").trim().slice(0,400);
|
|
98
|
+
const item="\n"+(MARK[i]||"-")+" "+summary+" ("+when+", "+src+")";
|
|
99
|
+
if(msg.length+item.length>CAP) break; // 총량 상한 — relevance 순 절삭
|
|
100
|
+
msg+=item; used.push(r.id);
|
|
101
|
+
}
|
|
102
|
+
if(!used.length) return;
|
|
103
|
+
if(stFile){
|
|
104
|
+
try{
|
|
105
|
+
fs.mkdirSync(process.env.ROOKIE_PUSH_DIR,{recursive:true,mode:0o700});
|
|
106
|
+
fs.writeFileSync(stFile,JSON.stringify({epoch,ids:st.ids.concat(used)}),{mode:0o600});
|
|
107
|
+
}catch{} // dedup 기록 실패 → 다음 턴 중복 주입 위험만, 주입은 진행
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:msg}}));
|
|
110
|
+
}catch{ /* fail-soft — 조용히 무주입 */ }
|
|
111
|
+
});
|
|
112
|
+
' 2>/dev/null
|
|
113
|
+
|
|
114
|
+
exit 0
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
{
|
|
70
70
|
"__rookie_managed": true,
|
|
71
71
|
"hooks": [
|
|
72
|
-
{ "type": "command", "command": "__USER_PROMPT_SKILL_GATE_CMD__", "timeout": 3 }
|
|
72
|
+
{ "type": "command", "command": "__USER_PROMPT_SKILL_GATE_CMD__", "timeout": 3 },
|
|
73
|
+
{ "type": "command", "command": "__RECALL_PUSH_CMD__", "timeout": 5 }
|
|
73
74
|
]
|
|
74
75
|
}
|
|
75
76
|
]
|
package/version.json
CHANGED