therookie 0.4.13 → 0.4.14

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.13",
3
+ "version": "0.4.14",
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": [
@@ -72,6 +72,33 @@ try {
72
72
  const ENVQ=/(포트|유저명|계정|경로|도메인|호스트|엔드포인트|서버\s*주소)[^\n]{0,12}(뭐|몇|어디|알아|기억|였)/;
73
73
  const isSignal=(t)=>PAST_TIME.test(t)||PAST_HOW.test(t)||REPRO.test(t)||CONTINUE.test(t)||DECIDED.test(t)||ENVQ.test(t)||AMNESIA.test(t);
74
74
 
75
+ // ── v2 §D2: 교정 발화 (계측 라벨 전용) ────────────────────────────────────
76
+ // 두목이 "이미 아는 걸 왜 모르냐"고 짚는 발화. **강제 경로(user-prompt-skill-gate.sh)에는 넣지
77
+ // 않는다** — 넣는 순간 spec 2026-07-25 §7 이 금지한 "9번째 새 트리거 추가"가 된다.
78
+ // 집합은 오늘 사고 발화 + 오탐 후보로 실측해 확정(교정 9/10 hit, 일반 8/8 미탐지).
79
+ const CORRECTION=new RegExp([
80
+ "(저장|기록)(해두지|해놓지|한\\s*거|해\\s*둔\\s*거)?\\s*(않았|안\\s*했|안했)",
81
+ "기억(하고|해두고|해\\s*두고)\\s*있(잖|었잖)",
82
+ "(툴|도구)(에|로|에다)\\s*(저장|기록)",
83
+ "회상(해|해봐|해도|부터|은?\\s*했)",
84
+ "찾아(봤|보지|봐야)\\s*(않았|았어야|어야)",
85
+ "더루키(에서|에|를)[^\\n]{0,20}(찾|봐|확인|안\\s*찾|찾지)",
86
+ "(전에|이미|지난번에?)\\s*[^\\n]{0,10}?(했|됐|만들었|성공했)(잖|었잖)"
87
+ ].join("|"));
88
+
89
+ // ── v2 §D1: human 프롬프트 판정 ──────────────────────────────────────────
90
+ // 1단 = transcript 의 isMeta 필드(Skill 로드 결과·이미지 첨부·local-command-caveat 를 정확히 거른다).
91
+ // 2단 = isMeta 가 안 붙는 나머지를 문자열로 배제. 판정 실패는 **포함 방향**(fail-open) —
92
+ // human 을 놓치는 것이 분모 결함의 재발이라 그쪽이 더 비싸다.
93
+ const SYS_PREFIX=/^\s*<(command-name|task-notification|system-reminder|local-command|command-message)/;
94
+ const isHumanPrompt=(j,txt)=>{
95
+ if(j && j.isMeta===true) return false;
96
+ if(SYS_PREFIX.test(txt)) return false;
97
+ if(/^\s*\[Request interrupted by user/.test(txt)) return false;
98
+ if(/^\s*\/\S+$/.test(txt)) return false; // 붙여넣은 파일경로 1줄
99
+ return true;
100
+ };
101
+
75
102
  // ── compact/clear boundary offsets (이 transcript 것만, 정렬) ──────────────
76
103
  let boundaries=[];
77
104
  try {
@@ -138,22 +165,37 @@ try {
138
165
  if(role==="user"){
139
166
  const txt=userText(m);
140
167
  if(!txt) continue; // tool_result-only user 라인 등은 프롬프트 아님
141
- const sig=isSignal(txt), amn=AMNESIA.test(txt);
142
- // 직전 회상 episode 의 "다음 프롬프트"가 기억무시 지적이면 표시(관련 기억 있었는데 되물음).
143
- if(cur && amn && cur.recall_called) cur.amnesia_next_prompt=true;
144
- if(sig){
145
- cur={
146
- episode_seq: seq++,
147
- episode_start_byte_offset: lineOff,
148
- recall_signal: true, recall_called: false, verified_solution_served: false,
149
- external_search: false, amnesia_next_prompt: false,
150
- first_action_after_recall: null, failure_classes: [],
151
- _boundary: firstBoundaryAfter(lineOff),
152
- _recallSeen: false,
153
- };
154
- episodes.push(cur);
168
+ // v2 §D1 — human 프롬프트만 episode 를 연다. 시스템 주입(Skill 로드·이미지·훅 알림)은 제외.
169
+ if(!isHumanPrompt(j,txt)) continue;
170
+ const sig=isSignal(txt), amn=AMNESIA.test(txt), corr=CORRECTION.test(txt);
171
+
172
+ // v2 §D3 — 교정 발화 귀속. recall_called 전제를 제거하고(회상 안 한 쪽이 더 나쁜 신호),
173
+ // filler episode(도구 0건)는 건너뛰어 실제 작업 episode 에 붙인다. compact 경계는 안 넘는다.
174
+ if(amn||corr){
175
+ for(let k=episodes.length-1;k>=0;k--){
176
+ const e=episodes[k];
177
+ if(lineOff>=e._boundary) break; // 이 교정은 e 의 compact 구간 밖 — 더 거슬러 갈 수 없다
178
+ if(!e._sawTool) continue; // filler — 책임을 붙이지 않는다
179
+ if(amn) e.amnesia_next_prompt=true;
180
+ if(corr) e.correction_next_prompt=true;
181
+ break;
182
+ }
155
183
  }
156
- // 회상 신호 아닌 user 프롬프트는 현재 episode 를 이어간다(별 처리 없음, spec §3.2).
184
+
185
+ cur={
186
+ episode_seq: seq++,
187
+ episode_start_byte_offset: lineOff,
188
+ parser_version: 2,
189
+ is_human_prompt: true,
190
+ recall_signal: sig, recall_called: false, verified_solution_served: false,
191
+ external_search: false, amnesia_next_prompt: false, correction_next_prompt: false,
192
+ first_action_after_recall: null, failure_classes: [],
193
+ _boundary: firstBoundaryAfter(lineOff),
194
+ _recallSeen: false,
195
+ _sawTool: false,
196
+ _ts: typeof j.timestamp==="string" ? j.timestamp : null,
197
+ };
198
+ episodes.push(cur);
157
199
  continue;
158
200
  }
159
201
 
@@ -166,6 +208,7 @@ try {
166
208
  if(b.type==="text" && typeof b.text==="string" && b.text.trim()) hadText=true;
167
209
  if(b.type==="tool_use"){
168
210
  sawToolThisMsg=true;
211
+ cur._sawTool=true; // filler episode 판별용(§D3 귀속에서 건너뛸 대상)
169
212
  const name=String(b.name||"");
170
213
  const cmd=b.input&&typeof b.input.command==="string"?b.input.command:"";
171
214
  let action="other";
@@ -203,19 +246,58 @@ try {
203
246
  if(e.verified_solution_served && e.first_action_after_recall==="external_search")
204
247
  fc.push("ignored_verified_solution");
205
248
  if(e.recall_called && e.amnesia_next_prompt) fc.push("user_correction_after_memory_available");
249
+ // v2 §D3 — 교정 발화를 recall 여부로 가른다. 회상조차 안 하고 지적당한 쪽이 더 나쁜 신호다.
250
+ if(e.correction_next_prompt) fc.push(e.recall_called?"correction_after_recall":"correction_after_no_recall");
206
251
  e.failure_classes=fc;
207
- delete e._boundary; delete e._recallSeen;
208
252
  }
209
253
 
210
- // route schema 는 max 500 — 긴 세션이 400 으로 전량 드롭되지 않게 최근 500 episode 만 보낸다(codex P2).
211
- process.stdout.write(JSON.stringify(episodes.slice(-500)));
212
- } catch { process.stdout.write("[]"); }
254
+ // ── v2 §D6: 활성 시각 컷오프 ─────────────────────────────────────────────
255
+ // 파서는 매 Stop 마다 transcript 를 처음부터 재파싱한다. 컷오프가 없으면 배포 후 살아 있는
256
+ // 옛 세션이 **배포 이전 발화까지** v2 행으로 적재해 "v2 = 배포 후" 전제가 깨진다.
257
+ // 활성 시각은 첫 v2 실행 때 한 번만 기록(wx = 원자적, 이미 있으면 유지).
258
+ // ⚠️ **기록에 성공한 시각만 채택한다.** 메모리상의 "지금"을 쓰면 매 Stop 마다 컷오프가
259
+ // 앞으로 밀려(불안정) 무엇이 왜 빠졌는지 사후 확인이 불가능해진다.
260
+ let activatedAt=null;
261
+ try{
262
+ const ap=path.join(bndDir,".v2-activated");
263
+ const readAct=()=>{ try{ const v=JSON.parse(fs.readFileSync(ap,"utf8")).activated_at; return typeof v==="string"?v:null; }catch{ return null; } };
264
+ activatedAt=readAct();
265
+ if(!activatedAt){
266
+ const now=new Date().toISOString();
267
+ try{
268
+ fs.mkdirSync(bndDir,{recursive:true,mode:0o700});
269
+ fs.writeFileSync(ap,JSON.stringify({activated_at:now}),{flag:"wx",mode:0o600});
270
+ activatedAt=now;
271
+ }catch{ activatedAt=readAct(); } // 경쟁(형제가 먼저 만듦)이면 그 값. 손상·쓰기불가면 null.
272
+ }
273
+ }catch{}
274
+ // timestamp 없는/파싱 불가 개시 라인은 **제외**(fail-closed). D1 의 human 판정과 방향이 반대인데
275
+ // 의도적이다 — 저기서는 분모 누락이, 여기서는 baseline 오염이 위험이다.
276
+ // 비교는 문자열이 아니라 epoch ms 로 한다(codex 코드검토 R1 P2): transcript timestamp 는
277
+ // ms 유무·offset 표기가 섞일 수 있어 문자열 대소 비교가 경계에서 틀린다.
278
+ const actMs = activatedAt ? Date.parse(activatedAt) : NaN;
279
+ // 활성 시각을 세우지 못하면(파일 IO·JSON 오류) 전량 emit 하지 않는다 — 컷오프 없는 emit 은
280
+ // 배포 이전 발화를 v2 baseline 에 섞는 것이라 계측 목적 자체를 깬다(fail-closed).
281
+ const emitted = Number.isFinite(actMs)
282
+ ? episodes.filter(e=>{ const t=typeof e._ts==="string"?Date.parse(e._ts):NaN; return Number.isFinite(t) && t>=actMs; })
283
+ : [];
284
+ for(const e of emitted){ delete e._boundary; delete e._recallSeen; delete e._sawTool; delete e._ts; }
285
+
286
+ // route schema 는 max 500 — 긴 세션이 400 으로 전량 드롭되지 않게 최근 500 만 보낸다(codex P2).
287
+ // 버린 개수를 함께 실어 조용한 절단을 관측 가능하게 만든다(§D1).
288
+ const truncated = Math.max(0, emitted.length-500);
289
+ process.stdout.write(JSON.stringify({episodes:emitted.slice(-500), truncated_count:truncated}));
290
+ } catch { process.stdout.write("{\"episodes\":[],\"truncated_count\":0}"); }
213
291
  ' 2>/dev/null)
214
292
 
215
293
  [ -z "$EPISODES" ] && exit 0
216
- [ "$EPISODES" = "[]" ] && exit 0
217
294
 
218
- BODY=$(ROOKIE_SID="$SESSION_ID" ROOKIE_EP="$EPISODES" node -e 'try{process.stdout.write(JSON.stringify({session_id:process.env.ROOKIE_SID,episodes:JSON.parse(process.env.ROOKIE_EP)}))}catch{}' 2>/dev/null)
295
+ # 파서는 {episodes, truncated_count} 를 낸다(v2). episode 0건이면 보낼 것이 없다.
296
+ BODY=$(ROOKIE_SID="$SESSION_ID" ROOKIE_EP="$EPISODES" node -e 'try{
297
+ const p=JSON.parse(process.env.ROOKIE_EP);
298
+ if(!p||!Array.isArray(p.episodes)||p.episodes.length===0) process.exit(0);
299
+ process.stdout.write(JSON.stringify({session_id:process.env.ROOKIE_SID,episodes:p.episodes,truncated_count:p.truncated_count||0}));
300
+ }catch{}' 2>/dev/null)
219
301
  [ -z "$BODY" ] && exit 0
220
302
 
221
303
  # 계측 POST — 세션 종료를 지연시키지 않게 background + fail-soft.
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.13",
3
- "skill_version": "1.34.0"
2
+ "cli_version": "0.4.14",
3
+ "skill_version": "1.35.0"
4
4
  }