therookie 0.4.15 → 0.4.17
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-push.sh +114 -0
- package/templates/hook/settings.partial.json +2 -1
- package/templates/skill/SKILL.md +3 -0
- package/version.json +2 -2
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
|
|
@@ -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/templates/skill/SKILL.md
CHANGED
|
@@ -44,6 +44,9 @@ installed_by: __INSTALLED_BY__
|
|
|
44
44
|
"$ENDPOINT/api/rookie/context?repo_url=$(url_encode $REPO_URL)&repo_path=$(url_encode $REPO_PATH)"
|
|
45
45
|
```
|
|
46
46
|
5. `system_prompt` 를 세션 페르소나로 채택(기억 상세는 recall 위임).
|
|
47
|
+
- ⚠️ **생략 섹션 복구 (필수)**: SessionStart 주입문 말미에 ``…(컨텍스트 예산 초과로 생략됨: A · B …)`` 가 있으면 **그 섹션들이 이번 세션에 안 들어온 것**이다. 위 4번 curl 응답(무예산이라 전문)에서 **그 섹션 본문을 직접 읽는다** — 특히 `자기규칙 …` 이 목록에 있으면 `## Rookie Self` 이하 전문을 읽고 세션 규칙으로 채택한다.
|
|
48
|
+
- **응답을 파일로 받아 `current_project`·`entry_cards` 같은 필드만 뽑고 끝내지 않는다.** 컨텍스트를 아끼려는 그 습관이 기본값이라, 가르친 규칙이 매 세션 통째로 새던 원인이다(2026-08-21 실측: 자기 규칙 8,611자 전량 무음 드롭).
|
|
49
|
+
- 생략 안내가 **없으면** 전량 주입된 것이니 추가 조회 불필요.
|
|
47
50
|
- **이상 신호(B1)**: 주입 페르소나·계정 요약(`[루키] 계정 …`, `metadata.account`)이 평소와 다르면(프로젝트 0·낯선 호칭·빈 계정) 작업 전에 알린다.
|
|
48
51
|
6. **미link 감지·능동 제안**: `current_project` 가 `null` 이면 미link. 서버 `metadata.link_hint:'unlinked_repo'` 를 그대로 신뢰(B2).
|
|
49
52
|
- `~/.rookie/link-dismissed.json`(경로 배열)에 현재 cwd 가 있으면 안내 skip.
|
package/version.json
CHANGED