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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "therookie",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "the Rookie — the AI new hire that remembers what you teach it. A CLI that connects a personal AI memory to AI tools like Claude and Codex.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -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 = {}
@@ -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: 2,
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"){ cur.external_search=true; }
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,".v2-activated");
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
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.14",
2
+ "cli_version": "0.4.16",
3
3
  "skill_version": "1.35.0"
4
4
  }