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,423 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Rookie hot-write dispatcher — batch-mutate POST 의 retry + partial failure 처리.
|
|
3
|
+
# spec: docs/superpowers/specs/2026-04-23-post-v5-decisions.md §17.2 H-2.1 / H-2.2
|
|
4
|
+
# plan: docs/superpowers/plans/2026-05-05-v6-p1-3-hot-write-resilience.md Task 1
|
|
5
|
+
#
|
|
6
|
+
# 함수:
|
|
7
|
+
# mutate_dispatch ENDPOINT TOKEN USER_ID SESSION_ID MUTATE_JSON
|
|
8
|
+
# return 0 = 성공 (partial failure 재큐 가능 포함)
|
|
9
|
+
# return 1 = 큐화 (전체 실패)
|
|
10
|
+
# return 2 = no-op (MUTATE_JSON 빈값)
|
|
11
|
+
#
|
|
12
|
+
# 의존:
|
|
13
|
+
# queue.sh : queue_enqueue (없으면 telemetry 로 fallback)
|
|
14
|
+
# curl : --max-time 10 + http_code 회수 (flush-session.sh 패턴 동일)
|
|
15
|
+
# jq : 응답 partial failure 파싱 (없으면 전체 재큐 + warn)
|
|
16
|
+
# node : retry payload 재구성 (queue.sh 와 동일하게 node 의존 가정)
|
|
17
|
+
#
|
|
18
|
+
# 보안 (spec §10):
|
|
19
|
+
# - mutate_json 본문은 stdout 으로 출력 금지 (telemetry 로그도 본문 미포함, status/session 만)
|
|
20
|
+
# - 임시 파일은 mktemp + trap 으로 정리
|
|
21
|
+
# - 큐 디렉토리 권한 chmod 600 은 queue.sh 가 보장
|
|
22
|
+
|
|
23
|
+
ROOKIE_DROP_LOG="${ROOKIE_DROP_LOG:-$HOME/.rookie/dropped-mutations.log}"
|
|
24
|
+
# spec §17.2 H-2.3 dead-letter 격리 디렉토리 — queue.sh 의 ROOKIE_DEAD_LETTER_DIR 와 일관.
|
|
25
|
+
ROOKIE_DEAD_LETTER_DIR="${ROOKIE_DEAD_LETTER_DIR:-$HOME/.rookie/dead-letter}"
|
|
26
|
+
|
|
27
|
+
# drop log 디렉토리 보장 — stderr redirect 가 silent fail 하지 않도록.
|
|
28
|
+
mkdir -p "$(dirname "$ROOKIE_DROP_LOG")" 2>/dev/null || true
|
|
29
|
+
|
|
30
|
+
# 내부: 1회 POST. $1=endpoint $2=token $3=user_id $4=mutate_json $5=output_body_path
|
|
31
|
+
# stdout 으로 http_code 만 출력. curl exit ≠ 0 이면 "000".
|
|
32
|
+
# curl stderr 는 ROOKIE_DROP_LOG 로 보존 — DNS/TLS/timeout 진단을 위해 silent 차단.
|
|
33
|
+
_post_once() {
|
|
34
|
+
local endpoint="$1"
|
|
35
|
+
local token="$2"
|
|
36
|
+
local user_id="$3"
|
|
37
|
+
local mutate_json="$4"
|
|
38
|
+
local out_body="$5"
|
|
39
|
+
local code
|
|
40
|
+
# spec 2026-07-06 §F-1 — x-rookie-hostname 전송(세션 project 의 repo_path 매칭 요구).
|
|
41
|
+
# dispatch 는 origin 호스트에서 실행되므로 $(hostname) == 세션 발생 호스트.
|
|
42
|
+
code=$(curl -sS --max-time 10 \
|
|
43
|
+
-X POST \
|
|
44
|
+
-H "Authorization: Bearer $token" \
|
|
45
|
+
-H "Content-Type: application/json" \
|
|
46
|
+
-H "x-rookie-user-id: $user_id" \
|
|
47
|
+
-H "x-rookie-hostname: $(hostname)" \
|
|
48
|
+
-d "$mutate_json" \
|
|
49
|
+
-o "$out_body" \
|
|
50
|
+
-w "%{http_code}" \
|
|
51
|
+
"$endpoint/api/rookie/batch-mutate" 2>>"$ROOKIE_DROP_LOG") || code="000"
|
|
52
|
+
if [ -z "$code" ]; then
|
|
53
|
+
code="000"
|
|
54
|
+
fi
|
|
55
|
+
printf '%s' "$code"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# 내부: failed 배열 + 원본 mutate_json 으로 retry payload 재구성.
|
|
59
|
+
# stdout = retry_json (jq + node 둘 다 사용 가능 시), 실패 시 비어있음 → caller 가 fallback.
|
|
60
|
+
# working_cards 는 object 이므로 failed.type=working_cards 가 한 건이라도 있으면 통째로 보존.
|
|
61
|
+
# self_rules / evidence / verifications 는 array → failed.index 에 해당하는 element 만 추출.
|
|
62
|
+
_build_retry_payload() {
|
|
63
|
+
local mutate_json="$1"
|
|
64
|
+
local failed_json="$2"
|
|
65
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
66
|
+
return 1
|
|
67
|
+
fi
|
|
68
|
+
node -e "
|
|
69
|
+
try {
|
|
70
|
+
const src = JSON.parse(process.argv[1]);
|
|
71
|
+
const failed = JSON.parse(process.argv[2]);
|
|
72
|
+
if (!Array.isArray(failed) || failed.length === 0) { process.exit(1); }
|
|
73
|
+
const out = {};
|
|
74
|
+
const wcFail = failed.some(f => f && f.type === 'working_cards');
|
|
75
|
+
if (wcFail && src.working_cards && typeof src.working_cards === 'object') {
|
|
76
|
+
out.working_cards = src.working_cards;
|
|
77
|
+
}
|
|
78
|
+
for (const arrType of ['self_rules', 'evidence', 'verifications']) {
|
|
79
|
+
const idxs = failed
|
|
80
|
+
.filter(f => f && f.type === arrType && Number.isInteger(f.index))
|
|
81
|
+
.map(f => f.index);
|
|
82
|
+
if (idxs.length === 0) continue;
|
|
83
|
+
const arr = Array.isArray(src[arrType]) ? src[arrType] : [];
|
|
84
|
+
const picked = idxs
|
|
85
|
+
.filter(i => i >= 0 && i < arr.length)
|
|
86
|
+
.map(i => arr[i]);
|
|
87
|
+
if (picked.length > 0) out[arrType] = picked;
|
|
88
|
+
}
|
|
89
|
+
const keys = Object.keys(out);
|
|
90
|
+
if (keys.length === 0) { process.exit(1); }
|
|
91
|
+
// spec 2026-07-06 §F-1(a) — top-level repo 컨텍스트 보존(재시도 저장이 세션 project 를 잃지 않도록).
|
|
92
|
+
// mutation key 유무 판정 이후에 부착 — repo 컨텍스트만으로 재큐되지 않게(empty-check 는 mutation 만).
|
|
93
|
+
if (typeof src.repo_url === 'string') out.repo_url = src.repo_url;
|
|
94
|
+
if (typeof src.repo_path === 'string') out.repo_path = src.repo_path;
|
|
95
|
+
process.stdout.write(JSON.stringify(out));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
process.stderr.write('build_retry_payload_error: ' + (e && e.message ? e.message : String(e)) + '\n');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
" "$mutate_json" "$failed_json" 2>>"$ROOKIE_DROP_LOG"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# 내부: failed paths 배열 + 원본 mutate_json 으로 결함 element 를 **제외**한 payload 재구성.
|
|
104
|
+
# spec §17.2 H-2.3 — _build_retry_payload 와 대칭 (그 함수는 failed 만 추출, 본 함수는 failed 만 제외).
|
|
105
|
+
# stdout = filtered_json (성공 시), 실패/empty 시 비어있음 → caller 가 fallback.
|
|
106
|
+
_build_filtered_payload() {
|
|
107
|
+
local mutate_json="$1"
|
|
108
|
+
local failed_paths_json="$2" # 예: [["verifications", 0, "fact_id"], ["self_rules", 1, "category"]]
|
|
109
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
110
|
+
return 1
|
|
111
|
+
fi
|
|
112
|
+
node -e "
|
|
113
|
+
try {
|
|
114
|
+
const src = JSON.parse(process.argv[1]);
|
|
115
|
+
const paths = JSON.parse(process.argv[2]);
|
|
116
|
+
if (!Array.isArray(paths) || paths.length === 0) { process.exit(1); }
|
|
117
|
+
const failedIdxByType = {
|
|
118
|
+
working_cards: false,
|
|
119
|
+
self_rules: new Set(),
|
|
120
|
+
evidence: new Set(),
|
|
121
|
+
verifications: new Set(),
|
|
122
|
+
};
|
|
123
|
+
for (const p of paths) {
|
|
124
|
+
if (!Array.isArray(p) || p.length === 0) continue;
|
|
125
|
+
const t = p[0];
|
|
126
|
+
if (t === 'working_cards') { failedIdxByType.working_cards = true; continue; }
|
|
127
|
+
if (Object.prototype.hasOwnProperty.call(failedIdxByType, t) && Number.isInteger(p[1])) {
|
|
128
|
+
failedIdxByType[t].add(p[1]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const out = {};
|
|
132
|
+
if (src.working_cards && !failedIdxByType.working_cards) out.working_cards = src.working_cards;
|
|
133
|
+
for (const arrType of ['self_rules', 'evidence', 'verifications']) {
|
|
134
|
+
const arr = Array.isArray(src[arrType]) ? src[arrType] : [];
|
|
135
|
+
if (arr.length === 0) continue;
|
|
136
|
+
const kept = arr.filter((_, i) => !failedIdxByType[arrType].has(i));
|
|
137
|
+
if (kept.length > 0) out[arrType] = kept;
|
|
138
|
+
}
|
|
139
|
+
const keys = Object.keys(out);
|
|
140
|
+
if (keys.length === 0) { process.exit(1); }
|
|
141
|
+
// spec 2026-07-06 §F-1(a) — top-level repo 컨텍스트 보존(400 재구성 저장도 세션 project 유지).
|
|
142
|
+
if (typeof src.repo_url === 'string') out.repo_url = src.repo_url;
|
|
143
|
+
if (typeof src.repo_path === 'string') out.repo_path = src.repo_path;
|
|
144
|
+
process.stdout.write(JSON.stringify(out));
|
|
145
|
+
} catch (e) {
|
|
146
|
+
process.stderr.write('build_filtered_payload_error: ' + (e && e.message ? e.message : String(e)) + '\n');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
" "$mutate_json" "$failed_paths_json" 2>>"$ROOKIE_DROP_LOG"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# 내부: queue_enqueue wrapper. queue.sh 가 source 안 됐으면 telemetry 로 fallback.
|
|
153
|
+
_safe_enqueue() {
|
|
154
|
+
local label="$1"
|
|
155
|
+
local body="$2"
|
|
156
|
+
local code="$3"
|
|
157
|
+
if command -v queue_enqueue >/dev/null 2>&1; then
|
|
158
|
+
queue_enqueue "$label" "$body" "$code" 2>/dev/null
|
|
159
|
+
return $?
|
|
160
|
+
fi
|
|
161
|
+
printf '%s queue_unavailable label=%s code=%s\n' \
|
|
162
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$label" "$code" \
|
|
163
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
164
|
+
return 1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# 내부: partial failure 처리. http_code=200 응답 body 를 파싱해 failed[] 분기.
|
|
168
|
+
# 0 return = 처리 완료 (정상 또는 retry 재큐 성공/fallback)
|
|
169
|
+
_handle_200() {
|
|
170
|
+
local body_path="$1"
|
|
171
|
+
local mutate_json="$2"
|
|
172
|
+
local session_id="$3"
|
|
173
|
+
|
|
174
|
+
# jq 미설치 → 응답 schema 파싱 불가능. 보수적 처리: 전체 mutate_json 을 retry 큐로 재적재
|
|
175
|
+
# + warn 1줄. silent drop 방지.
|
|
176
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
177
|
+
_safe_enqueue "$session_id-mutate-retry" "$mutate_json" "200-jq-missing" || true
|
|
178
|
+
printf '%s jq_missing_partial_failure_unknown session=%s\n' \
|
|
179
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session_id" \
|
|
180
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
local failed_count jq_status
|
|
185
|
+
failed_count=$(jq -r '.summary.failed | length // 0' < "$body_path" 2>>"$ROOKIE_DROP_LOG")
|
|
186
|
+
jq_status=$?
|
|
187
|
+
if [ "$jq_status" -ne 0 ] || [ -z "$failed_count" ] || [ "$failed_count" = "null" ]; then
|
|
188
|
+
# jq 파싱 실패 — 응답 schema 비정상. silent 0 처리 금지, fallback 으로 전체 재큐.
|
|
189
|
+
_safe_enqueue "$session_id-mutate-retry" "$mutate_json" "200-jq-parse-fail" || true
|
|
190
|
+
printf '%s jq_parse_failed session=%s\n' \
|
|
191
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session_id" \
|
|
192
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
193
|
+
return 0
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
if [ "$failed_count" -le 0 ] 2>/dev/null; then
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
local failed_json
|
|
201
|
+
failed_json=$(jq -c '.summary.failed' < "$body_path" 2>>"$ROOKIE_DROP_LOG") || failed_json=""
|
|
202
|
+
if [ -z "$failed_json" ] || [ "$failed_json" = "null" ]; then
|
|
203
|
+
# failed length > 0 인데 추출 실패 — 비정상. fallback 으로 전체 재큐.
|
|
204
|
+
_safe_enqueue "$session_id-mutate-retry" "$mutate_json" "200-failed-extract-fail" || true
|
|
205
|
+
return 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
local retry_json
|
|
209
|
+
retry_json=$(_build_retry_payload "$mutate_json" "$failed_json") || retry_json=""
|
|
210
|
+
if [ -z "$retry_json" ]; then
|
|
211
|
+
_safe_enqueue "$session_id-mutate-retry" "$mutate_json" "200-build-fail" || true
|
|
212
|
+
return 0
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
_safe_enqueue "$session_id-mutate-retry" "$retry_json" "200-partial" || true
|
|
216
|
+
return 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# 내부: HTTP 400 invalid_body 응답 처리 — spec §17.2 H-2.3.
|
|
220
|
+
# stdout = 결함 element 를 제외한 재구성 mutate_json (재시도 가능, non-empty 일 때),
|
|
221
|
+
# 빈 출력 = caller 가 fallback (전체 dead-letter or 기존 400 로직 진입).
|
|
222
|
+
# return 0 = invalid_body 분기 처리됨, return 1 = invalid_body 아님/파싱 불가 → caller 가 기존 400 로직.
|
|
223
|
+
_handle_400_invalid_body() {
|
|
224
|
+
local body_path="$1"
|
|
225
|
+
local mutate_json="$2"
|
|
226
|
+
local session_id="$3"
|
|
227
|
+
|
|
228
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
229
|
+
return 1
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
local error_kind
|
|
233
|
+
error_kind=$(jq -r '.error // ""' < "$body_path" 2>>"$ROOKIE_DROP_LOG")
|
|
234
|
+
if [ "$error_kind" != "invalid_body" ]; then
|
|
235
|
+
return 1
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
local paths_json
|
|
239
|
+
paths_json=$(jq -c '[.detail[]?.path]' < "$body_path" 2>>"$ROOKIE_DROP_LOG") || paths_json=""
|
|
240
|
+
if [ -z "$paths_json" ] || [ "$paths_json" = "null" ] || [ "$paths_json" = "[]" ]; then
|
|
241
|
+
return 1
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# 결함 element 들 dead-letter 이동 (전체 mutate_json 보존 — 추적용).
|
|
245
|
+
# spec §17.2 H-2.3: 재시도 무의미 → 즉시 격리. spec §10: chmod 600.
|
|
246
|
+
local ts
|
|
247
|
+
ts=$(date -u +%Y%m%dT%H%M%SZ)
|
|
248
|
+
local dead_letter_path="$ROOKIE_DEAD_LETTER_DIR/${session_id}-mutate-invalid-${ts}.jsonl"
|
|
249
|
+
mkdir -m 700 -p "$ROOKIE_DEAD_LETTER_DIR" 2>/dev/null || true
|
|
250
|
+
printf '%s\n' "$mutate_json" > "$dead_letter_path" 2>>"$ROOKIE_DROP_LOG"
|
|
251
|
+
chmod 600 "$dead_letter_path" 2>/dev/null || true
|
|
252
|
+
# telemetry: path 만 기록, 본문 미포함 (spec §10).
|
|
253
|
+
printf '%s invalid_body session=%s paths=%s dead_letter=%s\n' \
|
|
254
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session_id" "$paths_json" "$dead_letter_path" \
|
|
255
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
256
|
+
|
|
257
|
+
# 정상 element 재구성 — 모두 결함이면 빈 출력 (caller 가 fallback).
|
|
258
|
+
local filtered
|
|
259
|
+
filtered=$(_build_filtered_payload "$mutate_json" "$paths_json") || filtered=""
|
|
260
|
+
printf '%s' "$filtered"
|
|
261
|
+
return 0
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
mutate_dispatch() {
|
|
265
|
+
local endpoint="$1"
|
|
266
|
+
local token="$2"
|
|
267
|
+
local user_id="$3"
|
|
268
|
+
local session_id="$4"
|
|
269
|
+
local mutate_json="$5"
|
|
270
|
+
# spec §17.2 H-2.3: 재귀 cap=2. 외부 호출은 depth=0, _handle_400_invalid_body 회수 후 +1.
|
|
271
|
+
local depth="${6:-0}"
|
|
272
|
+
|
|
273
|
+
if [ -z "$mutate_json" ]; then
|
|
274
|
+
return 2
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
local body_tmp
|
|
278
|
+
body_tmp=$(mktemp -t rookie-mutate-body.XXXXXX 2>>"$ROOKIE_DROP_LOG") || body_tmp=""
|
|
279
|
+
if [ -z "$body_tmp" ]; then
|
|
280
|
+
# mktemp 실패 시 silent drop 차단 — partial failure 회수 불가능하므로
|
|
281
|
+
# 1차 POST 시도조차 안 하고 mutate_json 전체를 재큐에 넣어 다음 drain 으로 회수.
|
|
282
|
+
# spec §17.2 H-2 큐 메커니즘 의존 (서버측 멱등성 + retry cap).
|
|
283
|
+
_safe_enqueue "$session_id-mutate" "$mutate_json" "mktemp-fail" || true
|
|
284
|
+
printf '%s mktemp_fail session=%s\n' \
|
|
285
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session_id" \
|
|
286
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
287
|
+
return 1
|
|
288
|
+
fi
|
|
289
|
+
# spec §10: 임시 파일에 응답 body 가 들어가지만 본문은 stdout 출력 금지.
|
|
290
|
+
# trap 으로 함수 종료 시 정리 (bash RETURN trap, dispatcher.sh shebang 보장).
|
|
291
|
+
# set -u 호환을 위해 :- 가드.
|
|
292
|
+
trap 'rm -f "${body_tmp:-}" 2>/dev/null || true' RETURN
|
|
293
|
+
|
|
294
|
+
local http_code
|
|
295
|
+
http_code=$(_post_once "$endpoint" "$token" "$user_id" "$mutate_json" "$body_tmp")
|
|
296
|
+
|
|
297
|
+
if [ "$http_code" = "200" ]; then
|
|
298
|
+
_handle_200 "$body_tmp" "$mutate_json" "$session_id"
|
|
299
|
+
return 0
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# spec §17.2 H-2.3 — 400 invalid_body partial validation 회수.
|
|
303
|
+
# depth<2 일 때만 회수 시도. 재구성 JSON 이 있으면 재귀 호출 (sleep/재큐 우회 — 결함 element 는 이미 dead-letter 격리됨).
|
|
304
|
+
# _handle_400_invalid_body 가 return 1 (invalid_body 아님 / jq 미설치 / 파싱 실패) → 기존 H-2.1 retry 로직 진입.
|
|
305
|
+
if [ "$http_code" = "400" ] && [ "$depth" -lt 2 ]; then
|
|
306
|
+
local refiltered
|
|
307
|
+
refiltered=$(_handle_400_invalid_body "$body_tmp" "$mutate_json" "$session_id")
|
|
308
|
+
local handle_status=$?
|
|
309
|
+
if [ "$handle_status" = "0" ]; then
|
|
310
|
+
if [ -n "$refiltered" ]; then
|
|
311
|
+
# 재구성 JSON 재시도 (depth+1)
|
|
312
|
+
mutate_dispatch "$endpoint" "$token" "$user_id" "$session_id" "$refiltered" "$((depth + 1))"
|
|
313
|
+
return $?
|
|
314
|
+
fi
|
|
315
|
+
# 모든 element 결함 → 이미 dead-letter 이동됨, 더 이상 할 일 없음.
|
|
316
|
+
return 0
|
|
317
|
+
fi
|
|
318
|
+
# handle_status=1 → invalid_body 분기 아님 → 아래 기존 retry 로직으로 fall-through
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# spec §17.2 H-2.3 무한 루프 방지: depth>=2 도달 했는데도 또 invalid_body → 통째 dead-letter.
|
|
322
|
+
# (재구성을 또 해봐도 같은 결함 클래스 — 재시도 무의미. 큐 재진입 차단.)
|
|
323
|
+
if [ "$http_code" = "400" ] && [ "$depth" -ge 2 ]; then
|
|
324
|
+
if command -v jq >/dev/null 2>&1; then
|
|
325
|
+
local error_kind_cap
|
|
326
|
+
error_kind_cap=$(jq -r '.error // ""' < "$body_tmp" 2>>"$ROOKIE_DROP_LOG")
|
|
327
|
+
if [ "$error_kind_cap" = "invalid_body" ]; then
|
|
328
|
+
local ts_cap
|
|
329
|
+
ts_cap=$(date -u +%Y%m%dT%H%M%SZ)
|
|
330
|
+
local dead_letter_path_cap="$ROOKIE_DEAD_LETTER_DIR/${session_id}-mutate-invalid-cap-${ts_cap}.jsonl"
|
|
331
|
+
mkdir -m 700 -p "$ROOKIE_DEAD_LETTER_DIR" 2>/dev/null || true
|
|
332
|
+
printf '%s\n' "$mutate_json" > "$dead_letter_path_cap" 2>>"$ROOKIE_DROP_LOG"
|
|
333
|
+
chmod 600 "$dead_letter_path_cap" 2>/dev/null || true
|
|
334
|
+
printf '%s invalid_body_cap session=%s depth=%s dead_letter=%s\n' \
|
|
335
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session_id" "$depth" "$dead_letter_path_cap" \
|
|
336
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
fi
|
|
340
|
+
# invalid_body 아닌 400 (또는 jq 미설치) → fall-through 으로 기존 H-2.1 retry 로직 진입.
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# 1차 실패 → spec §17.2 H-2.1 sleep 1 + 1회 retry.
|
|
344
|
+
sleep 1
|
|
345
|
+
http_code=$(_post_once "$endpoint" "$token" "$user_id" "$mutate_json" "$body_tmp")
|
|
346
|
+
|
|
347
|
+
if [ "$http_code" = "200" ]; then
|
|
348
|
+
_handle_200 "$body_tmp" "$mutate_json" "$session_id"
|
|
349
|
+
return 0
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
# 2회 모두 실패 → 전체 재큐.
|
|
353
|
+
_safe_enqueue "$session_id-mutate" "$mutate_json" "$http_code" || true
|
|
354
|
+
return 1
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# v6 P1 #5 — Stage 1 측정 hook buffer flush.
|
|
358
|
+
# spec §7.1 / plan Task 4
|
|
359
|
+
#
|
|
360
|
+
# PreToolUse hook 가 적재한 ~/.rookie/metric-pending/<session>-md-read.jsonl 을
|
|
361
|
+
# entries 배열로 wrap → /api/rookie/metric/md-read 로 batch POST.
|
|
362
|
+
# 200 → buffer 삭제. 실패 → buffer 보존 (다음 세션 재시도, retry cap 안 둠).
|
|
363
|
+
#
|
|
364
|
+
# return 0 = 성공 또는 no-op (buffer 없음/빈값), 1 = POST 실패 (buffer 잔존)
|
|
365
|
+
metric_dispatch_md_read() {
|
|
366
|
+
local endpoint="$1"
|
|
367
|
+
local token="$2"
|
|
368
|
+
local user_id="$3"
|
|
369
|
+
local session_id="$4"
|
|
370
|
+
local buf_dir="${ROOKIE_METRIC_BUFFER_DIR:-$HOME/.rookie/metric-pending}"
|
|
371
|
+
local buf_file="$buf_dir/${session_id}-md-read.jsonl"
|
|
372
|
+
|
|
373
|
+
[ -f "$buf_file" ] || return 0
|
|
374
|
+
if [ ! -s "$buf_file" ]; then
|
|
375
|
+
rm -f "$buf_file"
|
|
376
|
+
return 0
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
380
|
+
return 1
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
local body
|
|
384
|
+
body=$(node -e "
|
|
385
|
+
try {
|
|
386
|
+
const fs = require('fs');
|
|
387
|
+
const lines = fs.readFileSync(process.argv[1], 'utf8').split('\n').filter(Boolean);
|
|
388
|
+
const entries = [];
|
|
389
|
+
for (const line of lines) {
|
|
390
|
+
try { entries.push(JSON.parse(line)); } catch {}
|
|
391
|
+
}
|
|
392
|
+
if (entries.length === 0) process.exit(1);
|
|
393
|
+
process.stdout.write(JSON.stringify({ entries }));
|
|
394
|
+
} catch (e) {
|
|
395
|
+
process.stderr.write('metric_buffer_parse_error: ' + (e && e.message ? e.message : String(e)) + '\n');
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
" "$buf_file" 2>>"$ROOKIE_DROP_LOG") || return 1
|
|
399
|
+
|
|
400
|
+
if [ -z "$body" ]; then
|
|
401
|
+
return 1
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
local code
|
|
405
|
+
code=$(curl -sS --max-time 10 -o /dev/null -w "%{http_code}" \
|
|
406
|
+
-X POST \
|
|
407
|
+
-H "Authorization: Bearer $token" \
|
|
408
|
+
-H "Content-Type: application/json" \
|
|
409
|
+
-H "x-rookie-user-id: $user_id" \
|
|
410
|
+
-d "$body" \
|
|
411
|
+
"$endpoint/api/rookie/metric/md-read" 2>>"$ROOKIE_DROP_LOG") || code="000"
|
|
412
|
+
|
|
413
|
+
if [ "$code" = "200" ]; then
|
|
414
|
+
rm -f "$buf_file"
|
|
415
|
+
return 0
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
# 실패 — buffer 보존, telemetry 1줄.
|
|
419
|
+
printf '%s metric_md_read_post_failed session=%s code=%s\n' \
|
|
420
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session_id" "$code" \
|
|
421
|
+
>> "$ROOKIE_DROP_LOG" 2>/dev/null || true
|
|
422
|
+
return 1
|
|
423
|
+
}
|