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,314 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Stop hook — 매 assistant 응답 종료마다 Rookie 앱으로 transcript 플러시.
|
|
3
|
+
# 입력: stdin JSON with session_id (Claude Code Stop hook 계약).
|
|
4
|
+
# spec §17 (2026-04-30): silent failure 차단 + 로컬 큐 + 회복 flush.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# self-reference 방지(spec O-4) 1차 방어선:
|
|
9
|
+
# 차차 internal LLM 호출(`claude -p` from cli-provider)에서 발사된 hook은 즉시 종료.
|
|
10
|
+
if [ "$ROOKIE_INTERNAL_INVOCATION" = "1" ]; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# §17 큐 라이브러리 로드 (best-effort — 미발견 시에도 hook 자체는 동작)
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
|
|
17
|
+
# B6-1 (spec 2026-07-11 §B6): config 해석 — env > 프로젝트-로컬(.rookie/, cwd 상향) > 전역.
|
|
18
|
+
# Stop hook 은 프로젝트 cwd 에서 발사되므로 $PWD 기준. config-resolve.sh 미배포(구설치)면 전역 폴백.
|
|
19
|
+
if [ -f "$SCRIPT_DIR/lib/config-resolve.sh" ]; then
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
. "$SCRIPT_DIR/lib/config-resolve.sh"
|
|
22
|
+
elif [ -f "$HOME/.rookie/bin/lib/config-resolve.sh" ]; then
|
|
23
|
+
# shellcheck source=/dev/null
|
|
24
|
+
. "$HOME/.rookie/bin/lib/config-resolve.sh"
|
|
25
|
+
fi
|
|
26
|
+
if command -v rookie_resolve_credentials >/dev/null 2>&1; then
|
|
27
|
+
rookie_resolve_credentials "$PWD" || exit 0
|
|
28
|
+
ENDPOINT="$ROOKIE_RESOLVED_ENDPOINT"
|
|
29
|
+
TOKEN="$ROOKIE_RESOLVED_TOKEN"
|
|
30
|
+
USER_ID="$ROOKIE_RESOLVED_USER_ID"
|
|
31
|
+
else
|
|
32
|
+
CONFIG="$HOME/.rookie/config.json"
|
|
33
|
+
if [ ! -f "$CONFIG" ]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
ENDPOINT=$(node -e "console.log(require('$CONFIG').endpoint)" 2>/dev/null || echo "http://localhost:3000")
|
|
37
|
+
TOKEN=$(node -e "console.log(require('$CONFIG').token)" 2>/dev/null || echo "")
|
|
38
|
+
USER_ID=$(node -e "console.log(require('$CONFIG').user_id || '')" 2>/dev/null || echo "")
|
|
39
|
+
fi
|
|
40
|
+
if [ -z "$TOKEN" ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
if [ -f "$SCRIPT_DIR/lib/queue.sh" ]; then
|
|
44
|
+
# shellcheck source=/dev/null
|
|
45
|
+
. "$SCRIPT_DIR/lib/queue.sh"
|
|
46
|
+
elif [ -f "$HOME/.rookie/bin/lib/queue.sh" ]; then
|
|
47
|
+
# shellcheck source=/dev/null
|
|
48
|
+
. "$HOME/.rookie/bin/lib/queue.sh"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# §17.2 H-2.1/H-2.2 dispatcher 라이브러리 로드 (queue.sh 보다 뒤 — _safe_enqueue 가 queue_enqueue 의존)
|
|
52
|
+
if [ -f "$SCRIPT_DIR/lib/dispatcher.sh" ]; then
|
|
53
|
+
# shellcheck source=/dev/null
|
|
54
|
+
. "$SCRIPT_DIR/lib/dispatcher.sh"
|
|
55
|
+
elif [ -f "$HOME/.rookie/bin/lib/dispatcher.sh" ]; then
|
|
56
|
+
# shellcheck source=/dev/null
|
|
57
|
+
. "$HOME/.rookie/bin/lib/dispatcher.sh"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# spec 2026-06-18 crash-recovery: transcript 변환 공유 helper + orphan marker.
|
|
61
|
+
# transcript.sh(§3.8 messages 추출 단일화) → orphan-recovery.sh(content-bearing marker).
|
|
62
|
+
for _lib in transcript.sh orphan-recovery.sh; do
|
|
63
|
+
if [ -f "$SCRIPT_DIR/lib/$_lib" ]; then
|
|
64
|
+
# shellcheck source=/dev/null
|
|
65
|
+
. "$SCRIPT_DIR/lib/$_lib"
|
|
66
|
+
elif [ -f "$HOME/.rookie/bin/lib/$_lib" ]; then
|
|
67
|
+
# shellcheck source=/dev/null
|
|
68
|
+
. "$HOME/.rookie/bin/lib/$_lib"
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
STDIN_BODY=$(cat)
|
|
73
|
+
|
|
74
|
+
SESSION_ID=$(echo "$STDIN_BODY" | node -e "
|
|
75
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
76
|
+
try { const j = JSON.parse(d); console.log(j.session_id || j.sessionId || ''); } catch { console.log(''); }
|
|
77
|
+
})
|
|
78
|
+
")
|
|
79
|
+
|
|
80
|
+
if [ -z "$SESSION_ID" ]; then
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# B6-3 (spec 2026-07-11 §B6): 세션 identity pinning 비교 — SessionStart 가 기록한 유효 토큰
|
|
85
|
+
# hash 와 현재 토큰이 다르면(세션 도중 계정 전환) flush 를 중단하고 경고 로그를 남긴다.
|
|
86
|
+
# 진행 중 세션의 기억이 새 계정으로 오귀속되는 교차 오염 차단. pin 없음(구세션)은 통과.
|
|
87
|
+
if command -v identity_pin_check >/dev/null 2>&1; then
|
|
88
|
+
if ! identity_pin_check "$SESSION_ID" "$TOKEN"; then
|
|
89
|
+
identity_pin_log_mismatch "$SESSION_ID" "stop" || true
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
TRANSCRIPT_PATH=$(echo "$STDIN_BODY" | node -e "
|
|
95
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
96
|
+
try { const j = JSON.parse(d); console.log(j.transcript_path || ''); } catch { console.log(''); }
|
|
97
|
+
})
|
|
98
|
+
")
|
|
99
|
+
|
|
100
|
+
# self-reference 방지(spec O-4) 2차 방어선:
|
|
101
|
+
# env 마커가 끊어진 경우라도 transcript 첫 4KB에 fact 추출 system prompt 시그니처가 있으면 skip.
|
|
102
|
+
# 시그니처 = src/lib/context/fact-extraction.ts FACT_EXTRACTION_MARKER. (lib/transcript.sh 공유)
|
|
103
|
+
if command -v transcript_has_self_reference >/dev/null 2>&1; then
|
|
104
|
+
if transcript_has_self_reference "$TRANSCRIPT_PATH"; then
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
elif [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
108
|
+
if head -c 4096 "$TRANSCRIPT_PATH" | grep -q "the Rookie의 기억 정리 모듈"; then
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
REPO_URL=""
|
|
114
|
+
if command -v git >/dev/null; then
|
|
115
|
+
REPO_URL=$(git remote get-url origin 2>/dev/null || echo "")
|
|
116
|
+
fi
|
|
117
|
+
REPO_PATH="$PWD"
|
|
118
|
+
HOST=$(hostname)
|
|
119
|
+
|
|
120
|
+
# spec §27 D-Multimac-1: 멀티 mac 운영에서 Rookie 서버는 hook 발사 mac 의 transcript file 을
|
|
121
|
+
# read 할 수 없다. hook 측에서 jsonl 을 직접 read → messages array → POST body 본문 송신.
|
|
122
|
+
# turn limit 200 (D-Multimac-3). spec 2026-06-18 §3.8: 추출 로직을 lib/transcript.sh 로 단일화
|
|
123
|
+
# (session-start.sh orphan recovery 와 동일 구현 보장). helper 미로드 시 인라인 폴백 유지.
|
|
124
|
+
MESSAGES_JSON='[]'
|
|
125
|
+
if command -v transcript_messages_json >/dev/null 2>&1; then
|
|
126
|
+
MESSAGES_JSON=$(transcript_messages_json "$TRANSCRIPT_PATH")
|
|
127
|
+
elif [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
128
|
+
MESSAGES_JSON=$(node -e "
|
|
129
|
+
const fs = require('node:fs');
|
|
130
|
+
const path = process.argv[1];
|
|
131
|
+
if (!path || !fs.existsSync(path)) { console.log('[]'); process.exit(0); }
|
|
132
|
+
let raw;
|
|
133
|
+
try { raw = fs.readFileSync(path, 'utf8'); } catch { console.log('[]'); process.exit(0); }
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const line of raw.split('\n')) {
|
|
136
|
+
const t = line.trim(); if (!t) continue;
|
|
137
|
+
try {
|
|
138
|
+
const j = JSON.parse(t);
|
|
139
|
+
const m = j.message || j;
|
|
140
|
+
if (m.role !== 'user' && m.role !== 'assistant') continue;
|
|
141
|
+
let c = m.content;
|
|
142
|
+
if (typeof c !== 'string') {
|
|
143
|
+
if (Array.isArray(c)) {
|
|
144
|
+
c = c.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
|
145
|
+
.map(b => b.text).join('\n').trim();
|
|
146
|
+
} else { c = ''; }
|
|
147
|
+
} else { c = c.trim(); }
|
|
148
|
+
if (!c) continue;
|
|
149
|
+
out.push({ role: m.role, content: c });
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
const TURN_LIMIT = 200;
|
|
153
|
+
let pairs = 0;
|
|
154
|
+
for (let i = 1; i < out.length; i++) {
|
|
155
|
+
if (out[i-1].role === 'user' && out[i].role === 'assistant') pairs++;
|
|
156
|
+
}
|
|
157
|
+
if (pairs > TURN_LIMIT) {
|
|
158
|
+
let dropPairs = pairs - TURN_LIMIT;
|
|
159
|
+
let dropIdx = 0;
|
|
160
|
+
while (dropPairs > 0 && dropIdx < out.length - 1) {
|
|
161
|
+
if (out[dropIdx].role === 'user' && out[dropIdx+1].role === 'assistant') {
|
|
162
|
+
dropIdx += 2; dropPairs--;
|
|
163
|
+
} else { dropIdx++; }
|
|
164
|
+
}
|
|
165
|
+
out.splice(0, dropIdx);
|
|
166
|
+
}
|
|
167
|
+
console.log(JSON.stringify(out));
|
|
168
|
+
" "$TRANSCRIPT_PATH" 2>/dev/null || echo "[]")
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# spec 2026-06-18 §3.2 marker 경로 1: 정상 flush-session 의 content-bearing success 시
|
|
172
|
+
# orphan flushed marker(source=stop) 생성 → 다음 SessionStart orphan scanner 가 이 세션 제외.
|
|
173
|
+
MESSAGE_COUNT=$(printf '%s' "$MESSAGES_JSON" | node -e "
|
|
174
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
175
|
+
try { const a=JSON.parse(d); process.stdout.write(String(Array.isArray(a)?a.length:0)); }
|
|
176
|
+
catch { process.stdout.write('0'); }
|
|
177
|
+
});
|
|
178
|
+
" 2>/dev/null || echo 0)
|
|
179
|
+
|
|
180
|
+
# P3 (2026-06-18 코드리뷰): messages 를 argv 대신 stdin 으로 전달 — 큰 transcript(긴 content)에서
|
|
181
|
+
# ARG_MAX/env 한계로 body build 가 깨지는 것 방지 (orphan_recover 와 동일 취지).
|
|
182
|
+
BODY=$(printf '%s' "$MESSAGES_JSON" | node -e "
|
|
183
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
184
|
+
const b = {
|
|
185
|
+
session_id: process.argv[1],
|
|
186
|
+
hostname: process.argv[2],
|
|
187
|
+
repo_url: process.argv[3] || undefined,
|
|
188
|
+
repo_path: process.argv[4],
|
|
189
|
+
transcript_path: process.argv[5] || undefined,
|
|
190
|
+
};
|
|
191
|
+
let msgs = [];
|
|
192
|
+
try { msgs = JSON.parse(d || '[]'); } catch {}
|
|
193
|
+
if (Array.isArray(msgs) && msgs.length > 0) b.messages = msgs;
|
|
194
|
+
process.stdout.write(JSON.stringify(b));
|
|
195
|
+
});
|
|
196
|
+
" "$SESSION_ID" "$HOST" "$REPO_URL" "$REPO_PATH" "$TRANSCRIPT_PATH")
|
|
197
|
+
|
|
198
|
+
# spec §35 PR-1 Task 3: flush-session 보다 먼저 batch-mutate 를 POST 해야
|
|
199
|
+
# cold dedup 게이트(Task 2)가 hot write 결과를 SELECT 할 수 있다.
|
|
200
|
+
# Latency: 본 dispatcher curl + flush-session curl 모두 --max-time 10 → 합산 최대 20s.
|
|
201
|
+
# Stop hook latency budget 안 — 추가 sequential curl 도입 시 재산정 필요.
|
|
202
|
+
DROP_LOG="$HOME/.rookie/dropped-mutations.log"
|
|
203
|
+
# P3: messages 를 argv 대신 stdin 으로 (위 BODY build 와 동일 — ARG_MAX 회피).
|
|
204
|
+
MUTATE_JSON=$(printf '%s' "$MESSAGES_JSON" | node -e "
|
|
205
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
206
|
+
let msgs = [];
|
|
207
|
+
try { msgs = JSON.parse(d || '[]'); } catch {}
|
|
208
|
+
if (!Array.isArray(msgs) || msgs.length === 0) return;
|
|
209
|
+
let last = null;
|
|
210
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
211
|
+
if (msgs[i].role === 'assistant') { last = msgs[i]; break; }
|
|
212
|
+
}
|
|
213
|
+
if (!last || typeof last.content !== 'string') return;
|
|
214
|
+
const tick = String.fromCharCode(96).repeat(3);
|
|
215
|
+
const re = new RegExp(tick + 'rookie:mutate\\\\s*\\\\n([\\\\s\\\\S]*?)\\\\n' + tick);
|
|
216
|
+
const m = last.content.match(re);
|
|
217
|
+
if (!m) return;
|
|
218
|
+
try {
|
|
219
|
+
const j = JSON.parse(m[1]);
|
|
220
|
+
if (j && typeof j === 'object') process.stdout.write(JSON.stringify(j));
|
|
221
|
+
else process.stdout.write('PARSE_FAILED');
|
|
222
|
+
} catch { process.stdout.write('PARSE_FAILED'); }
|
|
223
|
+
});
|
|
224
|
+
" 2>/dev/null) || MUTATE_JSON=""
|
|
225
|
+
|
|
226
|
+
# fence 가 있었지만 JSON.parse 실패 → 무성 silent drop 대신 telemetry 로그.
|
|
227
|
+
if [ "$MUTATE_JSON" = "PARSE_FAILED" ]; then
|
|
228
|
+
printf '%s fence_parse_failed session=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SESSION_ID" >> "$DROP_LOG" 2>/dev/null || true
|
|
229
|
+
MUTATE_JSON=""
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# spec 2026-07-06 §F-1(a) — fence 경유 저장의 세션 project 귀속. fence 에서 뽑은 MUTATE_JSON 엔
|
|
233
|
+
# repo 컨텍스트가 없다 → top-level 에 repo_url/repo_path merge(이미 명시된 값은 보존). 서버가
|
|
234
|
+
# 이걸로 세션 project 를 확정해 미지정 카드를 NULL 대신 세션 project 로 귀속(진입카드 stale 근본 차단).
|
|
235
|
+
if [ -n "$MUTATE_JSON" ]; then
|
|
236
|
+
_MERGED_MUTATE=$(printf '%s' "$MUTATE_JSON" | node -e "
|
|
237
|
+
let d=''; process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
238
|
+
let j; try { j = JSON.parse(d); } catch { process.stdout.write(d); return; }
|
|
239
|
+
if (j && typeof j === 'object' && !Array.isArray(j)) {
|
|
240
|
+
if (process.argv[1] && j.repo_url === undefined) j.repo_url = process.argv[1];
|
|
241
|
+
if (process.argv[2] && j.repo_path === undefined) j.repo_path = process.argv[2];
|
|
242
|
+
process.stdout.write(JSON.stringify(j));
|
|
243
|
+
} else { process.stdout.write(d); }
|
|
244
|
+
});
|
|
245
|
+
" "$REPO_URL" "$REPO_PATH" 2>/dev/null)
|
|
246
|
+
if [ -n "$_MERGED_MUTATE" ]; then MUTATE_JSON="$_MERGED_MUTATE"; fi
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
if [ -n "$MUTATE_JSON" ]; then
|
|
250
|
+
if command -v mutate_dispatch >/dev/null 2>&1; then
|
|
251
|
+
# spec §17.2 H-2.1 (retry 1회) + H-2.2 (partial failure 재큐) 책임을 lib/dispatcher.sh 로 위임.
|
|
252
|
+
# return 0 = 성공 (partial failure 재큐 가능 포함), 1 = 큐화 (전체 실패), 2 = no-op.
|
|
253
|
+
mutate_dispatch "$ENDPOINT" "$TOKEN" "$USER_ID" "$SESSION_ID" "$MUTATE_JSON" || true
|
|
254
|
+
else
|
|
255
|
+
# dispatcher.sh source 실패 → silent drop 대신 telemetry. queue 라도 살아있으면 큐화 시도.
|
|
256
|
+
printf '%s dispatcher_unavailable session=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SESSION_ID" >> "$DROP_LOG" 2>/dev/null || true
|
|
257
|
+
if command -v queue_enqueue >/dev/null 2>&1; then
|
|
258
|
+
queue_enqueue "$SESSION_ID-mutate" "$MUTATE_JSON" "dispatcher-missing" 2>/dev/null || true
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# v6 P1 #5 — Stage 1 측정 hook buffer flush.
|
|
264
|
+
# PreToolUse hook 가 적재한 metric jsonl 을 mutate_dispatch 와 무관하게 항상 flush 시도.
|
|
265
|
+
# silent fail (buffer 잔존 시 다음 세션 재시도).
|
|
266
|
+
if command -v metric_dispatch_md_read >/dev/null 2>&1; then
|
|
267
|
+
metric_dispatch_md_read "$ENDPOINT" "$TOKEN" "$USER_ID" "$SESSION_ID" || true
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# §17 H-1: silent (`> /dev/null || true`) → fail-aware (HTTP code + 큐화)
|
|
271
|
+
# spec 2026-06-18 §3.2: 응답 body 도 캡처(chunks_inserted) → content-bearing marker 판정.
|
|
272
|
+
# P3: body 를 argv(-d "$BODY") 대신 파일(--data-binary @file)로 송신 — 큰 body ARG_MAX 회피.
|
|
273
|
+
# mktemp 실패 시에만 기존 -d 폴백.
|
|
274
|
+
RESP_FILE=$(mktemp 2>/dev/null || echo "")
|
|
275
|
+
BODY_FILE=$(mktemp 2>/dev/null || echo "")
|
|
276
|
+
if [ -n "$BODY_FILE" ]; then
|
|
277
|
+
printf '%s' "$BODY" > "$BODY_FILE" 2>/dev/null
|
|
278
|
+
DATA_ARG=(--data-binary "@$BODY_FILE")
|
|
279
|
+
else
|
|
280
|
+
DATA_ARG=(-d "$BODY")
|
|
281
|
+
fi
|
|
282
|
+
HTTP_CODE=$(curl -sS --max-time 10 \
|
|
283
|
+
-X POST \
|
|
284
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
285
|
+
-H "Content-Type: application/json" \
|
|
286
|
+
-H "x-rookie-user-id: $USER_ID" \
|
|
287
|
+
"${DATA_ARG[@]}" \
|
|
288
|
+
-o "${RESP_FILE:-/dev/null}" \
|
|
289
|
+
-w "%{http_code}" \
|
|
290
|
+
"$ENDPOINT/api/rookie/flush-session" 2>/dev/null) || HTTP_CODE="000"
|
|
291
|
+
|
|
292
|
+
if [ "$HTTP_CODE" = "200" ]; then
|
|
293
|
+
# 정상 — 마지막 성공 시각 갱신 + 큐가 있으면 chain drain
|
|
294
|
+
if command -v queue_record_success >/dev/null 2>&1; then
|
|
295
|
+
queue_record_success
|
|
296
|
+
queue_drain "$ENDPOINT" "$TOKEN" "$USER_ID" >/dev/null 2>&1 || true
|
|
297
|
+
fi
|
|
298
|
+
# P0.4 Step 4 (codex #10): marker 조건 = code 200(이 분기) + message_count>0 (chunks_inserted 무관).
|
|
299
|
+
# 멱등 재flush(chunks 0)도 marker 박아 orphan scan 이 같은 세션을 영구 재적재하지 않게 한다.
|
|
300
|
+
# 빈 messages 는 MESSAGE_COUNT=0 으로 미부착(복구 여지 보존).
|
|
301
|
+
if command -v orphan_mark_flushed >/dev/null 2>&1 && [ "${MESSAGE_COUNT:-0}" -gt 0 ] 2>/dev/null; then
|
|
302
|
+
orphan_mark_flushed "$SESSION_ID" "stop" "$MESSAGE_COUNT" || true
|
|
303
|
+
fi
|
|
304
|
+
else
|
|
305
|
+
# §17 H-2: 큐 적재 + 1회 chain drain (일시적 깜빡임 회복)
|
|
306
|
+
if command -v queue_enqueue >/dev/null 2>&1; then
|
|
307
|
+
queue_enqueue "$SESSION_ID" "$BODY" "$HTTP_CODE" 2>/dev/null || true
|
|
308
|
+
queue_drain "$ENDPOINT" "$TOKEN" "$USER_ID" >/dev/null 2>&1 || true
|
|
309
|
+
fi
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
if [ -n "$RESP_FILE" ]; then rm -f "$RESP_FILE" 2>/dev/null || true; fi
|
|
313
|
+
if [ -n "$BODY_FILE" ]; then rm -f "$BODY_FILE" 2>/dev/null || true; fi
|
|
314
|
+
exit 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse(Bash) 가드 — 라이브/원격 DB·git 파괴적 명령을 하네스 레벨에서 차단.
|
|
3
|
+
#
|
|
4
|
+
# 배경(2026-06-02 KST): 검증으로 `supabase db reset --linked`를 라이브 원격 DB에 돌려
|
|
5
|
+
# auth.users(실 로그인 계정) 전체가 소실됨. 모델 판단에 의존하지 않고
|
|
6
|
+
# 명령 문자열 전체를 검사해 차단한다(파이프/래핑 우회 방지 — `printf y | npx supabase db reset --linked`도 잡힘).
|
|
7
|
+
#
|
|
8
|
+
# 출력: 위험 시 PreToolUse permissionDecision=deny JSON, 아니면 침묵(exit 0).
|
|
9
|
+
# 정당한 경우엔 사용자가 직접 `!` 명령으로 실행(이 hook은 모델의 Bash 도구만 가로챔).
|
|
10
|
+
|
|
11
|
+
cmd=$(jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
12
|
+
[ -z "$cmd" ] && exit 0
|
|
13
|
+
|
|
14
|
+
# 위험 패턴(대소문자 무시). db reset --local 등 로컬 안전 변형은 매칭하지 않는다.
|
|
15
|
+
# git push 의 -f 는 push 직후(`git push -f`)·인자 뒤(`git push origin -f`) 둘 다 잡는다.
|
|
16
|
+
# drop role 은 database|schema|table 과 동일하게 앞 공백 요구 없이 잡는다(따옴표/문두 우회 방지).
|
|
17
|
+
deny_re='(supabase[[:space:]]+db[[:space:]]+reset[[:space:]]+--linked|db[[:space:]]+reset[[:space:]]+--remote|drop[[:space:]]+(database|schema|table|role)|truncate[[:space:]]+(table|only)|git[[:space:]]+push[[:space:]]+(.*[[:space:]])?(--force([[:space:]=]|$)|--force-with-lease|-f([[:space:]]|$))|git[[:space:]]+reset[[:space:]]+--hard)'
|
|
18
|
+
|
|
19
|
+
if printf '%s' "$cmd" | grep -qiE "$deny_re"; then
|
|
20
|
+
reason="🛑 하네스 차단(guard-destructive-db): 라이브/원격 DB 또는 git 파괴적 명령(supabase db reset --linked·drop·truncate·git push --force·git reset --hard)은 모델이 자동 실행할 수 없습니다. 스키마 검증은 로컬(supabase db reset --local)에서. 정말 필요하면 사용자가 직접 ! 명령으로 실행하세요."
|
|
21
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
exit 0
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# lib/config-resolve.sh — B6 한 PC 다계정 격리 (spec 2026-07-11-memory-reliability-fix-design §B6).
|
|
3
|
+
# (1) config 상향 탐색: env(ROOKIE_ENDPOINT+ROOKIE_TOKEN) > <dir>/.rookie/config.json (cwd 상향) > 전역.
|
|
4
|
+
# 모든 hook 진입 스크립트가 이 함수 하나로 해석한다 — 한 곳이라도 전역 고정 경로가 남으면
|
|
5
|
+
# 그 경로로 계정 오염이 재발한다(§B6-1 파일 목록 원칙).
|
|
6
|
+
# (2) 세션 identity pinning(§B6-3): SessionStart 가 유효 토큰 hash 를 기록, Stop(flush)이 비교 —
|
|
7
|
+
# 세션 도중 계정 전환 시 flush 를 중단해 진행 중 세션의 기억이 새 계정으로 오귀속되는 것을 차단.
|
|
8
|
+
|
|
9
|
+
# cwd 기준 유효 config 경로를 stdout 으로. 로컬(.rookie/config.json, 상향 탐색) > 전역.
|
|
10
|
+
# 전역 경로(~/.rookie/config.json) 자체는 로컬 후보에서 제외($HOME 도달 시 이중 판정 방지).
|
|
11
|
+
rookie_config_path_for_cwd() {
|
|
12
|
+
local dir="$1"
|
|
13
|
+
local global="$HOME/.rookie/config.json"
|
|
14
|
+
if [ -z "$dir" ] || [ ! -d "$dir" ]; then
|
|
15
|
+
printf '%s' "$global"
|
|
16
|
+
return 0
|
|
17
|
+
fi
|
|
18
|
+
dir="$(cd "$dir" 2>/dev/null && pwd)" || { printf '%s' "$global"; return 0; }
|
|
19
|
+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
20
|
+
if [ "$dir/.rookie/config.json" != "$global" ] && [ -f "$dir/.rookie/config.json" ]; then
|
|
21
|
+
printf '%s' "$dir/.rookie/config.json"
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
dir="$(dirname "$dir")"
|
|
25
|
+
done
|
|
26
|
+
printf '%s' "$global"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# env > 로컬 > 전역 해석. 결과를 전역 변수로 내보낸다:
|
|
30
|
+
# ROOKIE_RESOLVED_ENDPOINT / ROOKIE_RESOLVED_TOKEN / ROOKIE_RESOLVED_USER_ID
|
|
31
|
+
# ROOKIE_ACTIVE_CONFIG (해석된 config 파일 경로 — env 스코프는 "env", sweep partition 기준)
|
|
32
|
+
# 반환 1 = 자격증명 없음(hook 은 조용히 exit 0 해야 함).
|
|
33
|
+
rookie_resolve_credentials() {
|
|
34
|
+
local start_dir="$1"
|
|
35
|
+
ROOKIE_RESOLVED_ENDPOINT=""
|
|
36
|
+
ROOKIE_RESOLVED_TOKEN=""
|
|
37
|
+
ROOKIE_RESOLVED_USER_ID=""
|
|
38
|
+
ROOKIE_ACTIVE_CONFIG=""
|
|
39
|
+
# set -u 환경(user-prompt-skill-gate.sh)에서도 안전하게 :- 기본값 참조.
|
|
40
|
+
if [ -n "${ROOKIE_ENDPOINT:-}" ] && [ -n "${ROOKIE_TOKEN:-}" ]; then
|
|
41
|
+
ROOKIE_RESOLVED_ENDPOINT="$ROOKIE_ENDPOINT"
|
|
42
|
+
ROOKIE_RESOLVED_TOKEN="$ROOKIE_TOKEN"
|
|
43
|
+
ROOKIE_ACTIVE_CONFIG="env"
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
local cfg
|
|
47
|
+
cfg="$(rookie_config_path_for_cwd "$start_dir")"
|
|
48
|
+
[ -f "$cfg" ] || return 1
|
|
49
|
+
ROOKIE_RESOLVED_ENDPOINT=$(node -e "console.log(require(process.argv[1]).endpoint || '')" "$cfg" 2>/dev/null || echo "")
|
|
50
|
+
ROOKIE_RESOLVED_TOKEN=$(node -e "console.log(require(process.argv[1]).token || '')" "$cfg" 2>/dev/null || echo "")
|
|
51
|
+
ROOKIE_RESOLVED_USER_ID=$(node -e "console.log(require(process.argv[1]).user_id || '')" "$cfg" 2>/dev/null || echo "")
|
|
52
|
+
[ -n "$ROOKIE_RESOLVED_TOKEN" ] || return 1
|
|
53
|
+
[ -n "$ROOKIE_RESOLVED_ENDPOINT" ] || ROOKIE_RESOLVED_ENDPOINT="http://localhost:3000"
|
|
54
|
+
ROOKIE_ACTIVE_CONFIG="$cfg"
|
|
55
|
+
return 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ── B6-3 세션 identity pinning ──
|
|
59
|
+
# pinning 기준 = 유효 토큰 hash (config user_id 아님 — env 주입 세션에서도 동일 기준, 3R P2).
|
|
60
|
+
ROOKIE_IDENTITY_PIN_DIR="${ROOKIE_IDENTITY_PIN_DIR:-$HOME/.rookie/session-identity}"
|
|
61
|
+
ROOKIE_IDENTITY_MISMATCH_LOG="${ROOKIE_IDENTITY_MISMATCH_LOG:-$HOME/.rookie/identity-mismatch.log}"
|
|
62
|
+
|
|
63
|
+
_rookie_token_hash() {
|
|
64
|
+
printf '%s' "$1" | node -e '
|
|
65
|
+
const c=require("crypto"); let d="";
|
|
66
|
+
process.stdin.on("data",x=>d+=x).on("end",()=>process.stdout.write(c.createHash("sha256").update(d).digest("hex").slice(0,16)));
|
|
67
|
+
' 2>/dev/null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# SessionStart: 세션의 effective identity 기록 (+14일 지난 pin 기회적 정리).
|
|
71
|
+
identity_pin_write() {
|
|
72
|
+
local sid="$1" token="$2"
|
|
73
|
+
[ -n "$sid" ] && [ -n "$token" ] || return 0
|
|
74
|
+
mkdir -p "$ROOKIE_IDENTITY_PIN_DIR" 2>/dev/null || return 0
|
|
75
|
+
local hash
|
|
76
|
+
hash="$(_rookie_token_hash "$token")"
|
|
77
|
+
[ -n "$hash" ] || return 0
|
|
78
|
+
( umask 077; printf '%s' "$hash" > "$ROOKIE_IDENTITY_PIN_DIR/$sid" 2>/dev/null ) || true
|
|
79
|
+
# 14일+ pin 정리 — pin 은 세션 수명 추적용 소모품(마커 TTL 과 동일 감각).
|
|
80
|
+
find "$ROOKIE_IDENTITY_PIN_DIR" -type f -mtime +14 -delete 2>/dev/null || true
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Stop(flush): pin 과 현재 토큰 비교. 0 = 통과(pin 없음 포함 — 구세션 fail-open), 1 = 불일치.
|
|
85
|
+
identity_pin_check() {
|
|
86
|
+
local sid="$1" token="$2"
|
|
87
|
+
[ -n "$sid" ] && [ -n "$token" ] || return 0
|
|
88
|
+
local pin_file="$ROOKIE_IDENTITY_PIN_DIR/$sid"
|
|
89
|
+
[ -f "$pin_file" ] || return 0
|
|
90
|
+
local pinned current
|
|
91
|
+
pinned="$(cat "$pin_file" 2>/dev/null)"
|
|
92
|
+
[ -n "$pinned" ] || return 0
|
|
93
|
+
current="$(_rookie_token_hash "$token")"
|
|
94
|
+
[ -n "$current" ] || return 0
|
|
95
|
+
[ "$pinned" = "$current" ] && return 0
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# 불일치 로그 — doctor 가 집계·경고. 토큰·hash 원문은 남기지 않는다(§10d).
|
|
100
|
+
identity_pin_log_mismatch() {
|
|
101
|
+
local sid="$1" source="${2:-stop}"
|
|
102
|
+
mkdir -p "$(dirname "$ROOKIE_IDENTITY_MISMATCH_LOG")" 2>/dev/null || return 0
|
|
103
|
+
printf '%s session=%s source=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$sid" "$source" \
|
|
104
|
+
>> "$ROOKIE_IDENTITY_MISMATCH_LOG" 2>/dev/null || true
|
|
105
|
+
return 0
|
|
106
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# lib/cross-runtime-sweep.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 → 차차) 로 위임 — 여기선 chunk capture 까지만.
|
|
6
|
+
#
|
|
7
|
+
# 의존: lib/transcript.sh (Claude·Codex 파서) + lib/orphan-recovery.sh (marker·orphan_recover).
|
|
8
|
+
# marker namespace 분리(§3.2): Codex 는 ROOKIE_ORPHAN_DIR/codex 하위(dynamic scope override)로
|
|
9
|
+
# flushed/baseline/deferred/pending/client_extracted 를 둔다 → "어느 런타임 포맷" 구분 보존.
|
|
10
|
+
|
|
11
|
+
ROOKIE_CODEX_SESSIONS_DIR="${ROOKIE_CODEX_SESSIONS_DIR:-$HOME/.codex/sessions}"
|
|
12
|
+
ROOKIE_CLAUDE_PROJECTS_DIR="${ROOKIE_CLAUDE_PROJECTS_DIR:-$HOME/.claude/projects}"
|
|
13
|
+
ROOKIE_CODEX_SWEEP_DAYS="${ROOKIE_CODEX_SWEEP_DAYS:-7}" # 날짜 파티션 순회 창
|
|
14
|
+
ROOKIE_CODEX_SWEEP_CAP="${ROOKIE_CODEX_SWEEP_CAP:-20}" # 1회 sweep 최대 Codex 세션 수
|
|
15
|
+
ROOKIE_SWEEP_TIME_BUDGET="${ROOKIE_SWEEP_TIME_BUDGET:-30}" # 총 sweep 예산(초)
|
|
16
|
+
|
|
17
|
+
# 최근 N일 Codex rollout 중 복구 후보 경로를 stdout(1줄당 1개). 최신순, marker·hold 제외, cap.
|
|
18
|
+
# marker root = ROOKIE_ORPHAN_DIR 를 그대로 사용한다 — codex_recover 가 codex namespace
|
|
19
|
+
# (base/codex)로 설정해 호출하므로, 여기선 /codex 를 다시 붙이지 않는다(이중 방지).
|
|
20
|
+
# allowlist: regular file·≤MAXBYTES·rollout-*.jsonl. session_meta.cwd 는 recover 단계에서 읽는다.
|
|
21
|
+
codex_scan_candidates() {
|
|
22
|
+
local root="$ROOKIE_CODEX_SESSIONS_DIR"
|
|
23
|
+
[ -d "$root" ] || return 0
|
|
24
|
+
local cdir="${ROOKIE_ORPHAN_DIR:-$HOME/.rookie/orphan-recovery}"
|
|
25
|
+
node -e '
|
|
26
|
+
const fs=require("fs"), path=require("path");
|
|
27
|
+
const [root, cdir, daysS, capS, holdS, maxBytesS] = process.argv.slice(1);
|
|
28
|
+
const now=Date.now()/1000, days=+daysS, cap=+capS, hold=+holdS, maxB=+maxBytesS;
|
|
29
|
+
const flushedDir=path.join(cdir,"flushed"), baselineDir=path.join(cdir,"baseline"), deferredDir=path.join(cdir,"deferred");
|
|
30
|
+
// P0.4: Claude scanner 와 대칭으로 low_value·expired 도 제외(한쪽만 고치면 런타임 비대칭 재진입 — §12-P2-11).
|
|
31
|
+
const lowValueDir=path.join(cdir,"low_value"), expiredDir=path.join(cdir,"expired");
|
|
32
|
+
const minMtime=now-days*86400;
|
|
33
|
+
const uuidRe=/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
34
|
+
const marked=(sid)=>fs.existsSync(path.join(flushedDir,sid+".json"))||fs.existsSync(path.join(baselineDir,sid+".json"))||fs.existsSync(path.join(lowValueDir,sid+".json"))||fs.existsSync(path.join(expiredDir,sid+".json"));
|
|
35
|
+
const deferBlocked=(sid)=>{ try{
|
|
36
|
+
const j=JSON.parse(fs.readFileSync(path.join(deferredDir,sid+".json"),"utf8"));
|
|
37
|
+
if((j.fail_count||0)>=3) return true;
|
|
38
|
+
const nr=Date.parse(j.next_retry_at); return !isNaN(nr)&&nr/1000>now;
|
|
39
|
+
}catch{ return false; } };
|
|
40
|
+
// 날짜 파티션 재귀 walk → rollout-*.jsonl 수집.
|
|
41
|
+
const found=[];
|
|
42
|
+
const walk=(d)=>{ let names=[]; try{names=fs.readdirSync(d);}catch{return;}
|
|
43
|
+
for(const n of names){ const fp=path.join(d,n); let st; try{st=fs.lstatSync(fp);}catch{continue;}
|
|
44
|
+
if(st.isDirectory()){ walk(fp); continue; }
|
|
45
|
+
if(!st.isFile()) continue;
|
|
46
|
+
if(!/^rollout-.*\.jsonl$/.test(n)) continue;
|
|
47
|
+
if(st.size>maxB) continue;
|
|
48
|
+
if(st.mtimeMs/1000<minMtime) continue; // 날짜 창 밖
|
|
49
|
+
if((now-st.mtimeMs/1000)<hold) continue; // 최근 수정 보류
|
|
50
|
+
const m=n.slice(0,-6).match(uuidRe); if(!m) continue; // .jsonl 제거 후 끝 UUID
|
|
51
|
+
const sid=m[0];
|
|
52
|
+
if(marked(sid)||deferBlocked(sid)) continue;
|
|
53
|
+
found.push({fp,sid,mtime:st.mtimeMs});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(root);
|
|
57
|
+
found.sort((a,b)=>b.mtime-a.mtime);
|
|
58
|
+
for(const e of found.slice(0,cap)) process.stdout.write(e.fp+"\n");
|
|
59
|
+
' "$root" "$cdir" "$ROOKIE_CODEX_SWEEP_DAYS" "$ROOKIE_CODEX_SWEEP_CAP" \
|
|
60
|
+
"${ROOKIE_ORPHAN_AGE_HOLD:-60}" "${ROOKIE_ORPHAN_MAX_BYTES:-52428800}" 2>/dev/null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Codex 세션 사후 회수. orphan_recover 의 Codex 판 — Codex 파서 + codex marker namespace(source=sweep).
|
|
64
|
+
# stdout 미사용. 복구 건수는 ROOKIE_SWEEP_CODEX_COUNT.
|
|
65
|
+
codex_recover() {
|
|
66
|
+
local endpoint="$1" token="$2" user_id="$3"
|
|
67
|
+
ROOKIE_SWEEP_CODEX_COUNT=0
|
|
68
|
+
[ -n "$endpoint" ] && [ -n "$token" ] || return 0
|
|
69
|
+
command -v transcript_messages_json_codex >/dev/null 2>&1 || return 0
|
|
70
|
+
# marker·scan 이 codex namespace(base/codex)를 쓰도록 dynamic scope override.
|
|
71
|
+
# base 를 먼저 읽고(별도 변수) shadow — 한 줄 분리 시 local 이 변수를 비워 $HOME 폴백되는 함정 회피.
|
|
72
|
+
local _base="${ROOKIE_ORPHAN_DIR:-$HOME/.rookie/orphan-recovery}"
|
|
73
|
+
local ROOKIE_ORPHAN_DIR="$_base/codex"
|
|
74
|
+
|
|
75
|
+
local deadline; deadline=$(( $(date +%s) + ROOKIE_SWEEP_TIME_BUDGET ))
|
|
76
|
+
local host; host="$(hostname)"
|
|
77
|
+
local candidates; candidates="$(codex_scan_candidates)" || return 0
|
|
78
|
+
[ -n "$candidates" ] || return 0
|
|
79
|
+
|
|
80
|
+
local fp sid cwd messages mc msg_file body_file code
|
|
81
|
+
while IFS= read -r fp; do
|
|
82
|
+
[ -n "$fp" ] || continue
|
|
83
|
+
[ "$(date +%s)" -ge "$deadline" ] && break
|
|
84
|
+
sid="$(transcript_session_id_from_path_codex "$fp")"
|
|
85
|
+
[ -n "$sid" ] || continue
|
|
86
|
+
_orphan_deferred_blocked "$sid" && continue
|
|
87
|
+
cwd="$(transcript_codex_cwd "$fp")" # 프로젝트 매칭(repo_path)
|
|
88
|
+
# B6-4 (spec 2026-07-11 §B6): cwd 불명 orphan 보류 + 타 config 소속 skip — Claude 경로
|
|
89
|
+
# (orphan_recover)와 대칭. 판별 불가·타 계정 몫을 현재 config 로 flush 하지 않는다.
|
|
90
|
+
if [ -z "$cwd" ]; then
|
|
91
|
+
_orphan_log_telemetry "$sid" "codex_skip_unknown_cwd"; continue
|
|
92
|
+
fi
|
|
93
|
+
# codex R1 P1-2: cwd 디렉토리 삭제·이동 → 판별 불가 보류 (전역 폴백 flush 금지).
|
|
94
|
+
if [ ! -d "$cwd" ]; then
|
|
95
|
+
_orphan_log_telemetry "$sid" "codex_skip_missing_cwd_dir"; continue
|
|
96
|
+
fi
|
|
97
|
+
if command -v rookie_config_path_for_cwd >/dev/null 2>&1 && [ -n "${ROOKIE_ACTIVE_CONFIG:-}" ]; then
|
|
98
|
+
if [ "$(rookie_config_path_for_cwd "$cwd")" != "$ROOKIE_ACTIVE_CONFIG" ]; then
|
|
99
|
+
_orphan_log_telemetry "$sid" "codex_skip_other_config"; continue
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
# codex R1 P1-1: 세션 identity pin 불일치 → 보류 (orphan_recover 와 대칭).
|
|
103
|
+
if command -v identity_pin_check >/dev/null 2>&1 && ! identity_pin_check "$sid" "$token"; then
|
|
104
|
+
_orphan_log_telemetry "$sid" "codex_skip_identity_mismatch"; continue
|
|
105
|
+
fi
|
|
106
|
+
messages="$(transcript_messages_json_codex "$fp")"
|
|
107
|
+
mc="$(_orphan_messages_count "$messages")"
|
|
108
|
+
if [ "${mc:-0}" -le 0 ] 2>/dev/null; then
|
|
109
|
+
_orphan_log_telemetry "$sid" "codex_skip_empty_messages"; continue
|
|
110
|
+
fi
|
|
111
|
+
# P3: messages 를 임시 파일로 전달 (argv ARG_MAX 회피).
|
|
112
|
+
msg_file="$(mktemp 2>/dev/null)" || continue
|
|
113
|
+
printf '%s' "$messages" > "$msg_file" 2>/dev/null
|
|
114
|
+
body_file="$(mktemp 2>/dev/null)" || { rm -f "$msg_file"; continue; }
|
|
115
|
+
node -e '
|
|
116
|
+
const fs=require("fs");
|
|
117
|
+
const b={session_id:process.argv[1],hostname:process.argv[2],repo_path:process.argv[3]||undefined,transcript_path:process.argv[4]};
|
|
118
|
+
try{ const m=JSON.parse(fs.readFileSync(process.argv[5],"utf8")||"[]"); if(Array.isArray(m)&&m.length) b.messages=m; }catch{}
|
|
119
|
+
process.stdout.write(JSON.stringify(b));
|
|
120
|
+
' "$sid" "$host" "$cwd" "$fp" "$msg_file" > "$body_file" 2>/dev/null
|
|
121
|
+
rm -f "$msg_file"
|
|
122
|
+
if [ ! -s "$body_file" ]; then rm -f "$body_file"; continue; fi
|
|
123
|
+
code="$(curl -sS --max-time 10 -X POST \
|
|
124
|
+
-H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
|
125
|
+
-H "x-rookie-user-id: $user_id" --data-binary @"$body_file" \
|
|
126
|
+
-o /dev/null -w "%{http_code}" \
|
|
127
|
+
"$endpoint/api/rookie/flush-session" 2>/dev/null)" || code="000"
|
|
128
|
+
rm -f "$body_file"
|
|
129
|
+
# P0.4 Step 4: marker 조건 = code 200 + message_count>0 (Claude 경로와 대칭).
|
|
130
|
+
if [ "$code" = "200" ] && [ "${mc:-0}" -gt 0 ] 2>/dev/null; then
|
|
131
|
+
if orphan_mark_flushed "$sid" "sweep" "$mc"; then
|
|
132
|
+
_orphan_clear_deferred "$sid"
|
|
133
|
+
# P0.4 Step 1: 짧고 내용 없는 Codex 세션은 low_value, 운영사실 있으면 pending 적재.
|
|
134
|
+
if orphan_pending_is_worthwhile "$messages" "$mc"; then
|
|
135
|
+
orphan_record_pending "$sid" "$fp" "$mc" || true # Codex tail 도 차차가 추출(codex ns pending)
|
|
136
|
+
else
|
|
137
|
+
orphan_mark_low_value "$sid" "short_no_content" "$mc" || true
|
|
138
|
+
fi
|
|
139
|
+
ROOKIE_SWEEP_CODEX_COUNT=$((ROOKIE_SWEEP_CODEX_COUNT + 1))
|
|
140
|
+
else
|
|
141
|
+
_orphan_record_deferred "$sid" "marker_write_failed"
|
|
142
|
+
fi
|
|
143
|
+
else
|
|
144
|
+
_orphan_log_telemetry "$sid" "codex_flush_failed code=$code mc=$mc"
|
|
145
|
+
fi
|
|
146
|
+
done <<EOF
|
|
147
|
+
$candidates
|
|
148
|
+
EOF
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# ── opportunistic 하루 1회 가드 (spec 2026-06-18 — launchd 대신 진입점 sweep 기본값) ──
|
|
153
|
+
# OS 스케줄러 없이 사용자 진입점(Claude SessionStart·Codex rookie_context·수동 CLI)마다
|
|
154
|
+
# "오늘 아직 안 했으면" sweep → 꺼져 있던 PC도 다음 사용 때 따라잡는다.
|
|
155
|
+
_sweep_stamp_file() { printf '%s/last-sweep-date' "${ROOKIE_ORPHAN_DIR:-$HOME/.rookie/orphan-recovery}"; }
|
|
156
|
+
|
|
157
|
+
# 오늘 아직 sweep 안 했으면 exit 0 (due). 이미 했으면 1.
|
|
158
|
+
sweep_due_today() {
|
|
159
|
+
local f today
|
|
160
|
+
f="$(_sweep_stamp_file)"
|
|
161
|
+
today="$(date +%Y-%m-%d)"
|
|
162
|
+
[ -f "$f" ] || return 0
|
|
163
|
+
[ "$(cat "$f" 2>/dev/null)" = "$today" ] && return 1 || return 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# 오늘 sweep 했음을 기록 (날짜 스탬프).
|
|
167
|
+
sweep_mark_ran_today() {
|
|
168
|
+
local f; f="$(_sweep_stamp_file)"
|
|
169
|
+
mkdir -p "$(dirname "$f")" 2>/dev/null
|
|
170
|
+
( umask 077; date +%Y-%m-%d > "$f" 2>/dev/null ) || true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# 전체 sweep: Claude 모든 프로젝트(orphan_recover 재사용) + Codex(codex_recover).
|
|
174
|
+
# 복구 합계는 ROOKIE_SWEEP_TOTAL.
|
|
175
|
+
sweep_all() {
|
|
176
|
+
local endpoint="$1" token="$2" user_id="$3"
|
|
177
|
+
ROOKIE_SWEEP_TOTAL=0
|
|
178
|
+
[ -n "$endpoint" ] && [ -n "$token" ] || return 0
|
|
179
|
+
|
|
180
|
+
# Claude: 프로젝트 디렉토리별로 orphan_recover (Claude namespace 기본 ROOKIE_ORPHAN_DIR).
|
|
181
|
+
if command -v orphan_recover >/dev/null 2>&1 && [ -d "$ROOKIE_CLAUDE_PROJECTS_DIR" ]; then
|
|
182
|
+
local d
|
|
183
|
+
for d in "$ROOKIE_CLAUDE_PROJECTS_DIR"/*/; do
|
|
184
|
+
[ -d "$d" ] || continue
|
|
185
|
+
ROOKIE_ORPHAN_PROJECT_DIR="$d" orphan_recover "$endpoint" "$token" "$user_id" >/dev/null 2>&1 || true
|
|
186
|
+
ROOKIE_SWEEP_TOTAL=$((ROOKIE_SWEEP_TOTAL + ${ROOKIE_ORPHAN_RECOVERED_COUNT:-0}))
|
|
187
|
+
done
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# Codex: 최근 N일 세션.
|
|
191
|
+
if command -v codex_recover >/dev/null 2>&1; then
|
|
192
|
+
codex_recover "$endpoint" "$token" "$user_id" || true
|
|
193
|
+
ROOKIE_SWEEP_TOTAL=$((ROOKIE_SWEEP_TOTAL + ${ROOKIE_SWEEP_CODEX_COUNT:-0}))
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# P0.4 Step 3: 14일+ 미처리 pending 정리(expired marker) — base(Claude) + codex namespace 둘 다.
|
|
197
|
+
if command -v orphan_prune_expired >/dev/null 2>&1; then
|
|
198
|
+
orphan_prune_expired || true
|
|
199
|
+
ROOKIE_ORPHAN_DIR="${ROOKIE_ORPHAN_DIR:-$HOME/.rookie/orphan-recovery}/codex" orphan_prune_expired 2>/dev/null || true
|
|
200
|
+
fi
|
|
201
|
+
return 0
|
|
202
|
+
}
|