therookie 0.4.3
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/LICENSE +21 -0
- package/README.md +63 -0
- package/bin/rookie.js +6 -0
- package/package.json +40 -0
- package/src/api.js +67 -0
- package/src/cli.js +139 -0
- package/src/commands/cluster-todos.js +74 -0
- package/src/commands/codex-setup.js +36 -0
- package/src/commands/context-diet.js +80 -0
- package/src/commands/doctor.js +710 -0
- package/src/commands/done-scan.js +162 -0
- package/src/commands/init.js +175 -0
- package/src/commands/link.js +53 -0
- package/src/commands/login.js +160 -0
- package/src/commands/orphan-extract.js +44 -0
- package/src/commands/save.js +91 -0
- package/src/commands/scan-specs.js +153 -0
- package/src/commands/status.js +44 -0
- package/src/commands/sweep-recovery.js +33 -0
- package/src/commands/token.js +105 -0
- package/src/commands/unlink.js +40 -0
- package/src/commands/update.js +220 -0
- package/src/config.js +65 -0
- package/src/detect.js +109 -0
- package/src/lib/account-ui.js +28 -0
- package/src/lib/apply-tool-setup.js +89 -0
- package/src/lib/codex-config.js +46 -0
- package/src/lib/connect-ui.js +59 -0
- package/src/lib/context-diet.js +628 -0
- package/src/lib/dead-letter.js +73 -0
- package/src/lib/deploy-artifacts.js +185 -0
- package/src/lib/hook-command.js +31 -0
- package/src/lib/manifest.js +87 -0
- package/src/lib/merge-hooks.js +31 -0
- package/src/lib/orphan-extract-core.js +113 -0
- package/src/lib/orphan-pending.js +49 -0
- package/src/lib/parse-hook-cwd.js +11 -0
- package/src/lib/poll-state.js +14 -0
- package/src/lib/redact-lite.js +55 -0
- package/src/lib/resolve-endpoint.js +8 -0
- package/src/lib/self-update.js +79 -0
- package/src/lib/skill-installer.js +55 -0
- package/src/lib/spec-scanner.js +85 -0
- package/src/lib/sweep-core.mjs +423 -0
- package/src/lib/task-path-match.js +122 -0
- package/src/prompt.js +60 -0
- package/templates/hook/bin/posttool-recall-nudge.sh +96 -0
- package/templates/hook/bin/pretool-md-read.sh +107 -0
- package/templates/hook/flush-session.sh +314 -0
- package/templates/hook/guard-destructive-db.sh +24 -0
- package/templates/hook/lib/config-resolve.sh +106 -0
- package/templates/hook/lib/cross-runtime-sweep.sh +202 -0
- package/templates/hook/lib/dispatcher.sh +423 -0
- package/templates/hook/lib/orphan-recovery.sh +491 -0
- package/templates/hook/lib/queue.sh +314 -0
- package/templates/hook/lib/skill-gate.sh +28 -0
- package/templates/hook/lib/transcript.sh +166 -0
- package/templates/hook/refresh-context.sh +163 -0
- package/templates/hook/session-start.sh +417 -0
- package/templates/hook/settings.partial.json +55 -0
- package/templates/hook/sweep-recovery.sh +43 -0
- package/templates/hook/user-prompt-skill-gate.sh +90 -0
- package/templates/launchd/com.rollingmango.rookie-sweep.plist +38 -0
- package/templates/skill/SKILL.md +214 -0
- package/version.json +4 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SessionStart hook — Claude Code 세션 시작 시 1회 발사.
|
|
3
|
+
# spec §37 (2026-05-04): UserPromptSubmit 매 turn 발사 → SessionStart 1회로 이동.
|
|
4
|
+
# - cache_creation 절감 목적 (실측 매 turn ~3K 토큰 invalidate → 0)
|
|
5
|
+
# - user_prompt 기반 hybrid retrieval 자동 inject 폐지 (안 c-3). 차차가 필요 시 직접
|
|
6
|
+
# `/api/rookie/context?user_prompt=...` curl 명시 호출.
|
|
7
|
+
# spec §17 (2026-04-30): status 헬스체크 + 큐 drain + 두목 경고 prefix.
|
|
8
|
+
|
|
9
|
+
# self-reference 방지(spec O-4): 차차 internal LLM 호출(`claude -p` from cli-provider)에서
|
|
10
|
+
# 발사된 hook은 즉시 종료. 두목과의 진짜 대화에서만 동작.
|
|
11
|
+
if [ "$ROOKIE_INTERNAL_INVOCATION" = "1" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# B-3 connection 비콘 — 이 hook 을 등록한 런타임(표면)을 인자로 받는다.
|
|
16
|
+
# Claude Code 등록은 무인자라 기본 claude_code, Codex 등록(codex-setup)은 codex_cli 를 명시 전달.
|
|
17
|
+
# 서버가 x-rookie-surface 를 enum 화이트리스트로 canonicalize(미지원값→local_cli)하므로 안전.
|
|
18
|
+
ROOKIE_SURFACE="${1:-claude_code}"
|
|
19
|
+
|
|
20
|
+
# §17 큐 라이브러리 로드
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
if [ -f "$SCRIPT_DIR/lib/queue.sh" ]; then
|
|
23
|
+
# shellcheck source=/dev/null
|
|
24
|
+
. "$SCRIPT_DIR/lib/queue.sh"
|
|
25
|
+
elif [ -f "$HOME/.rookie/bin/lib/queue.sh" ]; then
|
|
26
|
+
# shellcheck source=/dev/null
|
|
27
|
+
. "$HOME/.rookie/bin/lib/queue.sh"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# spec 2026-06-18 crash-recovery: orphan flush recovery helper (transcript.sh → orphan-recovery.sh).
|
|
31
|
+
# 비정상 종료(Stop hook 미발사) 세션의 transcript 를 다음 SessionStart 에서 사후 회수.
|
|
32
|
+
# B6-1 (spec 2026-07-11): config-resolve.sh — env > 로컬(.rookie/, cwd 상향) > 전역 해석 + identity pin.
|
|
33
|
+
for _lib in transcript.sh orphan-recovery.sh skill-gate.sh config-resolve.sh; do
|
|
34
|
+
if [ -f "$SCRIPT_DIR/lib/$_lib" ]; then
|
|
35
|
+
# shellcheck source=/dev/null
|
|
36
|
+
. "$SCRIPT_DIR/lib/$_lib"
|
|
37
|
+
elif [ -f "$HOME/.rookie/bin/lib/$_lib" ]; then
|
|
38
|
+
# shellcheck source=/dev/null
|
|
39
|
+
. "$HOME/.rookie/bin/lib/$_lib"
|
|
40
|
+
fi
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# stdin 의 source 만 파싱 (SessionStart payload). compact 일 때 작업 카드 더 복구.
|
|
44
|
+
STDIN_BODY=$(cat)
|
|
45
|
+
SOURCE=$(printf '%s' "$STDIN_BODY" | node -e "
|
|
46
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
47
|
+
try { const j = JSON.parse(d); const s=(j.source||'').toString();
|
|
48
|
+
if (['startup','resume','clear','compact'].includes(s)) process.stdout.write(s);
|
|
49
|
+
} catch {}
|
|
50
|
+
});
|
|
51
|
+
" 2>/dev/null || echo "")
|
|
52
|
+
|
|
53
|
+
# spec 2026-06-18 §3.2: 현재 세션 session_id — orphan scanner 가 진행 중 세션을 정확히 제외(1차 P1-3).
|
|
54
|
+
SESSION_ID=$(printf '%s' "$STDIN_BODY" | node -e "
|
|
55
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
56
|
+
try { const j = JSON.parse(d); process.stdout.write((j.session_id || j.sessionId || '').toString()); } catch {}
|
|
57
|
+
});
|
|
58
|
+
" 2>/dev/null || echo "")
|
|
59
|
+
|
|
60
|
+
# 설계 2026-06-24 §3.4: /clear·compact 는 컨텍스트만 비우고 transcript 는 유지되므로, gate 가
|
|
61
|
+
# 과거 Skill('rookie') 호출을 "현재 로드됨"으로 오인할 수 있다. 현재 transcript byte size 를 epoch 로
|
|
62
|
+
# 기록 + loaded marker 무효화 → UserPromptSubmit gate 는 그 offset 이후만 scan(과거 호출 무시).
|
|
63
|
+
if [ "$SOURCE" = "clear" ] || [ "$SOURCE" = "compact" ]; then
|
|
64
|
+
TRANSCRIPT_PATH=$(printf '%s' "$STDIN_BODY" | node -e "
|
|
65
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
66
|
+
try { const j = JSON.parse(d); process.stdout.write((j.transcript_path || j.transcriptPath || '').toString()); } catch {}
|
|
67
|
+
});
|
|
68
|
+
" 2>/dev/null || echo "")
|
|
69
|
+
if command -v skill_gate_record_epoch >/dev/null 2>&1; then
|
|
70
|
+
skill_gate_record_epoch "$SESSION_ID" "$TRANSCRIPT_PATH" || true
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Codex hook 은 실행 cwd 가 프로젝트와 다를 수 있어 stdin cwd 우선(없으면 $PWD — Claude 무변화).
|
|
75
|
+
REPO_PATH=$(printf '%s' "$STDIN_BODY" | node -e "
|
|
76
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
77
|
+
try { const j = JSON.parse(d); const c=(j.cwd||'').toString().trim(); process.stdout.write(c||process.argv[1]); }
|
|
78
|
+
catch { process.stdout.write(process.argv[1]); }
|
|
79
|
+
});
|
|
80
|
+
" "$PWD" 2>/dev/null || echo "$PWD")
|
|
81
|
+
# REPO_URL 은 REPO_PATH 기준(git -C)으로 뽑는다 — hook 실행 cwd 가 프로젝트와 다르면(Codex) 엉뚱한
|
|
82
|
+
# repo_url 이 /context 에 실려 서버 matchProject 가 타 프로젝트 id 를 반환 → done-scan 교차매칭.
|
|
83
|
+
REPO_URL=""
|
|
84
|
+
if command -v git >/dev/null; then
|
|
85
|
+
REPO_URL=$(git -C "$REPO_PATH" remote get-url origin 2>/dev/null || echo "")
|
|
86
|
+
fi
|
|
87
|
+
HOST=$(hostname)
|
|
88
|
+
|
|
89
|
+
# B6-1 (spec 2026-07-11 §B6): config 해석 — env > 프로젝트-로컬(.rookie/, REPO_PATH 상향 탐색) > 전역.
|
|
90
|
+
# REPO_PATH(stdin cwd) 기준이라 Codex 처럼 hook 실행 cwd 가 프로젝트와 달라도 정확한 config 를 문다.
|
|
91
|
+
if command -v rookie_resolve_credentials >/dev/null 2>&1; then
|
|
92
|
+
rookie_resolve_credentials "$REPO_PATH" || exit 0
|
|
93
|
+
ENDPOINT="$ROOKIE_RESOLVED_ENDPOINT"
|
|
94
|
+
TOKEN="$ROOKIE_RESOLVED_TOKEN"
|
|
95
|
+
USER_ID="$ROOKIE_RESOLVED_USER_ID"
|
|
96
|
+
else
|
|
97
|
+
# config-resolve.sh 미배포(구설치) 폴백 — 기존 전역 고정 경로 동작 유지.
|
|
98
|
+
CONFIG="$HOME/.rookie/config.json"
|
|
99
|
+
[ -f "$CONFIG" ] || exit 0
|
|
100
|
+
ENDPOINT=$(node -e "console.log(require('$CONFIG').endpoint)" 2>/dev/null || echo "http://localhost:3000")
|
|
101
|
+
TOKEN=$(node -e "console.log(require('$CONFIG').token)" 2>/dev/null || echo "")
|
|
102
|
+
USER_ID=$(node -e "console.log(require('$CONFIG').user_id || '')" 2>/dev/null || echo "")
|
|
103
|
+
ROOKIE_ACTIVE_CONFIG="$CONFIG"
|
|
104
|
+
fi
|
|
105
|
+
if [ -z "$TOKEN" ]; then
|
|
106
|
+
exit 0
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# B6-3 (spec 2026-07-11 §B6): 세션 identity pinning — 유효 토큰 hash 기록. Stop hook 이 비교해
|
|
110
|
+
# 세션 도중 계정 전환 시 flush 를 중단한다(교차 오염 차단).
|
|
111
|
+
if [ -n "$SESSION_ID" ] && command -v identity_pin_write >/dev/null 2>&1; then
|
|
112
|
+
identity_pin_write "$SESSION_ID" "$TOKEN" || true
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
ENCODE() {
|
|
116
|
+
node -e "console.log(encodeURIComponent(process.argv[1] || ''))" "$1"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# §17 H-1 / H-3: status 5s 헬스체크 → UP/DOWN 분기
|
|
120
|
+
# §18 U-3: status body 도 함께 받아 templates manifest 미스매치 비교
|
|
121
|
+
STATUS_FILE=$(mktemp)
|
|
122
|
+
STATUS_CODE=$(curl -sS --max-time 5 -o "$STATUS_FILE" -w "%{http_code}" \
|
|
123
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
124
|
+
-H "x-rookie-user-id: $USER_ID" \
|
|
125
|
+
"$ENDPOINT/api/rookie/status" 2>/dev/null) || STATUS_CODE="000"
|
|
126
|
+
|
|
127
|
+
QUEUE_BEFORE=0
|
|
128
|
+
if command -v queue_count >/dev/null 2>&1; then
|
|
129
|
+
QUEUE_BEFORE=$(queue_count)
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if [ "$STATUS_CODE" = "200" ]; then
|
|
133
|
+
# §18→spec 2026-05-26: 서버가 manifest 비교·업데이트 지시를 담당한다.
|
|
134
|
+
# 여기선 로컬 manifest hash 만 읽어 context 요청 헤더로 보낸다.
|
|
135
|
+
# LOCAL_HASH="" (옛 install·manifest 없음) → curl 이 빈 값 헤더를 생략 → 서버는 clientHash=null(=conditional 분기)로 처리
|
|
136
|
+
LOCAL_HASH=""
|
|
137
|
+
if [ -f "$HOME/.rookie/manifest.json" ]; then
|
|
138
|
+
LOCAL_HASH=$(node -e "
|
|
139
|
+
try {
|
|
140
|
+
const j = require('$HOME/.rookie/manifest.json');
|
|
141
|
+
process.stdout.write(j.manifest_hash || '');
|
|
142
|
+
} catch {}
|
|
143
|
+
" 2>/dev/null || echo "")
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# §17 H-4: 큐 drain (오래된 작업이 새 컨텍스트보다 우선)
|
|
147
|
+
if command -v queue_drain >/dev/null 2>&1; then
|
|
148
|
+
queue_drain "$ENDPOINT" "$TOKEN" "$USER_ID" >/dev/null 2>&1 || true
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# spec 2026-06-18 §3.5: queue_drain 직후 / context GET 직전 — 비정상 종료 세션 사후 복구.
|
|
152
|
+
# queue 가 먼저 자기 flush marker 를 박은 뒤라 방금 drain 된 세션은 후보에서 제외된다.
|
|
153
|
+
# helper 는 stdout 미사용(§3.7) — 복구 건수만 ROOKIE_ORPHAN_RECOVERED_COUNT 로 받아 요약 합성.
|
|
154
|
+
# spec 2026-06-18 Phase 2 (client-assisted): orphan_recover 는 chunk 만 복구하고,
|
|
155
|
+
# tail fact 추출 대기 세션을 pending 큐로 남긴다. 그 큐를 차차에게 노출해 직접 추출시킨다.
|
|
156
|
+
ORPHAN_SUMMARY=""
|
|
157
|
+
if command -v orphan_recover >/dev/null 2>&1; then
|
|
158
|
+
# 설계 2026-07-02 (Codex 15s SessionStart timeout): orphan_recover 는 비정상 종료 세션의
|
|
159
|
+
# transcript 를 서버에 재전송하는데, backlog 이 쌓이면 세션 수만큼 네트워크 왕복이 동기로
|
|
160
|
+
# 누적돼 hook 이 15s+ 를 점유 → Codex hook timeout. 이 재전송(+sweep)은 세션 진입에 즉시
|
|
161
|
+
# 필요하지 않으므로 백그라운드로 detach 해 hook 을 즉시 리턴시킨다. `( trap '' HUP … ) & disown`
|
|
162
|
+
# 으로 부모 hook 이 exit 해도 자식이 reparent 되어 복구를 계속한다. 이번 세션에서 새로 발견되는
|
|
163
|
+
# orphan 은 이 세션 컨텍스트엔 안 뜨고 다음 세션 pending 목록에 반영된다(detach 의 수용된 trade-off).
|
|
164
|
+
# 서브셸은 부모의 source 된 lib 함수(orphan_recover/sweep_all)를 상속받는다.
|
|
165
|
+
( trap '' HUP
|
|
166
|
+
ROOKIE_ORPHAN_CURRENT_SID="$SESSION_ID" orphan_recover "$ENDPOINT" "$TOKEN" "$USER_ID" >/dev/null 2>&1 || true
|
|
167
|
+
|
|
168
|
+
# spec 2026-06-18: opportunistic 전체 sweep — 하루 1회 (launchd 없이 진입점 backstop).
|
|
169
|
+
# 현재 프로젝트 외 다른 프로젝트 + Codex 까지 따라잡는다. tight budget 으로 SessionStart 지연 최소화.
|
|
170
|
+
# CURRENT_SID 전파로 진행 중 라이브 세션은 sweep 에서도 제외(조기 flush 방지).
|
|
171
|
+
if command -v sweep_all >/dev/null 2>&1 && command -v sweep_due_today >/dev/null 2>&1 && sweep_due_today; then
|
|
172
|
+
ROOKIE_ORPHAN_CURRENT_SID="$SESSION_ID" ROOKIE_SWEEP_TIME_BUDGET="${ROOKIE_SWEEP_TIME_BUDGET:-8}" \
|
|
173
|
+
sweep_all "$ENDPOINT" "$TOKEN" "$USER_ID" >/dev/null 2>&1 || true
|
|
174
|
+
sweep_mark_ran_today
|
|
175
|
+
fi
|
|
176
|
+
) &
|
|
177
|
+
disown 2>/dev/null || true
|
|
178
|
+
|
|
179
|
+
# orphan_pending 은 로컬 marker 읽기(네트워크 X, 빠름)라 동기 유지 — 이미 쌓인 pending 목록을
|
|
180
|
+
# 이번 세션 컨텍스트에 즉시 노출한다. 방금 detach 한 재전송이 추가할 신규 pending 은 다음 세션 몫.
|
|
181
|
+
if command -v orphan_pending_client_extraction >/dev/null 2>&1; then
|
|
182
|
+
# base(Claude) + codex namespace 의 pending 을 모두 모은다 (Phase 3 sweep 가 Codex 세션을
|
|
183
|
+
# codex namespace 에 남기므로 — 차차가 Claude 세션에서 Codex tail 까지 추출하게).
|
|
184
|
+
# P0.1: pending 출력은 JSONL. recovered_at DESC 정렬 후 top-5 만 표시(전체는 rookie doctor).
|
|
185
|
+
# 목록 폭주(243건 류)가 진입 컨텍스트를 오염시키지 않게 cap. 깨진 줄은 fail-soft skip(P3-5).
|
|
186
|
+
PENDING_JSONL=$(orphan_pending_client_extraction 2>/dev/null)
|
|
187
|
+
CODEX_PENDING_JSONL=$(ROOKIE_ORPHAN_DIR="${ROOKIE_ORPHAN_DIR:-$HOME/.rookie/orphan-recovery}/codex" orphan_pending_client_extraction 2>/dev/null)
|
|
188
|
+
ORPHAN_SUMMARY=$(printf '%s\n%s\n' "$PENDING_JSONL" "$CODEX_PENDING_JSONL" | grep . | node -e '
|
|
189
|
+
const lines=require("fs").readFileSync(0,"utf8").trim().split("\n").filter(Boolean);
|
|
190
|
+
const ls=[]; let bad=0;
|
|
191
|
+
for (const l of lines){ try{ ls.push(JSON.parse(l)); }catch{ bad++; } } // 줄 단위 try/catch
|
|
192
|
+
if(!ls.length){ process.exit(0); }
|
|
193
|
+
// oldest-first(FIFO): 가장 오래된 5건을 먼저 처리해 starvation 방지(두목 결정 2026-06-23).
|
|
194
|
+
// 최신순이면 오래된 항목이 top-5 에 영영 안 걸려 TTL prune 전까지 잔류한다.
|
|
195
|
+
ls.sort((a,b)=>(a.recovered_at||"").localeCompare(b.recovered_at||""));
|
|
196
|
+
const top=ls.slice(0,5).map(x=>"- "+x.sid+" :: "+(x.path||"(경로 미상)")).join("\n");
|
|
197
|
+
const oldest=ls[0].recovered_at||"-";
|
|
198
|
+
const skip=bad?" (파싱 실패 "+bad+"건 skip)":"";
|
|
199
|
+
const shown=Math.min(ls.length,5);
|
|
200
|
+
const head="🛟 비정상 종료로 복구된 세션 "+ls.length+"건의 tail 기억(fact·진입카드)이 아직 저장되지 않았습니다"+skip+" (가장 오래됨 "+oldest+", 오래된 "+shown+"건부터 처리 권장 — 전체 목록은 rookie doctor). rookie Skill 의 「복구 세션 tail 추출」 절차대로 rookie orphan-extract --batch=5 로 초안을 뽑아 핵심 working_card/evidence/self_rule/personal_fact 만 batch-mutate 후 orphan_mark_client_extracted <sid> 를 호출하세요:";
|
|
201
|
+
process.stdout.write(head+"\n"+top);
|
|
202
|
+
' 2>/dev/null)
|
|
203
|
+
# B5 (spec 2026-07-11 §B5) — 🛟 블록(orphan tail 추출 지시)이 실제로 노출될 때 served 계측.
|
|
204
|
+
# 서버는 이 블록을 모른다(로컬 marker 발원) — client POST 로 적재. 공용 헬퍼
|
|
205
|
+
# _orphan_directive_metric(백그라운드 fail-soft, surface 헤더 포함)로 acted 쪽과
|
|
206
|
+
# POST 형태를 일원화(리뷰 F13). env 주입으로 이 세션에서 이미 해석한 자격증명 재사용.
|
|
207
|
+
if [ -n "$ORPHAN_SUMMARY" ] && command -v _orphan_directive_metric >/dev/null 2>&1; then
|
|
208
|
+
ROOKIE_ENDPOINT="$ENDPOINT" ROOKIE_TOKEN="$TOKEN" ROOKIE_SURFACE="$ROOKIE_SURFACE" \
|
|
209
|
+
_orphan_directive_metric "served"
|
|
210
|
+
fi
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# §37: user_prompt 파라미터 없음 — hybrid retrieval 자동 inject 폐지 (안 c-3)
|
|
215
|
+
# spec 2026-07-10: inject_budget — Claude Code v2.1.191+ 는 hook 출력 10,000자 초과 시
|
|
216
|
+
# 파일 persist + preview 만 인라인이라, 서버가 이 예산 이하로 lean prompt 를 조립한다.
|
|
217
|
+
# 이 hook 경로만 전달한다 — SKILL.md 의 curl(무예산)은 전문을 받는 복구 경로.
|
|
218
|
+
# 8500 = 10K 임계 − route directive·아래 부가 블록(done 후보·큐 경고·orphan) 여유.
|
|
219
|
+
INJECT_BUDGET=8500
|
|
220
|
+
QS="repo_path=$(ENCODE "$REPO_PATH")&inject_budget=$INJECT_BUDGET"
|
|
221
|
+
if [ -n "$REPO_URL" ]; then
|
|
222
|
+
QS="$QS&repo_url=$(ENCODE "$REPO_URL")"
|
|
223
|
+
fi
|
|
224
|
+
if [ -n "$SOURCE" ]; then
|
|
225
|
+
QS="$QS&source=$(ENCODE "$SOURCE")"
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
RESP_FILE=$(mktemp)
|
|
229
|
+
CTX_CODE=$(curl -sS --max-time 10 \
|
|
230
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
231
|
+
-H "x-rookie-hostname: $HOST" \
|
|
232
|
+
-H "x-rookie-user-id: $USER_ID" \
|
|
233
|
+
-H "x-rookie-surface: $ROOKIE_SURFACE" \
|
|
234
|
+
-H "x-rookie-manifest-hash: $LOCAL_HASH" \
|
|
235
|
+
-o "$RESP_FILE" \
|
|
236
|
+
-w "%{http_code}" \
|
|
237
|
+
"$ENDPOINT/api/rookie/context?$QS" 2>/dev/null) || CTX_CODE="000"
|
|
238
|
+
|
|
239
|
+
if [ "$CTX_CODE" = "200" ]; then
|
|
240
|
+
if command -v queue_record_success >/dev/null 2>&1; then
|
|
241
|
+
queue_record_success
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
QUEUE_AFTER=0
|
|
245
|
+
if command -v queue_count >/dev/null 2>&1; then
|
|
246
|
+
QUEUE_AFTER=$(queue_count)
|
|
247
|
+
fi
|
|
248
|
+
WARN=""
|
|
249
|
+
if [ "$QUEUE_AFTER" -gt 0 ] && command -v queue_warning_prefix >/dev/null 2>&1; then
|
|
250
|
+
WARN=$(queue_warning_prefix "$(last_success_minutes_ago)" "$QUEUE_AFTER")
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# B1-3 (spec 2026-07-11 §B1): 계정 상태줄 — status 응답 account 요약 1줄(+빈-계정 경고).
|
|
254
|
+
# 구서버(account 필드 없음)·파싱 실패 시 빈 값으로 조용히 생략.
|
|
255
|
+
ACCOUNT_LINE=$(node -e "
|
|
256
|
+
try { const j=require(process.argv[1]); const a=j.account||null;
|
|
257
|
+
if (a && (a.email_masked || a.profile_name)) {
|
|
258
|
+
let s='[루키] 계정 '+(a.email_masked||a.profile_name);
|
|
259
|
+
if (typeof a.projects_count==='number') s+=' · 프로젝트 '+a.projects_count;
|
|
260
|
+
if (typeof a.facts_count==='number') s+=' · 기억 '+(a.facts_count>=1000?Math.round(a.facts_count/100)/10+'k':a.facts_count)+'건';
|
|
261
|
+
if (a.projects_count===0 && typeof a.facts_count==='number' && a.facts_count<50) s+='\n⚠️ 이 계정은 비어 있습니다 — 본체 계정이 맞는지 확인하세요 (rookie login)';
|
|
262
|
+
process.stdout.write(s);
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
" "$STATUS_FILE" 2>/dev/null || echo "")
|
|
266
|
+
|
|
267
|
+
# 옵션A (spec 2026-07-01-completion-linkage-remove-vs-improve): 완료 후보 감지.
|
|
268
|
+
# context 로 이미 받은 current_project.id 를 넘겨 done-scan 이 context 재fetch 없이 doing task 를
|
|
269
|
+
# 최근 커밋과 경로 강매칭 → 완료 후보 compact 줄만 stdout. 하루 1회(HEAD 변경 시 우회) throttle.
|
|
270
|
+
# 전역 `rookie` CLI 있을 때만(없으면 조용히 skip — 마무리 reconcile 이 백업). best-effort, 세션 방해 금지.
|
|
271
|
+
DONE_CANDIDATES=""
|
|
272
|
+
if command -v rookie >/dev/null 2>&1; then
|
|
273
|
+
PROJECT_ID=$(node -e "try{const j=require('$RESP_FILE');process.stdout.write((j.current_project&&j.current_project.id)||'')}catch{}" 2>/dev/null || echo "")
|
|
274
|
+
if [ -n "$PROJECT_ID" ]; then
|
|
275
|
+
# REPO_PATH(stdin cwd) 로 이동 후 실행 — hook 실행 cwd 가 프로젝트와 다를 수 있어(Codex 등)
|
|
276
|
+
# done-scan 의 git log 가 엉뚱한 repo 를 보고 프로젝트 A task 를 repo B 커밋과 오매칭하는 것 방지.
|
|
277
|
+
DONE_LINES=$(cd "$REPO_PATH" 2>/dev/null && rookie done-scan --project-id="$PROJECT_ID" 2>/dev/null || true)
|
|
278
|
+
# spec 2026-07-10: 후보 폭주가 additionalContext 10K 임계를 뚫지 않게 상위 5건 cap
|
|
279
|
+
# (session-start 주입 경로 한정 — CLI 직접 실행은 무변경).
|
|
280
|
+
if [ -n "$DONE_LINES" ]; then
|
|
281
|
+
DONE_TOTAL=$(printf '%s\n' "$DONE_LINES" | grep -c .)
|
|
282
|
+
if [ "$DONE_TOTAL" -gt 5 ] 2>/dev/null; then
|
|
283
|
+
DONE_LINES="$(printf '%s\n' "$DONE_LINES" | grep . | head -5)
|
|
284
|
+
…외 $((DONE_TOTAL - 5))건 — 전체는 rookie done-scan"
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
if [ -n "$DONE_LINES" ]; then
|
|
288
|
+
DONE_CANDIDATES=$(printf '## 🎯 완료 후보 감지 (git 강매칭)\n아래 doing task 가 커밋 산출물과 매칭됩니다. 두목께 완료 승인을 여쭙고, 승인 시에만 done 처리하세요(자동 done 금지). 두목이 "아직"이면 해당 task detail 에 `__done_defer:<sha>` 를 추가하세요.\n%s' "$DONE_LINES")
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
# spec 2026-07-10-context-diet-design.md §2.2: 컨텍스트 다이어트 권고 (주 1회).
|
|
294
|
+
# - source=startup 만 (resume/clear/compact 재발화에 권고가 끼어들지 않게)
|
|
295
|
+
# - claude_code 표면만 (Codex 측정은 1차 비목표 + Codex hook 15s timeout 보호)
|
|
296
|
+
# - throttle 검사는 hook(marker 나이 7일), 기록은 CLI 가 판정 성공 후 (실패 세션 미기록 → 재시도)
|
|
297
|
+
DIET_BLOCK=""
|
|
298
|
+
if [ "$SOURCE" = "startup" ] && [ "$ROOKIE_SURFACE" = "claude_code" ]; then
|
|
299
|
+
DIET_LAST="$HOME/.rookie/context-diet.last"
|
|
300
|
+
DIET_DUE="1"
|
|
301
|
+
if [ -f "$DIET_LAST" ]; then
|
|
302
|
+
DIET_DUE=$(node -e "
|
|
303
|
+
try { const { statSync } = require('fs');
|
|
304
|
+
process.stdout.write(Date.now() - statSync(process.argv[1]).mtimeMs > 7*24*60*60*1000 ? '1' : '0');
|
|
305
|
+
} catch { process.stdout.write('1'); }
|
|
306
|
+
" "$DIET_LAST" 2>/dev/null || echo "1")
|
|
307
|
+
fi
|
|
308
|
+
# 글로벌 rookie 미설치면 배포 CLI 최신 버전으로 폴백 (§2.2).
|
|
309
|
+
# sort -V 는 BSD(macOS 기본) sort 미보장이라 node 로 semver 최대값을 고른다.
|
|
310
|
+
DIET_CLI=""
|
|
311
|
+
if command -v rookie >/dev/null 2>&1; then
|
|
312
|
+
DIET_CLI="rookie"
|
|
313
|
+
else
|
|
314
|
+
DIET_VER=$(node -e "
|
|
315
|
+
try { const fs=require('fs');
|
|
316
|
+
const vs=fs.readdirSync(process.env.HOME+'/.rookie/cli').filter(v=>/^\d+\.\d+\.\d+$/.test(v))
|
|
317
|
+
.sort((a,b)=>{const p=a.split('.').map(Number),q=b.split('.').map(Number);return (p[0]-q[0])||(p[1]-q[1])||(p[2]-q[2]);});
|
|
318
|
+
process.stdout.write(vs.pop()||'');
|
|
319
|
+
} catch {}
|
|
320
|
+
" 2>/dev/null || echo "")
|
|
321
|
+
if [ -n "$DIET_VER" ] && [ -f "$HOME/.rookie/cli/$DIET_VER/bin/rookie.js" ]; then
|
|
322
|
+
DIET_CLI="node $HOME/.rookie/cli/$DIET_VER/bin/rookie.js"
|
|
323
|
+
fi
|
|
324
|
+
fi
|
|
325
|
+
if [ "$DIET_DUE" = "1" ] && [ -n "$DIET_CLI" ]; then
|
|
326
|
+
# shellcheck disable=SC2086 — DIET_CLI 는 "node <path>" 2단어 폴백을 허용해야 해서 의도적 무인용
|
|
327
|
+
DIET_JSON=$($DIET_CLI context-diet --json 2>/dev/null || true)
|
|
328
|
+
if [ -n "$DIET_JSON" ]; then
|
|
329
|
+
# 임계 초과 && !snooze 항목만 1줄씩, 블록 전체 ≤1KB (§2.2). 초과 0 이면 무출력.
|
|
330
|
+
DIET_BLOCK=$(printf '%s' "$DIET_JSON" | node -e '
|
|
331
|
+
let d=""; process.stdin.on("data",c=>d+=c).on("end",()=>{
|
|
332
|
+
let j={}; try { j=JSON.parse(d); } catch { return; }
|
|
333
|
+
const it=j.items||{}; const lines=[];
|
|
334
|
+
const num=(n)=>typeof n==="number"?n.toLocaleString("en-US"):"?";
|
|
335
|
+
const hit=(k)=>it[k]&&it[k].exceeded&&!it[k].snoozed;
|
|
336
|
+
if (hit("claude_md_project")) lines.push("- claude_md_project: "+num(it.claude_md_project.chars)+"자 (임계 "+num(it.claude_md_project.threshold)+") — 완료 이력·plan 상세를 docs 이관 권고");
|
|
337
|
+
if (hit("claude_md_total")) lines.push("- claude_md_total: "+num(it.claude_md_total.chars)+"자 (임계 "+num(it.claude_md_total.threshold)+", @import 포함)");
|
|
338
|
+
if (hit("skills_agents_desc")) lines.push("- skills_agents_desc: "+num(it.skills_agents_desc.chars)+"자 (임계 "+num(it.skills_agents_desc.threshold)+")");
|
|
339
|
+
if (hit("rookie_self")) lines.push("- rookie_self: SKILL.md "+num(it.rookie_self.skill_chars)+"자 / 주입 "+(it.rookie_self.injected_chars==null?"미기록":num(it.rookie_self.injected_chars)+"자")+" — 루키 자기 다이어트 후보");
|
|
340
|
+
if (hit("unused_skills")) lines.push("- unused_skills: "+it.unused_skills.count+"개 "+it.unused_skills.window_days+"일간 0회 — 후보 목록: rookie context-diet");
|
|
341
|
+
if (!lines.length) return;
|
|
342
|
+
let out="## 🍞 컨텍스트 다이어트 권고 (주 1회)";
|
|
343
|
+
for (const l of lines) { if ((out+"\n"+l).length>1024) break; out+="\n"+l; }
|
|
344
|
+
process.stdout.write(out);
|
|
345
|
+
});
|
|
346
|
+
' 2>/dev/null || true)
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# SessionStart 컨텍스트 주입 = JSON hookSpecificOutput.additionalContext.
|
|
352
|
+
# spec 2026-07-10 (구 2026-06-08 실측 대체): Claude Code v2.1.191+ 는 additionalContext 가
|
|
353
|
+
# 10,000자를 넘으면 파일 persist + ~2KB preview 만 인라인 주입한다. 서버가 inject_budget
|
|
354
|
+
# 으로 system_prompt 를 8,500자 이하로 조립하고, 여기선 부가 블록까지 합친 최종 길이를
|
|
355
|
+
# 9,800자 게이트로 재검사한다(이중 방어 — 구서버·directive·부가 블록 합산 대비).
|
|
356
|
+
# P0.2: 순서 = system_prompt → done 후보 → queue warning → orphan summary. system_prompt
|
|
357
|
+
# (skill 지시·회상가이드·진입카드)가 먼저 와야 preview 안에 들어가 차차가 첫 응답 전에 본다.
|
|
358
|
+
# 설계 2026-07-02 Codex 컨텍스트 벽: Codex(codex_cli 표면)는 hook 의 additionalContext 를 화면에
|
|
359
|
+
# "hook context" 로 통째 렌더한다(접기 불가). 큰 system_prompt 를 여기서 주입하면 매 세션
|
|
360
|
+
# 컨텍스트 벽이 뜬다. → Codex 에서는 system_prompt 를 MCP `rookie_context` 로 일원화하고, hook 은
|
|
361
|
+
# 결정론 보장용 "rookie_context 호출 지시 한 줄" 만 남긴다. Claude Code(claude_code)는 hook 이
|
|
362
|
+
# 주경로라 전체 주입 유지(무손상). done 후보·서버경고·복구알림 작은 블록은 양쪽 공통으로 유지.
|
|
363
|
+
ROOKIE_SURFACE="$ROOKIE_SURFACE" ROOKIE_ACCOUNT_LINE="$ACCOUNT_LINE" ROOKIE_WARN="$WARN" ROOKIE_ORPHAN_SUMMARY="$ORPHAN_SUMMARY" ROOKIE_DONE_CANDIDATES="$DONE_CANDIDATES" ROOKIE_DIET_BLOCK="$DIET_BLOCK" ROOKIE_SOURCE="$SOURCE" node -e "
|
|
364
|
+
const fs=require('fs');
|
|
365
|
+
const path=require('path');
|
|
366
|
+
const os=require('os');
|
|
367
|
+
let j={}; try { j=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); } catch {}
|
|
368
|
+
const parts=[];
|
|
369
|
+
const isCodex = process.env.ROOKIE_SURFACE === 'codex_cli';
|
|
370
|
+
if (j.system_prompt) {
|
|
371
|
+
if (isCodex) parts.push('[Rookie] 세션 컨텍스트(차차 정체성·회사·프로젝트·진행 카드)는 이 hook 이 직접 주입하지 않습니다. 첫 응답 전에 MCP \`rookie_context\` 툴을 1회 호출해 페르소나와 진행 카드를 로드하세요.');
|
|
372
|
+
else parts.push(j.system_prompt);
|
|
373
|
+
}
|
|
374
|
+
// spec 2026-07-10 최종 길이 게이트: system_prompt 는 그대로 두고(예산은 서버 책임),
|
|
375
|
+
// 부가 블록은 합산 9,800자 이내까지만 append. 초과 블록은 생략 + 안내 1줄.
|
|
376
|
+
const GATE=9800;
|
|
377
|
+
const extras=[];
|
|
378
|
+
// B1-3: 계정 상태줄이 첫 extra — 오-binding 신호가 preview 에 최대한 가깝게.
|
|
379
|
+
if (process.env.ROOKIE_ACCOUNT_LINE) extras.push(process.env.ROOKIE_ACCOUNT_LINE);
|
|
380
|
+
if (process.env.ROOKIE_DONE_CANDIDATES) extras.push(process.env.ROOKIE_DONE_CANDIDATES);
|
|
381
|
+
if (process.env.ROOKIE_WARN) extras.push(process.env.ROOKIE_WARN);
|
|
382
|
+
if (process.env.ROOKIE_ORPHAN_SUMMARY) extras.push(process.env.ROOKIE_ORPHAN_SUMMARY);
|
|
383
|
+
if (process.env.ROOKIE_DIET_BLOCK) extras.push(process.env.ROOKIE_DIET_BLOCK);
|
|
384
|
+
let ctx=parts.join('\n\n');
|
|
385
|
+
let dropped=false;
|
|
386
|
+
for (const b of extras) {
|
|
387
|
+
const next = ctx ? ctx+'\n\n'+b : b;
|
|
388
|
+
if (next.length > GATE) { dropped=true; continue; }
|
|
389
|
+
ctx = next;
|
|
390
|
+
}
|
|
391
|
+
if (dropped) ctx = ctx ? ctx+'\n\n⚠️ (부가 알림 일부 생략 — rookie doctor 로 확인)' : '⚠️ (부가 알림 일부 생략 — rookie doctor 로 확인)';
|
|
392
|
+
// spec 2026-07-10-context-diet §2.2: 실주입 크기 기록 — context-diet rookie_self 판정 입력.
|
|
393
|
+
try {
|
|
394
|
+
fs.writeFileSync(path.join(os.homedir(),'.rookie','last-injection.json'), JSON.stringify({
|
|
395
|
+
bytes: Buffer.byteLength(ctx,'utf8'), chars: ctx.length,
|
|
396
|
+
at: new Date().toISOString(), source: process.env.ROOKIE_SOURCE||'', hook_version: '2026-07-10'
|
|
397
|
+
}));
|
|
398
|
+
} catch {}
|
|
399
|
+
if (ctx) process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:ctx}}));
|
|
400
|
+
" "$RESP_FILE" 2>/dev/null || true
|
|
401
|
+
else
|
|
402
|
+
if command -v queue_warning_prefix >/dev/null 2>&1; then
|
|
403
|
+
WARN=$(queue_warning_prefix "$(last_success_minutes_ago)" "$QUEUE_BEFORE")
|
|
404
|
+
ROOKIE_WARN="$WARN" node -e "const c=process.env.ROOKIE_WARN||''; if(c) process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:c}}))" 2>/dev/null || true
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
rm -f "$RESP_FILE"
|
|
408
|
+
else
|
|
409
|
+
# §17 H-3: DOWN — 두목께 경고 노출 (silent 금지). additionalContext JSON 으로 통일.
|
|
410
|
+
if command -v queue_warning_prefix >/dev/null 2>&1; then
|
|
411
|
+
WARN=$(queue_warning_prefix "$(last_success_minutes_ago)" "$QUEUE_BEFORE")
|
|
412
|
+
ROOKIE_WARN="$WARN" node -e "const c=process.env.ROOKIE_WARN||''; if(c) process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:c}}))" 2>/dev/null || true
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
rm -f "$STATUS_FILE"
|
|
417
|
+
exit 0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"Stop": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": ".*",
|
|
6
|
+
"__rookie_managed": true,
|
|
7
|
+
"hooks": [
|
|
8
|
+
{ "type": "command", "command": "__FLUSH_SESSION_CMD__" }
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"SessionStart": [
|
|
13
|
+
{
|
|
14
|
+
"matcher": ".*",
|
|
15
|
+
"__rookie_managed": true,
|
|
16
|
+
"hooks": [
|
|
17
|
+
{ "type": "command", "command": "__SESSION_START_CMD__" }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"PreToolUse": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Read|Bash",
|
|
24
|
+
"__rookie_managed": true,
|
|
25
|
+
"hooks": [
|
|
26
|
+
{ "type": "command", "command": "__PRETOOL_MD_READ_CMD__" }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"__rookie_managed": true,
|
|
32
|
+
"hooks": [
|
|
33
|
+
{ "type": "command", "command": "__GUARD_DESTRUCTIVE_DB_CMD__" }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"PostToolUse": [
|
|
38
|
+
{
|
|
39
|
+
"matcher": "Read|Bash|Grep",
|
|
40
|
+
"__rookie_managed": true,
|
|
41
|
+
"hooks": [
|
|
42
|
+
{ "type": "command", "command": "__POSTTOOL_RECALL_NUDGE_CMD__" }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"UserPromptSubmit": [
|
|
47
|
+
{
|
|
48
|
+
"__rookie_managed": true,
|
|
49
|
+
"hooks": [
|
|
50
|
+
{ "type": "command", "command": "__USER_PROMPT_SKILL_GATE_CMD__", "timeout": 3 }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# sweep-recovery.sh — Phase 3 cross-runtime sweep 진입점.
|
|
3
|
+
# spec docs/superpowers/specs/2026-06-18-crash-recovery-orphan-flush.md §4 Phase 3.
|
|
4
|
+
# launchd 가 주기 실행(또는 수동)하여 Claude 전체 프로젝트 + Codex 세션을 사후 회수(chunk 복구).
|
|
5
|
+
# fact 는 Phase 2 client-assisted(pending → 다음 Claude 세션의 차차)로 위임.
|
|
6
|
+
# SessionStart 의 orphan_recover(현재 프로젝트만)와 달리, 이 sweep 은 모든 프로젝트·Codex 의 backstop.
|
|
7
|
+
|
|
8
|
+
# self-reference 방지(spec O-4): 차차 internal LLM 호출에서 발사되면 즉시 종료.
|
|
9
|
+
[ "$ROOKIE_INTERNAL_INVOCATION" = "1" ] && exit 0
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
for _lib in config-resolve.sh transcript.sh orphan-recovery.sh cross-runtime-sweep.sh; do
|
|
13
|
+
if [ -f "$SCRIPT_DIR/lib/$_lib" ]; then
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
. "$SCRIPT_DIR/lib/$_lib"
|
|
16
|
+
elif [ -f "$HOME/.rookie/bin/lib/$_lib" ]; then
|
|
17
|
+
# shellcheck source=/dev/null
|
|
18
|
+
. "$HOME/.rookie/bin/lib/$_lib"
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
# B6-1 (spec 2026-07-11 §B6): config 해석 — env > 로컬(.rookie/, 실행 cwd 상향) > 전역.
|
|
23
|
+
# launchd 실행은 보통 $HOME cwd → 전역 config. B6-4 partition 이 로컬 config 소속 transcript 를 걸러낸다.
|
|
24
|
+
if command -v rookie_resolve_credentials >/dev/null 2>&1; then
|
|
25
|
+
rookie_resolve_credentials "$PWD" || exit 0
|
|
26
|
+
ENDPOINT="$ROOKIE_RESOLVED_ENDPOINT"
|
|
27
|
+
TOKEN="$ROOKIE_RESOLVED_TOKEN"
|
|
28
|
+
USER_ID="$ROOKIE_RESOLVED_USER_ID"
|
|
29
|
+
else
|
|
30
|
+
CONFIG="$HOME/.rookie/config.json"
|
|
31
|
+
[ -f "$CONFIG" ] || exit 0
|
|
32
|
+
ENDPOINT=$(node -e "console.log(require('$CONFIG').endpoint)" 2>/dev/null || echo "")
|
|
33
|
+
TOKEN=$(node -e "console.log(require('$CONFIG').token)" 2>/dev/null || echo "")
|
|
34
|
+
USER_ID=$(node -e "console.log(require('$CONFIG').user_id || '')" 2>/dev/null || echo "")
|
|
35
|
+
fi
|
|
36
|
+
[ -n "$ENDPOINT" ] && [ -n "$TOKEN" ] || exit 0
|
|
37
|
+
|
|
38
|
+
command -v sweep_all >/dev/null 2>&1 || exit 0
|
|
39
|
+
sweep_all "$ENDPOINT" "$TOKEN" "$USER_ID" || true
|
|
40
|
+
|
|
41
|
+
# launchd StandardOutPath 로 1줄 요약 (회수 건수). 복구된 fact 추출은 다음 Claude 세션의 차차 몫.
|
|
42
|
+
printf '%s sweep recovered=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${ROOKIE_SWEEP_TOTAL:-0}"
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# UserPromptSubmit hook — rookie Skill 미로드 세션에 "먼저 Skill 호출" 상기 주입.
|
|
3
|
+
# 설계: docs/superpowers/plans/2026-06-24-sessionstart-skill-load-enforcement.md
|
|
4
|
+
# - 로드 감지 = transcript 에서 Skill(rookie) tool_use 를 semantic 파싱 (grep 고정문자열 아님).
|
|
5
|
+
# - hook-owned marker 캐시 = ~/.rookie/skill-gate/<sha256(session_id)>.loaded (로드 후 조용).
|
|
6
|
+
# - epoch = /clear·compact 후 과거 호출 무시 (SessionStart 가 .epoch 기록 → 그 byte offset 이후만 scan).
|
|
7
|
+
# - fail-soft: 어떤 실패도 stderr 없이 안내 주입 또는 조용 종료.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
# self-reference 방지: 차차 internal LLM 호출(claude -p)에서 발사된 hook 은 즉시 종료.
|
|
11
|
+
[ "${ROOKIE_INTERNAL_INVOCATION:-}" = "1" ] && exit 0
|
|
12
|
+
|
|
13
|
+
# B6-1 (spec 2026-07-11 §B6): config 해석 — env > 프로젝트-로컬(.rookie/, cwd 상향) > 전역.
|
|
14
|
+
# UserPromptSubmit hook 은 프로젝트 cwd 에서 발사되므로 $PWD 기준. lib 미배포(구설치)면 전역 폴백.
|
|
15
|
+
_GATE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if [ -f "$_GATE_SCRIPT_DIR/lib/config-resolve.sh" ]; then
|
|
17
|
+
# shellcheck source=/dev/null
|
|
18
|
+
. "$_GATE_SCRIPT_DIR/lib/config-resolve.sh"
|
|
19
|
+
elif [ -f "$HOME/.rookie/bin/lib/config-resolve.sh" ]; then
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
. "$HOME/.rookie/bin/lib/config-resolve.sh"
|
|
22
|
+
fi
|
|
23
|
+
if command -v rookie_resolve_credentials >/dev/null 2>&1; then
|
|
24
|
+
rookie_resolve_credentials "$PWD" || exit 0
|
|
25
|
+
TOKEN="$ROOKIE_RESOLVED_TOKEN"
|
|
26
|
+
else
|
|
27
|
+
CONFIG="$HOME/.rookie/config.json"
|
|
28
|
+
[ -f "$CONFIG" ] || exit 0
|
|
29
|
+
TOKEN=$(node -e "try{process.stdout.write(String(require('$CONFIG').token||''))}catch{}" 2>/dev/null)
|
|
30
|
+
fi
|
|
31
|
+
[ -n "$TOKEN" ] || exit 0
|
|
32
|
+
|
|
33
|
+
GATE_DIR="${ROOKIE_SKILL_GATE_DIR:-$HOME/.rookie/skill-gate}"
|
|
34
|
+
|
|
35
|
+
# stdin(UserPromptSubmit payload) → node 가 전부 처리(파싱·sha256·marker·epoch·scan·신호·출력 결정).
|
|
36
|
+
# node 단일 덩어리라 fail-soft·stderr 억제가 쉽다. code 안에 작은따옴표 미사용(bash single-quote 보호).
|
|
37
|
+
# 두 갈래(한 turn 한 메시지): skill 미로드 → SKILL_MSG / 로드됨 + 과거-방법 회상 신호 → RECALL_MSG.
|
|
38
|
+
# marker(.loaded)는 "로드됨 캐시"일 뿐 early-return 아님 — 로드 뒤에도 프롬프트 신호를 매 turn 검사(spec 2026-07-07-recall-first §3.1).
|
|
39
|
+
cat | ROOKIE_GATE_DIR="$GATE_DIR" node -e '
|
|
40
|
+
let d="";
|
|
41
|
+
process.stdin.on("data",c=>d+=c).on("end",()=>{
|
|
42
|
+
const SKILL_MSG="⚠️ 이 세션에서 아직 rookie Skill 을 로드하지 않았습니다. 다른 작업·응답 전에 먼저 rookie Skill 을 호출하세요 — 회상(recall)·미link 등록 제안·기억 저장 절차가 그 안에 있습니다. (이미 호출했다면 이 안내는 사라집니다.)";
|
|
43
|
+
const RECALL_MSG="🧠 이 프롬프트에 과거 맥락·방법 회상 신호가 있습니다. grep·파일 열람·자기판단·두목 되묻기 이전에 GET /api/rookie/recall 을 먼저 호출하세요 (brand.ts 회상-순서 룰).";
|
|
44
|
+
const injectMsg=(msg)=>{ try{ process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:msg}})); }catch{} };
|
|
45
|
+
try{
|
|
46
|
+
const fs=require("fs"), path=require("path"), crypto=require("crypto");
|
|
47
|
+
const gateDir=process.env.ROOKIE_GATE_DIR;
|
|
48
|
+
const p=JSON.parse(d);
|
|
49
|
+
const sid=(p.session_id||"").toString();
|
|
50
|
+
const tp=(p.transcript_path||"").toString();
|
|
51
|
+
const prompt=(p.prompt||"").toString();
|
|
52
|
+
// recall 신호(과거 방법 재현) — 과거형 집중. 현재형(구현해)·미래순서(배포 전에/이전에)·조건형(했으면) 제외.
|
|
53
|
+
const PAST_TIME=/어제|그때|저번|지난번|예전/;
|
|
54
|
+
const PAST_HOW=/어떻게[\s\S]{0,15}?(했|였|만들었|제작했|구현했|처리했|뽑았|짰|썼|캡[처쳐]했)(?!으면|으려|으실|으세|었으면)|어떻게\s*(하더라|했더라|됐)/;
|
|
55
|
+
const REPRO=/재현|기억\s*나|했었(?!으면)/;
|
|
56
|
+
// spec 2026-07-10 §3.B1 — SKILL.md 회상 표와 정합 확장(작업 연속·결정 회상·환경 질의).
|
|
57
|
+
const CONTINUE=/이어서|이어가|계속\s*(하|해|진행)|마저|어디까지|재개|하던\s*(거|것|작업)/;
|
|
58
|
+
const DECIDED=/뭐였|뭐더라|왜\s*였|왜\s*그랬|어떻게\s*됐|결정했|정했었/;
|
|
59
|
+
// ENVQ 는 명사 단독 매칭 금지(코드 얘기 중 "포트" 빈발) — 의문 신호 동반 필수.
|
|
60
|
+
const ENVQ=/(포트|유저명|계정|경로|도메인|호스트|엔드포인트|서버\s*주소)[^\n]{0,12}(뭐|몇|어디|알아|기억|였)/;
|
|
61
|
+
const recallSignal = PAST_TIME.test(prompt)||PAST_HOW.test(prompt)||REPRO.test(prompt)||CONTINUE.test(prompt)||DECIDED.test(prompt)||ENVQ.test(prompt);
|
|
62
|
+
const key= sid ? crypto.createHash("sha256").update(sid).digest("hex") : "";
|
|
63
|
+
let skillLoaded=false;
|
|
64
|
+
if(key && fs.existsSync(path.join(gateDir,key+".loaded"))){ skillLoaded=true; } // marker = 로드됨 캐시(재scan 회피), early-return 아님
|
|
65
|
+
else {
|
|
66
|
+
let offset=0;
|
|
67
|
+
if(key){ try{ const n=parseInt(String(fs.readFileSync(path.join(gateDir,key+".epoch"),"utf8")).trim(),10); if(Number.isFinite(n)&&n>0) offset=n; }catch{} }
|
|
68
|
+
let text=null;
|
|
69
|
+
try{ text=fs.readFileSync(tp).slice(offset).toString("utf8"); }catch{ text=null; } // transcript 없음 → 미로드 취급
|
|
70
|
+
if(text!==null){
|
|
71
|
+
for(const line of text.split("\n")){
|
|
72
|
+
const t=line.trim(); if(!t) continue;
|
|
73
|
+
let j; try{ j=JSON.parse(t); }catch{ continue; }
|
|
74
|
+
const m=(j&&j.message)||j;
|
|
75
|
+
const c=m&&m.content; const blocks=Array.isArray(c)?c:[];
|
|
76
|
+
for(const b of blocks){ if(b&&b.type==="tool_use"&&b.name==="Skill"&&b.input&&b.input.skill==="rookie"){ skillLoaded=true; break; } }
|
|
77
|
+
if(!skillLoaded && j&&j.name==="Skill"&&j.input&&j.input.skill==="rookie") skillLoaded=true;
|
|
78
|
+
if(skillLoaded) break;
|
|
79
|
+
}
|
|
80
|
+
if(skillLoaded && key){ try{ fs.mkdirSync(gateDir,{recursive:true,mode:0o700}); fs.writeFileSync(path.join(gateDir,key+".loaded"),JSON.stringify({session_id:sid,transcript_path:tp,loaded_at:new Date().toISOString()}),{mode:0o600}); }catch{} }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if(!skillLoaded) return injectMsg(SKILL_MSG); // 미로드 → skill 리마인더 (recall 리마인더 안 함, 한 turn 한 메시지)
|
|
84
|
+
if(recallSignal) return injectMsg(RECALL_MSG); // 로드됨 + 과거-방법 신호 → recall 리마인더
|
|
85
|
+
return; // 로드됨 + 신호 없음 → 조용
|
|
86
|
+
}catch{ return injectMsg(SKILL_MSG); } // fail-soft
|
|
87
|
+
});
|
|
88
|
+
' 2>/dev/null
|
|
89
|
+
|
|
90
|
+
exit 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<!--
|
|
4
|
+
Phase 3 sweep — OPTIONAL "상시 백그라운드 모드" (spec 2026-06-18-crash-recovery-orphan-flush §4).
|
|
5
|
+
|
|
6
|
+
⚠️ 기본값 아님. 기본 backstop 은 **진입점 opportunistic sweep(하루 1회)** 이다:
|
|
7
|
+
Claude SessionStart + (후속) Codex rookie_context + 수동 CLI 에서 sweep_due_today 가드로 실행.
|
|
8
|
+
그래야 OS 스케줄러 없이도, 꺼져 있던 PC도 다음 사용 때 따라잡는다(맥 전용 문제 회피).
|
|
9
|
+
이 launchd 는 "Claude/Codex 를 한동안 안 열어도 상시 자동 회수" 를 원할 때만 켜는 macOS 전용 옵션.
|
|
10
|
+
(Linux=systemd timer/cron, Windows=작업 스케줄러 — 별도. 이식 가능한 sweep core 는 후속 Node CLI.)
|
|
11
|
+
|
|
12
|
+
__SWEEP_CMD__ = 설치된 sweep-recovery.sh 절대경로 (rookie-cli 가 치환).
|
|
13
|
+
켜기(두목 승인 후 수동): ~/Library/LaunchAgents/com.rollingmango.rookie-sweep.plist 로 복사 →
|
|
14
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.rollingmango.rookie-sweep.plist
|
|
15
|
+
끄기: launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.rollingmango.rookie-sweep.plist
|
|
16
|
+
상태: launchctl print gui/$(id -u)/com.rollingmango.rookie-sweep
|
|
17
|
+
로그: ~/Library/Logs/Rookie/sweep.log / sweep.err
|
|
18
|
+
StartInterval 21600 = 6시간마다. RunAtLoad=true 로 등록 직후 1회.
|
|
19
|
+
-->
|
|
20
|
+
<plist version="1.0">
|
|
21
|
+
<dict>
|
|
22
|
+
<key>Label</key>
|
|
23
|
+
<string>com.rollingmango.rookie-sweep</string>
|
|
24
|
+
<key>ProgramArguments</key>
|
|
25
|
+
<array>
|
|
26
|
+
<string>/bin/bash</string>
|
|
27
|
+
<string>__SWEEP_CMD__</string>
|
|
28
|
+
</array>
|
|
29
|
+
<key>RunAtLoad</key>
|
|
30
|
+
<true/>
|
|
31
|
+
<key>StartInterval</key>
|
|
32
|
+
<integer>21600</integer>
|
|
33
|
+
<key>StandardOutPath</key>
|
|
34
|
+
<string>__HOME__/Library/Logs/Rookie/sweep.log</string>
|
|
35
|
+
<key>StandardErrorPath</key>
|
|
36
|
+
<string>__HOME__/Library/Logs/Rookie/sweep.err</string>
|
|
37
|
+
</dict>
|
|
38
|
+
</plist>
|