@chrono-meta/fh-gate 1.4.90 → 1.4.92
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/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +20 -0
- package/CLAUDE.md +23 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +160 -4
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +128 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +149 -0
- package/package.json +8 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +13 -0
- package/scripts/branch_claim.sh +318 -0
- package/scripts/fh_env_delta_scan.sh +15 -0
- package/scripts/fh_session_load.sh +34 -1
- package/scripts/field_canon_preload.sh +129 -0
- package/scripts/hook_source_lib.sh +41 -0
- package/scripts/selfcheck.sh +40 -0
- package/scripts/session_close_check.sh +134 -1
- package/scripts/test_branch_claim_lanes.sh +292 -0
- package/scripts/test_dispatch_log_lanes.sh +35 -1
- package/scripts/test_field_canon_lanes.sh +142 -0
- package/scripts/test_hook_source_gate_lanes.sh +81 -0
- package/templates/.git-hooks/pre-commit +44 -0
- package/templates/settings.FieldCanon.snippet.json +51 -0
|
@@ -94,6 +94,128 @@ if command -v gh >/dev/null 2>&1; then
|
|
|
94
94
|
[ "${PRS:-0}" -gt 0 ] && echo "⚠️ ①-b $PRS open PR(s) by you — classify: self-mergeable vs awaiting-external"
|
|
95
95
|
fi
|
|
96
96
|
|
|
97
|
+
# ── ①-d CLOSE RE-OPENED? ──────────────────────────────────────────────────────
|
|
98
|
+
# WHY: a session that closed cleanly and then kept working looks IDENTICAL to one that never
|
|
99
|
+
# closed — both read Completed from outside, and ①-c cannot separate them. Measured 2026-08-09:
|
|
100
|
+
# this session ran step 5 eleven times and a peer independently named the same gap. The two need
|
|
101
|
+
# opposite actions — an unfinished close must be FINISHED, a re-opened one must be RE-FOLDED.
|
|
102
|
+
# HOW: stamp each close-gated pass and count. Self-attested (whoever runs the check writes it):
|
|
103
|
+
# it detects the pattern, it does not prove it.
|
|
104
|
+
CLOSE_STAMP="$FH/tracks/_meta/.close_stamps_$TODAY"
|
|
105
|
+
if [ "${FH_SESSION_CLOSE:-0}" = "1" ]; then
|
|
106
|
+
mkdir -p "$(dirname "$CLOSE_STAMP")" 2>/dev/null
|
|
107
|
+
date -u +%Y-%m-%dT%H:%M:%SZ >> "$CLOSE_STAMP" 2>/dev/null || true
|
|
108
|
+
fi
|
|
109
|
+
if [ -f "$CLOSE_STAMP" ]; then
|
|
110
|
+
CLOSE_N=$(grep -c . "$CLOSE_STAMP" 2>/dev/null | tr -d ' ')
|
|
111
|
+
if [ "${CLOSE_N:-0}" -gt 1 ]; then
|
|
112
|
+
echo "ℹ️ ①-d close ran $CLOSE_N times today — CLOSED-THEN-WORKED, not a stalled close."
|
|
113
|
+
echo " Both look Completed from outside; only this counter separates them."
|
|
114
|
+
echo " Every re-open owes a FULL step 5: append to fh_completed FIRST, then rewrite the card."
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# ── ①-c LIVE PEER SESSIONS in this same harness ─────────────────────────────────
|
|
119
|
+
# WHY: FH's close chain has an agreed two-axis order (peer appends → ping → **ask "더 있나" before
|
|
120
|
+
# folding** → fold → re-check merged PRs), and it lived only in the session card as prose. Measured
|
|
121
|
+
# 2026-08-09, the first two-axis close: the ping surfaced four deltas the closing session did not
|
|
122
|
+
# know about, and **two of them were structurally invisible to `gh pr list`** (an inheritance
|
|
123
|
+
# channel and a landing check — no PR exists for either). So a PR sweep is not a substitute; the
|
|
124
|
+
# question is. This block does the DETECTION half mechanically. It never merges anything and never
|
|
125
|
+
# blocks — the merge is where the loss risk is (two sessions writing one card is the shared-checkout
|
|
126
|
+
# hazard this repo already named), and one occurrence is below this repo's own mechanization bar.
|
|
127
|
+
#
|
|
128
|
+
# HOW: a live session owns /tmp/cc-socks/<pid>.sock. pid → cwd resolves the ONE thing that actually
|
|
129
|
+
# matters, "is this peer in MY harness" — which the agent-facing session list cannot answer, because
|
|
130
|
+
# it reports names, not directories. Stale sockets are detectable (pid gone), so absence here is a
|
|
131
|
+
# measurement rather than silence.
|
|
132
|
+
# SCOPE (operator, 2026-08-09): this is a **claude-agents / Agent-View** feature — the surface where
|
|
133
|
+
# sessions are deliberately run in parallel. A plain solo terminal must not be nagged because some
|
|
134
|
+
# other window happens to be open. Discriminator is env, set by the parent that spawns the session.
|
|
135
|
+
# ⚠️ NEGATIVE ARM UNVERIFIED: a child session cannot observe a top-level session's environment, so
|
|
136
|
+
# "a real top-level session lacks these vars" is asserted, not measured. The lane simulates absence
|
|
137
|
+
# by unsetting them, which tests THIS SCRIPT'S logic — not that claim. Verify from a plain terminal.
|
|
138
|
+
if [ -z "${CLAUDE_CODE_CHILD_SESSION:-}${CLAUDE_CODE_AGENT:-}" ] && [ "${FH_PEER_SCAN_FORCE:-0}" != "1" ]; then
|
|
139
|
+
: # solo/top-level surface — ①-c is out of scope here, and silence is the correct output
|
|
140
|
+
else
|
|
141
|
+
PEER_LIVE=0; PEER_UNKNOWN=0; PEER_STALE=0; PEER_SPARE=0; PEER_IDS=""
|
|
142
|
+
SOCK_DIR="${FH_PEER_SOCK_DIR:-/tmp/cc-socks}"
|
|
143
|
+
# Self-exclusion must walk the ANCESTOR chain, not compare `$$`. This script is a grandchild of the
|
|
144
|
+
# session process that owns the socket, so `$$` never matches it — measured: the first run counted
|
|
145
|
+
# the calling session as its own peer, i.e. it would tell you to go ask yourself.
|
|
146
|
+
SELF_CHAIN=" $$ "
|
|
147
|
+
_p=$$
|
|
148
|
+
while :; do
|
|
149
|
+
_p=$(ps -o ppid= -p "$_p" 2>/dev/null | tr -d ' ')
|
|
150
|
+
case "$_p" in ''|0|1) break ;; esac
|
|
151
|
+
SELF_CHAIN="$SELF_CHAIN$_p "
|
|
152
|
+
done
|
|
153
|
+
_peer_cwd() { # $1=pid → cwd on stdout, empty if unresolvable
|
|
154
|
+
if [ -r "/proc/$1/cwd" ]; then readlink "/proc/$1/cwd" 2>/dev/null; return; fi
|
|
155
|
+
command -v lsof >/dev/null 2>&1 || return 0
|
|
156
|
+
lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1
|
|
157
|
+
}
|
|
158
|
+
if [ -d "$SOCK_DIR" ]; then
|
|
159
|
+
for _s in "$SOCK_DIR"/*.sock; do
|
|
160
|
+
[ -e "$_s" ] || continue
|
|
161
|
+
_pid="${_s##*/}"; _pid="${_pid%.sock}"
|
|
162
|
+
case "$_pid" in ''|*[!0-9]*) continue ;; esac
|
|
163
|
+
case "$SELF_CHAIN" in *" $_pid "*) continue ;; esac # 나 자신(조상 체인)
|
|
164
|
+
if ! ps -p "$_pid" >/dev/null 2>&1; then PEER_STALE=$((PEER_STALE+1)); continue; fi
|
|
165
|
+
# bg-spare = pre-warmed worker, NOT a session. It owns a socket and a cwd like everything else,
|
|
166
|
+
# so cwd matching alone counts it as a peer — measured 2026-08-09, and it is a pure false
|
|
167
|
+
# positive with a clean signature. Exclude mechanically rather than explaining it away.
|
|
168
|
+
case "$(ps -p "$_pid" -o args= 2>/dev/null)" in *--bg-spare*) PEER_SPARE=$((PEER_SPARE+1)); continue ;; esac
|
|
169
|
+
_pcwd="$(_peer_cwd "$_pid")"
|
|
170
|
+
if [ -z "$_pcwd" ]; then
|
|
171
|
+
# Alive but undirected. NOT zero — a peer we cannot place is exactly the case that must not
|
|
172
|
+
# render as "no peers" (not-found != zero).
|
|
173
|
+
PEER_UNKNOWN=$((PEER_UNKNOWN+1)); continue
|
|
174
|
+
fi
|
|
175
|
+
case "$_pcwd" in
|
|
176
|
+
"$FH"|"$FH"/*) PEER_LIVE=$((PEER_LIVE+1)); PEER_IDS="$PEER_IDS $_pid" ;;
|
|
177
|
+
esac
|
|
178
|
+
done
|
|
179
|
+
else
|
|
180
|
+
PEER_UNKNOWN=-1
|
|
181
|
+
fi
|
|
182
|
+
if [ "$PEER_UNKNOWN" = "-1" ]; then
|
|
183
|
+
echo "ℹ️ ①-c peer scan UNMEASURED — no session-socket dir at $SOCK_DIR (not 'no peers')"
|
|
184
|
+
elif [ "$PEER_LIVE" -gt 0 ]; then
|
|
185
|
+
echo "⚠️ ①-c $PEER_LIVE peer CANDIDATE(s) in THIS harness —$PEER_IDS"
|
|
186
|
+
echo " The surplus over the fleet view Working list is MIXED — do not read it as one thing."
|
|
187
|
+
echo " Measured 2026-08-09, three kinds land here and only the first needs a message:"
|
|
188
|
+
echo " working session → ask before folding"
|
|
189
|
+
echo " idle terminal → a real session sitting at a prompt. Not working. Usually skip."
|
|
190
|
+
echo " lingering process → a close that never finished (seen once, had to be killed by hand)"
|
|
191
|
+
echo " Only the fleet view separates them. An earlier version of this text called the whole"
|
|
192
|
+
echo " surplus unfinished closes; that was one case generalised, and it was wrong."
|
|
193
|
+
echo " Do NOT switch to the job state file to narrow it: it marked THIS session done"
|
|
194
|
+
echo " (firstTerminalAt set) while it was still running — it under-reports as hard as"
|
|
195
|
+
echo " sockets over-report. Cross-read with the fleet view; neither source alone is right."
|
|
196
|
+
echo " THIS IS A DETECTION LIST, NOT A RECIPIENT LIST — do not message all of them."
|
|
197
|
+
echo " What survives here is: a live PROCESS whose cwd is this harness, minus pre-warmed"
|
|
198
|
+
echo " spares. That still includes IDLE TERMINALS sitting at a prompt. It is NOT a list of"
|
|
199
|
+
echo " sessions that are working — nothing here can tell you that; the fleet view can."
|
|
200
|
+
echo " Notify only the ones actually WORKING/BUSY right now. Messaging a finished session"
|
|
201
|
+
echo " is noise, burns a recipient approval, and does not land in their record."
|
|
202
|
+
echo " Of those still working, ASK '지금부터 마감이다, 더 있나' — the question is wider than a"
|
|
203
|
+
echo " PR sweep: a peer's inheritance-channel or landing-check work has no PR to find."
|
|
204
|
+
echo " Then fold, then re-check \`gh pr list --merged\`. Merging their delta stays MANUAL."
|
|
205
|
+
echo " SENDING IS NOT DELIVERING — a cross-session message can be held for the recipient"
|
|
206
|
+
echo " user approval and never arrive; the send call still reports success. Measured"
|
|
207
|
+
echo " 2026-08-09: 2 of 5 notices were held while the card already claimed all 5 informed."
|
|
208
|
+
echo " Record ANSWERS, not sends, and put the unanswered count in the card."
|
|
209
|
+
[ "$PEER_UNKNOWN" -gt 0 ] && echo " + $PEER_UNKNOWN live session(s) whose directory is UNKNOWN (counted, not dismissed)"
|
|
210
|
+
elif [ "$PEER_UNKNOWN" -gt 0 ]; then
|
|
211
|
+
echo "⚠️ ①-c 0 peers placed in this harness, but $PEER_UNKNOWN live session(s) are UNPLACEABLE"
|
|
212
|
+
echo " — treat as possible peers and ask; do not read this as 'closing alone'."
|
|
213
|
+
else
|
|
214
|
+
echo "✅ ①-c no live peer in this harness (${PEER_STALE} stale · ${PEER_SPARE} pre-warmed spare(s) ignored)"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
fi
|
|
218
|
+
|
|
97
219
|
# ② FH assets changed today → harvest-loop owed
|
|
98
220
|
# NOTE: no `grep -q` here — under `set -o pipefail`, -q's early exit SIGPIPEs git log (141),
|
|
99
221
|
# masking a real match as pipeline failure so the warning NEVER fired on true positives
|
|
@@ -219,7 +341,18 @@ DISPATCHED=$(_int "$(grep -c "^$TODAY$" "$TALLY" 2>/dev/null | tr -d ' ' || true
|
|
|
219
341
|
# while anything appended via yaml.dump renders `- date: '"'"'2026-08-02'"'"'`. Matching one form counted
|
|
220
342
|
# half the entries as absent — a divergent-normalizer miss inside the check that exists to catch
|
|
221
343
|
# missing records. Known-pair calibrated below in test_dispatch_log_lanes.sh.
|
|
222
|
-
|
|
344
|
+
# ── 전환 설계: 레거시 단일 파일 ∪ 세션별 디렉터리를 **둘 다** 읽는다 ────────────
|
|
345
|
+
# WHY: 같은 파일 끝에 여러 세션이 append 하면 브랜치 병합이 확정 충돌이고, 그 해소가
|
|
346
|
+
# 조용히 손상된다 — git 이 엔트리 헤더 한 줄을 공통 접두로 밀어내서 «양쪽 다 보존»이라는
|
|
347
|
+
# 정답 해소가 엔트리를 흡수시킨다(2026-08-09 재현: 항목 133/정답 134, 파싱은 통과).
|
|
348
|
+
# 처방은 세션당 한 파일. 다만 **마이그레이션과 배선을 같은 커밋에 묶으면** 아직 머지 안 된
|
|
349
|
+
# 원장 브랜치와 대형 충돌이 나므로, 소비자를 먼저 «둘 다 읽게» 만든다. 그러면 데이터가
|
|
350
|
+
# 언제 옮겨가든 이 검사는 안 깨진다. 두 상태 모두 known-pair 로 고정돼 있다.
|
|
351
|
+
LOGDIR="$FH/knowledge/shared/learnings/subagent_invocations"
|
|
352
|
+
_cat_logs() { # 다중 파일에 grep -c 를 직접 걸면 파일별 `경로:개수` 를 찍어 합산이 깨진다(실측).
|
|
353
|
+
{ [ -f "$LOG" ] && cat "$LOG"; [ -d "$LOGDIR" ] && cat "$LOGDIR"/*.yaml; } 2>/dev/null
|
|
354
|
+
}
|
|
355
|
+
LOGGED=$(_int "$(_cat_logs | grep -cE "^- date: *'?$TODAY'?" | tr -d ' ' || true)")
|
|
223
356
|
if [ "${DISPATCHED:-0}" -gt 0 ] && [ "${LOGGED:-0}" -eq 0 ]; then
|
|
224
357
|
echo "❌ ④-e $DISPATCHED sub-agent dispatch(es) today and ZERO invocation-log entries — the 60/40"
|
|
225
358
|
echo " promotion gate and the UAP loop both read that file; an unlogged session is invisible to"
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_branch_claim_lanes.sh — branch_claim.sh known-pair 앵커.
|
|
3
|
+
#
|
|
4
|
+
# 왜 실행 테스트인가: `bash -n` 은 계기가 아니다 — 문법만 보고 exit 0 을 내므로
|
|
5
|
+
# «전부 통과»와 구분이 안 된다([[feedback_gate_verification_must_execute]]).
|
|
6
|
+
# «막혔다» ≠ «옳게 막혔다» 이므로 BLOCK 레인은 **완성 문장**으로 매칭한다 —
|
|
7
|
+
# 조각 매칭은 claim/HEAD 역할을 뒤집는 뮤테이션을 통과시켰다(적대검증 A-5 실측).
|
|
8
|
+
# 그 역할 뒤바뀜은 사용자를 반대 브랜치로 보내므로 게이트가 사고를 만드는 쪽이 된다.
|
|
9
|
+
#
|
|
10
|
+
# 격리: 레인마다 임시 저장소를 새로 판다. 실제 레포는 안 건드린다.
|
|
11
|
+
# 세션 시뮬레이션: FH_CLAIM_TEST=1 + FH_CLAIM_SESSION 으로 두 세션을 흉내낸다.
|
|
12
|
+
#
|
|
13
|
+
# usage: bash scripts/test_branch_claim_lanes.sh
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
SCRIPT="$(cd "$(dirname "$0")" && pwd)/branch_claim.sh"
|
|
17
|
+
PASS=0; FAIL=0; LIVE_PID=""
|
|
18
|
+
|
|
19
|
+
cleanup() {
|
|
20
|
+
[ -n "$LIVE_PID" ] && kill "$LIVE_PID" 2>/dev/null
|
|
21
|
+
[ -n "${SOCK_EMPTY:-}" ] && rm -rf "$SOCK_EMPTY"
|
|
22
|
+
return 0
|
|
23
|
+
}
|
|
24
|
+
trap cleanup EXIT
|
|
25
|
+
|
|
26
|
+
_mkrepo() {
|
|
27
|
+
local d; d=$(mktemp -d "${TMPDIR:-/tmp}/bclaim.XXXXXX")
|
|
28
|
+
git -C "$d" init -q -b main
|
|
29
|
+
git -C "$d" config user.email t@t; git -C "$d" config user.name t
|
|
30
|
+
echo x > "$d/f"; git -C "$d" add f; git -C "$d" commit -qm init
|
|
31
|
+
echo "$d"
|
|
32
|
+
}
|
|
33
|
+
# 살아있는 peer 세션 하나를 실제 프로세스로 만든다 (kill -0 이 참이 되도록)
|
|
34
|
+
# ⚠️ 반드시 fd 를 닫아서 띄운다: 백그라운드 프로세스가 stdout 을 물고 있으면
|
|
35
|
+
# 이 스크립트를 파이프에 물릴 때 파이프가 안 닫혀 무한 대기한다(실측: 120s 타임아웃).
|
|
36
|
+
_spawn_live() { sleep 900 >/dev/null 2>&1 & LIVE_PID=$!; echo "$LIVE_PID"; }
|
|
37
|
+
|
|
38
|
+
_lane() { # name expected_rc actual_rc [must_contain...]
|
|
39
|
+
local name="$1" exp="$2" got="$3"; shift 3
|
|
40
|
+
local out="$LANE_OUT" ok=1 pat
|
|
41
|
+
[ "$got" = "$exp" ] || { ok=0; echo " ❌ $name — rc=$got (기대 $exp)"; }
|
|
42
|
+
for pat in "$@"; do
|
|
43
|
+
case "$out" in *"$pat"*) ;; *) ok=0; echo " ❌ $name — 출력에 «${pat}» 없음";; esac
|
|
44
|
+
done
|
|
45
|
+
if [ "$ok" = 1 ]; then echo " ✅ $name"; PASS=$((PASS+1)); else FAIL=$((FAIL+1));
|
|
46
|
+
echo " ── 실제 출력 ──"; echo "$out" | sed 's/^/ /'; fi
|
|
47
|
+
}
|
|
48
|
+
_ok() { if [ "$2" = 1 ]; then echo " ✅ $1"; PASS=$((PASS+1)); else echo " ❌ $1"; FAIL=$((FAIL+1)); fi; }
|
|
49
|
+
|
|
50
|
+
# 세션 B(=peer)의 claim 을 살아있는 pid 로 직접 심는다
|
|
51
|
+
_plant_peer() { # repo branch pid
|
|
52
|
+
mkdir -p "$1/.git/fh-claims"
|
|
53
|
+
printf 'branch=%s\nlabel=peer\npid=%s\nat_epoch=%s\n' "$2" "$3" "$(date +%s)" \
|
|
54
|
+
> "$1/.git/fh-claims/session-PEER"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ⚠️ 주변 환경을 흡입하지 않는다 — 이 한 줄이 없어서 이 스위트는 «거짓 초록» 이었다.
|
|
58
|
+
# branch_claim.sh 의 `_session_pid() { echo "${CLAUDE_PID:-${PPID:-$$}}"; }` 때문에, 저자 셸에
|
|
59
|
+
# CLAUDE_PID 가 있으면 claim 이 **살아있는 pid**(그 Claude 프로세스)를 기록한다. 레인 ③ 은
|
|
60
|
+
# A 가 재-claim 해서 peer 기록을 덮는 시나리오라, 그 pid 가 살아있느냐가 판정을 통째로 뒤집는다.
|
|
61
|
+
# 실측 2026-08-09 (known-pair, 같은 트리·같은 커밋):
|
|
62
|
+
# CLAUDE_PID 있음 → 24 PASS / 0 FAIL (저자 로컬 — 초록)
|
|
63
|
+
# CLAUDE_PID 없음 → 23 PASS / 1 FAIL ③ (CI — 적색, rc=0 기대 1)
|
|
64
|
+
# 즉 코드가 아니라 **저자 환경**을 재고 있었고, 그 초록이 S-1 재설계의 유일한 증명이었다.
|
|
65
|
+
# 앵커가 배선되기 전까지(=호출부 0개) 이 사실은 드러날 수 없었다.
|
|
66
|
+
unset CLAUDE_PID
|
|
67
|
+
|
|
68
|
+
# ⚠️ 같은 이유로 **소켓 경로도 환경에서 빌리지 않는다.** _unclaimed_risk 가 기본으로 실제
|
|
69
|
+
# `/tmp/cc-socks` 를 보므로, 그대로 두면 «지금 이 머신에 CC 세션이 몇 개 떠 있나» 가 레인
|
|
70
|
+
# 판정을 바꾼다 — 실측 2026-08-09: 이 수정을 넣자 레인 ④ 가 즉시 깨졌다(실제 세션 5개).
|
|
71
|
+
# **위의 CLAUDE_PID 와 같은 결함을 수리 그 자체가 재생산한 것**이라, 같은 처방을 붙인다.
|
|
72
|
+
# 개별 레인은 필요할 때 이 값을 인라인으로 덮는다.
|
|
73
|
+
SOCK_EMPTY=$(mktemp -d "${TMPDIR:-/tmp}/bcsock0.XXXXXX")
|
|
74
|
+
export FH_CLAIM_SOCK_DIR="$SOCK_EMPTY"
|
|
75
|
+
|
|
76
|
+
echo "[branch-claim] known-pair 앵커"
|
|
77
|
+
PEERPID=$(_spawn_live)
|
|
78
|
+
|
|
79
|
+
# ── ① 기록 없음 → 통과 (부재 ≠ 위반) ───────────────────────────────────────
|
|
80
|
+
R=$(_mkrepo); LANE_OUT=$(cd "$R" && bash "$SCRIPT" check 2>&1); rc=$?
|
|
81
|
+
_lane "① 기록 없음 → 통과" 0 "$rc"; rm -rf "$R"
|
|
82
|
+
|
|
83
|
+
# ── ② 내 기록 == HEAD → 통과, 그리고 at_epoch 이 갱신된다 (A-6) ────────────
|
|
84
|
+
R=$(_mkrepo)
|
|
85
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=1000 bash "$SCRIPT" claim a >/dev/null 2>&1)
|
|
86
|
+
before=$(sed -n 's/^at_epoch=//p' "$R/.git/fh-claims/me")
|
|
87
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=99000 bash "$SCRIPT" check 2>&1); rc=$?
|
|
88
|
+
after=$(sed -n 's/^at_epoch=//p' "$R/.git/fh-claims/me")
|
|
89
|
+
_lane "② 내 기록==HEAD → 통과" 0 "$rc"
|
|
90
|
+
_ok "②-b at_epoch 갱신됨 (${before} -> ${after}) — 나이가 «유휴»를 재게 된다" "$([ "$after" = 99000 ] && echo 1 || echo 0)"
|
|
91
|
+
rm -rf "$R"
|
|
92
|
+
|
|
93
|
+
# ── ③ ★ S-1 회귀: 양쪽 세션이 다 프로토콜을 지켜도 잡는다 ──────────────────
|
|
94
|
+
# 초판 설계는 여기서 통과했다(= 오늘 사고를 못 막았다). 이 레인이 그 반증이다.
|
|
95
|
+
# 타임라인: B가 main claim → A가 switch+claim(프로토콜 준수) → B가 커밋 시도
|
|
96
|
+
R=$(_mkrepo)
|
|
97
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=B bash "$SCRIPT" claim bee >/dev/null 2>&1)
|
|
98
|
+
_plant_peer "$R" main "$PEERPID" # A 를 살아있는 peer 로
|
|
99
|
+
git -C "$R" switch -q -c ifkakao # A 가 트리를 옮기고
|
|
100
|
+
# A 는 «살아있는» 세션이다 — 그 사실을 환경에서 빌리지 말고 레인이 직접 고정한다.
|
|
101
|
+
# CLAUDE_PID 를 안 주면 claim 이 곧 죽을 서브셸의 PPID 를 기록하고, B 의 check 는
|
|
102
|
+
# «살아있는 peer 0» 으로 통과한다 — 시나리오가 성립하지도 않은 채 초록이 된다.
|
|
103
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=session-PEER CLAUDE_PID="$PEERPID" bash "$SCRIPT" claim ay >/dev/null 2>&1) # 프로토콜대로 claim
|
|
104
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=B bash "$SCRIPT" check 2>&1); rc=$?
|
|
105
|
+
_lane "③ ★S-1 회귀: 양쪽이 프로토콜을 지켜도 B 는 차단된다" 1 "$rc" \
|
|
106
|
+
"이 세션은 'main' 로 기록돼 있는데 HEAD 는 'ifkakao' 다" "git switch -- 'main'"
|
|
107
|
+
rm -rf "$R"
|
|
108
|
+
|
|
109
|
+
# ── ④ 불일치인데 살아있는 peer 없음 → 경고만, 통과 (A-2 과차단 방지) ───────
|
|
110
|
+
R=$(_mkrepo)
|
|
111
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
112
|
+
git -C "$R" switch -q -c next
|
|
113
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" check 2>&1); rc=$?
|
|
114
|
+
_lane "④ 1인 세션(살아있는 peer 0) → 차단 안 함" 0 "$rc" "살아있는 peer 세션이 없어"
|
|
115
|
+
rm -rf "$R"
|
|
116
|
+
|
|
117
|
+
# ── ④-b 컨트롤: 같은 상황에 살아있는 peer 가 있으면 차단 ───────────────────
|
|
118
|
+
R=$(_mkrepo)
|
|
119
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
120
|
+
_plant_peer "$R" whatever "$PEERPID"
|
|
121
|
+
git -C "$R" switch -q -c next
|
|
122
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" check 2>&1); rc=$?
|
|
123
|
+
_lane "④-b 컨트롤: peer 살아있으면 차단 (④가 무조건 통과가 아님)" 1 "$rc"
|
|
124
|
+
rm -rf "$R"
|
|
125
|
+
|
|
126
|
+
# ── ④-c 죽은 peer 는 동시성 증거가 아니다 ──────────────────────────────────
|
|
127
|
+
R=$(_mkrepo)
|
|
128
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
129
|
+
_plant_peer "$R" whatever 999999 # 존재하지 않는 pid
|
|
130
|
+
git -C "$R" switch -q -c next
|
|
131
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" check 2>&1); rc=$?
|
|
132
|
+
_lane "④-c 죽은 peer → 차단 안 함 (시간 상수 의존을 줄인다)" 0 "$rc"
|
|
133
|
+
rm -rf "$R"
|
|
134
|
+
|
|
135
|
+
# ── ⑤ STALE → 통과 / 임계 미만 → 차단 ──────────────────────────────────────
|
|
136
|
+
R=$(_mkrepo)
|
|
137
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=0 bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
138
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other
|
|
139
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=$((13*3600)) bash "$SCRIPT" check 2>&1); rc=$?
|
|
140
|
+
_lane "⑤ STALE(13h) → 통과" 0 "$rc" "STALE"
|
|
141
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=$((11*3600)) bash "$SCRIPT" check 2>&1); rc=$?
|
|
142
|
+
_lane "⑤-b 컨트롤: 11h → 여전히 차단" 1 "$rc"
|
|
143
|
+
rm -rf "$R"
|
|
144
|
+
|
|
145
|
+
# ── ⑥ override → 통과 + **파일에 실제로 기록** (A-4 팬텀 주장 수리) ────────
|
|
146
|
+
R=$(_mkrepo)
|
|
147
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
148
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other
|
|
149
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_BRANCH_CLAIM_OK=1 bash "$SCRIPT" check 2>&1); rc=$?
|
|
150
|
+
_lane "⑥ override → 통과" 0 "$rc" "FH_BRANCH_CLAIM_OK"
|
|
151
|
+
_ok "⑥-b override 로그 파일이 실제로 생성·append 됨" \
|
|
152
|
+
"$([ -s "$R/.git/fh-branch-claim-overrides.log" ] && echo 1 || echo 0)"
|
|
153
|
+
rm -rf "$R"
|
|
154
|
+
|
|
155
|
+
# ── ⑦ detached / rebase 중 → 통과 (A-1 과차단 수리) ────────────────────────
|
|
156
|
+
R=$(_mkrepo)
|
|
157
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
158
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" checkout -q --detach
|
|
159
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
160
|
+
_lane "⑦ detached HEAD → 통과 (rebase/bisect 중 커밋을 막지 않는다)" 0 "$rc"
|
|
161
|
+
rm -rf "$R"
|
|
162
|
+
R=$(_mkrepo)
|
|
163
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
164
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other; mkdir -p "$R/.git/rebase-merge"
|
|
165
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
166
|
+
_lane "⑦-b rebase 진행 중 → 통과" 0 "$rc"
|
|
167
|
+
rm -rf "$R"
|
|
168
|
+
|
|
169
|
+
# ── ⑧ 테스트 노브는 FH_CLAIM_TEST=1 없이 무시된다 (A-3 무음 우회 차단) ─────
|
|
170
|
+
R=$(_mkrepo)
|
|
171
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
172
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other
|
|
173
|
+
# 테스트 모드 없이 노브만 주면 → 세션ID 폴백이 달라 기록을 못 찾아 통과할 수 있으므로,
|
|
174
|
+
# 실제 세션ID를 고정해 두고 STALE/NOW 노브가 먹지 않는지만 본다
|
|
175
|
+
export CLAUDE_CODE_SESSION_ID=me
|
|
176
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_STALE_HOURS=0 FH_CLAIM_NOW=99999999999 bash "$SCRIPT" check 2>&1); rc=$?
|
|
177
|
+
_lane "⑧ FH_CLAIM_TEST 없이 STALE/NOW 노브 → 무시되고 차단 유지" 1 "$rc"
|
|
178
|
+
unset CLAUDE_CODE_SESSION_ID
|
|
179
|
+
rm -rf "$R"
|
|
180
|
+
|
|
181
|
+
# ── ⑨ 워크트리는 기록이 분리된다 ───────────────────────────────────────────
|
|
182
|
+
R=$(_mkrepo)
|
|
183
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
184
|
+
WT="$R-wt"; git -C "$R" worktree add -q -b wtb "$WT" >/dev/null 2>&1
|
|
185
|
+
if [ -d "$WT" ]; then
|
|
186
|
+
LANE_OUT=$(cd "$WT" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
187
|
+
_lane "⑨ 워크트리는 기록 분리 → 통과" 0 "$rc"
|
|
188
|
+
git -C "$R" worktree remove --force "$WT" >/dev/null 2>&1
|
|
189
|
+
else echo " ⚠️ ⑨ 워크트리 생성 실패 — SKIPPED(통과로 세지 않음)"; fi
|
|
190
|
+
rm -rf "$R" "$WT"
|
|
191
|
+
|
|
192
|
+
# ── ⑩ 서브디렉터리에서도 같은 기록을 본다 (상대 .git 절대화) ───────────────
|
|
193
|
+
R=$(_mkrepo)
|
|
194
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
195
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other; mkdir -p "$R/deep/nested"
|
|
196
|
+
LANE_OUT=$(cd "$R/deep/nested" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
197
|
+
_lane "⑩ 서브디렉터리에서도 차단" 1 "$rc"
|
|
198
|
+
_ok "⑩-b 기록이 저장소 .git 에만 생성됨" \
|
|
199
|
+
"$([ -d "$R/.git/fh-claims" ] && [ ! -d "$R/deep/nested/.git" ] && echo 1 || echo 0)"
|
|
200
|
+
rm -rf "$R"
|
|
201
|
+
|
|
202
|
+
# ── ⑪ release / reap / show 도 앵커가 있다 (B-3 무앵커 경로 수리) ──────────
|
|
203
|
+
R=$(_mkrepo)
|
|
204
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
205
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" release >/dev/null 2>&1)
|
|
206
|
+
_ok "⑪ release 가 실제로 기록을 지운다" "$([ ! -f "$R/.git/fh-claims/me" ] && echo 1 || echo 0)"
|
|
207
|
+
_plant_peer "$R" x 999999
|
|
208
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" reap >/dev/null 2>&1)
|
|
209
|
+
_ok "⑪-b reap 이 죽은 세션 기록을 지운다" "$([ ! -f "$R/.git/fh-claims/session-PEER" ] && echo 1 || echo 0)"
|
|
210
|
+
_plant_peer "$R" zzz "$PEERPID"
|
|
211
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" show 2>&1); rc=$?
|
|
212
|
+
_lane "⑪-c show 가 HEAD·세션·peer 를 낸다" 0 "$rc" "HEAD" "살아있는 peer claim: 1" "zzz"
|
|
213
|
+
rm -rf "$R"
|
|
214
|
+
|
|
215
|
+
# ── ⑫ 복구 명령의 브랜치명이 shell-quote 된다 (cross-family B-6) ──────────
|
|
216
|
+
# git ref 는 공백은 못 쓰지만 `;` `$` 는 허용한다. 복붙 시 주입형 사고.
|
|
217
|
+
R=$(_mkrepo); B='a;touch$IFS/tmp/BCLAIM_PWNED'
|
|
218
|
+
git -C "$R" branch "$B" >/dev/null 2>&1 && git -C "$R" switch -q "$B"
|
|
219
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
220
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q main
|
|
221
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
222
|
+
EXPECT="git switch -- '${B}'"
|
|
223
|
+
_lane "⑫ metachar 브랜치명이 인용된 채 출력된다" 1 "$rc" "$EXPECT"
|
|
224
|
+
rm -rf "$R"
|
|
225
|
+
|
|
226
|
+
# ── ⑬ --if-absent: 없으면 만들고, 있으면 안 덮는다 (세션시작 훅이 부르는 형태) ──
|
|
227
|
+
# 훅 안에 같은 로직을 복제하면 앵커가 안 걸리므로 스크립트에 두고 여기서 잡는다.
|
|
228
|
+
R=$(_mkrepo)
|
|
229
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim --if-absent s 2>&1); rc=$?
|
|
230
|
+
_ok "⑬ --if-absent: 기록 없으면 생성" \
|
|
231
|
+
"$([ "$rc" = 0 ] && [ -f "$R/.git/fh-claims/me" ] && echo 1 || echo 0)"
|
|
232
|
+
git -C "$R" switch -q -c moved # 브랜치가 옮겨진 뒤 다시 불러도
|
|
233
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim --if-absent s 2>&1); rc=$?
|
|
234
|
+
kept=$(sed -n 's/^branch=//p' "$R/.git/fh-claims/me")
|
|
235
|
+
_lane "⑬-b --if-absent: 기록 있으면 덮지 않는다" 0 "$rc" "이미 있다"
|
|
236
|
+
_ok "⑬-c 기존 값 보존 (main 이어야 함 · 지금=$kept)" "$([ "$kept" = main ] && echo 1 || echo 0)"
|
|
237
|
+
# ★ 이게 중요한 이유: 덮으면 「내가 믿는 브랜치」가 매 세션시작마다 HEAD 로 갱신돼
|
|
238
|
+
# 게이트가 영원히 통과한다 — 자동화가 게이트를 무장해제하는 경로다
|
|
239
|
+
rm -rf "$R"
|
|
240
|
+
|
|
241
|
+
# ── ⑭ «peer 없음» 과 «못 가름» 을 나눈다 (2026-08-09 라이브 거짓 음성 N=2 회귀) ──
|
|
242
|
+
# _live_peers 는 claim 기록만 센다 → claim 안 한 세션을 구조적으로 못 본다.
|
|
243
|
+
# 그걸 0 으로 렌더한 게 라이브 결함이었다. 세 값이 **각각 다른 문장**을 내야 한다.
|
|
244
|
+
# ⚠️ 세 팔이 rc 는 전부 0(차단 안 함)이라 **rc 로는 구분이 안 된다** — 출력으로 잡는다.
|
|
245
|
+
# 이 레인이 rc 만 봤으면 세 팔 다 통과하고 아무것도 안 잡았을 것이다.
|
|
246
|
+
_sockdir() { local d; d=$(mktemp -d "${TMPDIR:-/tmp}/bcsock.XXXXXX"); echo "$d"; }
|
|
247
|
+
|
|
248
|
+
# ⑭ 소켓엔 살아있는 세션이 있는데 claim 은 0 → 「판정할 수 없다」
|
|
249
|
+
R=$(_mkrepo); S=$(_sockdir); : > "$S/${PEERPID}.sock"
|
|
250
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim a >/dev/null 2>&1)
|
|
251
|
+
git -C "$R" switch -q -c other
|
|
252
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_SOCK_DIR="$S" bash "$SCRIPT" check 2>&1); rc=$?
|
|
253
|
+
_lane "⑭ claim 0 인데 살아있는 세션 있음 → «판정할 수 없다»" 0 "$rc" \
|
|
254
|
+
"판정할 수 없다" "구분하지 못한다" "«안전» 이 아니라 «미판정»"
|
|
255
|
+
rm -rf "$R" "$S"
|
|
256
|
+
|
|
257
|
+
# ⑭-b 컨트롤: 소켓 디렉터리는 있는데 살아있는 세션이 없다 → 진짜 「peer 없음」
|
|
258
|
+
R=$(_mkrepo); S=$(_sockdir)
|
|
259
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim a >/dev/null 2>&1)
|
|
260
|
+
git -C "$R" switch -q -c other
|
|
261
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_SOCK_DIR="$S" bash "$SCRIPT" check 2>&1); rc=$?
|
|
262
|
+
_lane "⑭-b 컨트롤: 살아있는 세션 0 → «peer 없음» 이라고 단정한다" 0 "$rc" \
|
|
263
|
+
"살아있는 peer 세션이 없어" "이 머신에 다른 CC 세션도 없다"
|
|
264
|
+
_ok "⑭-b′ 그 팔엔 «판정할 수 없다» 가 없다(문장이 갈린다)" \
|
|
265
|
+
"$(printf '%s' "$LANE_OUT" | grep -q "판정할 수 없다" && echo 0 || echo 1)"
|
|
266
|
+
rm -rf "$R" "$S"
|
|
267
|
+
|
|
268
|
+
# ⑭-c 소켓 경로 자체가 없다 → 0 이 아니라 「교차 확인 불가」
|
|
269
|
+
# 런타임이 소켓 경로를 바꾸면 여기로 떨어진다. 그때 «peer 없음» 이라고 말하면 안 된다.
|
|
270
|
+
R=$(_mkrepo)
|
|
271
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim a >/dev/null 2>&1)
|
|
272
|
+
git -C "$R" switch -q -c other
|
|
273
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_SOCK_DIR="/nonexistent-$$" bash "$SCRIPT" check 2>&1); rc=$?
|
|
274
|
+
_lane "⑭-c 소켓 경로 부재 → «교차 확인 불가»(0 아님)" 0 "$rc" "교차 확인 불가"
|
|
275
|
+
_ok "⑭-c′ 그 팔은 «peer 없음» 이라고 단정하지 않는다" \
|
|
276
|
+
"$(printf '%s' "$LANE_OUT" | grep -q "이 머신에 다른 CC 세션도 없다" && echo 0 || echo 1)"
|
|
277
|
+
rm -rf "$R"
|
|
278
|
+
|
|
279
|
+
# ⑭-d 노브 게이팅: FH_CLAIM_TEST 없이 FH_CLAIM_SOCK_DIR 를 주면 **무시돼야** 한다.
|
|
280
|
+
# 안 그러면 프로덕션에서 소켓 경로를 갈아끼워 게이트를 무력화할 수 있다
|
|
281
|
+
# (이 파일이 이미 같은 결함으로 한 번 수리된 적이 있다 — 테스트 노브 6종 무음 우회).
|
|
282
|
+
R=$(_mkrepo); S=$(_sockdir); : > "$S/${PEERPID}.sock"
|
|
283
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim a >/dev/null 2>&1)
|
|
284
|
+
git -C "$R" switch -q -c other
|
|
285
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_SESSION=me FH_CLAIM_SOCK_DIR="$S" bash "$SCRIPT" check 2>&1); rc=$?
|
|
286
|
+
_ok "⑭-d 노브가 FH_CLAIM_TEST 없이는 무시된다(실경로를 본다)" \
|
|
287
|
+
"$(printf '%s' "$LANE_OUT" | grep -qF -- "$S" && echo 0 || echo 1)"
|
|
288
|
+
rm -rf "$R" "$S"
|
|
289
|
+
|
|
290
|
+
echo
|
|
291
|
+
echo "[branch-claim] PASS=$PASS FAIL=$FAIL"
|
|
292
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
@@ -33,7 +33,14 @@ if [ -z "$MATCHER" ]; then
|
|
|
33
33
|
echo " this lane cannot verify what it claims to. Update the lane WITH the subject."
|
|
34
34
|
exit 1
|
|
35
35
|
fi
|
|
36
|
+
# 주어의 _cat_logs 정의를 그대로 들어온다(재작성 금지 — 재작성하면 정규화가 갈린다)
|
|
37
|
+
CATLOGS_DEF=$(awk '/^_cat_logs\(\) \{/,/^\}/' "$CHECK")
|
|
38
|
+
if [ -z "$CATLOGS_DEF" ]; then
|
|
39
|
+
echo "FAIL subject 에 _cat_logs 가 없다 — 전환 설계(레거시 ∪ 디렉터리)가 사라졌거나 이름이 바뀌었다."
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
36
42
|
echo "── matcher located in the subject"
|
|
43
|
+
echo "── _cat_logs lifted from the subject ($(printf '%s' "$CATLOGS_DEF" | grep -c '') lines)"
|
|
37
44
|
|
|
38
45
|
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
39
46
|
D=2026-08-02
|
|
@@ -123,7 +130,10 @@ _eval_subject_assign() { # $1 = variable name; echoes what the SUBJECT computes
|
|
|
123
130
|
[ -n "$line" ] || { echo "NOLINE"; return; }
|
|
124
131
|
bash -c "set -uo pipefail
|
|
125
132
|
_int() { case \"\${1:-}\" in (''|*[!0-9]*) echo 0 ;; (*) echo \"\$1\" ;; esac; }
|
|
126
|
-
|
|
133
|
+
# ★ LOGGED 는 주어의 _cat_logs 에 의존한다. 이걸 안 들여오면 «함수 없음 → 빈 입력 → 0»
|
|
134
|
+
# 으로 **틀린 이유로 통과**한다 — 이 파일이 경고하는 장식 앵커 그 자체다.
|
|
135
|
+
$CATLOGS_DEF
|
|
136
|
+
TODAY=NOSUCHDATE; TALLY='$CHECK'; LOG='$CHECK'; LOGDIR='$CHECK.nodir'
|
|
127
137
|
$line
|
|
128
138
|
printf %s \"\$$var\"" 2>/dev/null
|
|
129
139
|
}
|
|
@@ -133,6 +143,30 @@ for _v in DISPATCHED LOGGED; do
|
|
|
133
143
|
( set -uo pipefail; [ "${_got:-x}" -eq 0 ] ) 2>/dev/null ; chk $? "subject's \$$_v survives \`[ -eq 0 ]\` (the disarm threw here)"
|
|
134
144
|
done
|
|
135
145
|
|
|
146
|
+
echo ""
|
|
147
|
+
echo "── 전환 설계 레인: 레거시 단일 파일 ∪ 세션별 디렉터리 ──"
|
|
148
|
+
# 주어의 _cat_logs + LOGGED 를 그대로 들어와 4가지 상태에서 센다.
|
|
149
|
+
LOGGED_LINE=$(grep -E "^LOGGED=" "$CHECK" | head -1)
|
|
150
|
+
_count_in() { # $1=LOG 경로(없으면 빈문자) $2=LOGDIR 경로(없으면 빈문자)
|
|
151
|
+
bash -c "set -uo pipefail
|
|
152
|
+
_int() { case \"\${1:-}\" in (''|*[!0-9]*) echo 0 ;; (*) echo \"\$1\" ;; esac; }
|
|
153
|
+
$CATLOGS_DEF
|
|
154
|
+
TODAY='$D'; LOG='$1'; LOGDIR='$2'
|
|
155
|
+
$LOGGED_LINE
|
|
156
|
+
printf %s \"\$LOGGED\"" 2>/dev/null
|
|
157
|
+
}
|
|
158
|
+
LT="$T/split"; mkdir -p "$LT/dir"
|
|
159
|
+
printf -- "- date: %s\n" "$D" > "$LT/legacy.yaml" # 1건
|
|
160
|
+
printf -- "- date: %s\n- date: '%s'\n" "$D" "$D" > "$LT/dir/a.yaml" # 2건 (두 표기 다)
|
|
161
|
+
printf -- "- date: %s\n" "$D" > "$LT/dir/b.yaml" # 1건
|
|
162
|
+
[ "$(_count_in "$LT/legacy.yaml" "$LT/nosuchdir")" = 1 ] ; chk $? "레거시 파일만 → 1"
|
|
163
|
+
[ "$(_count_in "$LT/nofile.yaml" "$LT/dir")" = 3 ] ; chk $? "디렉터리만 → 3 (파일 2개 합산)"
|
|
164
|
+
[ "$(_count_in "$LT/legacy.yaml" "$LT/dir")" = 4 ] ; chk $? "둘 다 → 4 (합집합)"
|
|
165
|
+
[ "$(_count_in "$LT/nofile.yaml" "$LT/nosuchdir")" = 0 ] ; chk $? "둘 다 없음 → 0 (부재는 오류가 아니다)"
|
|
166
|
+
# ★ 컨트롤: 다중 파일에 grep -c 를 직접 걸면 깨진다는 실측을 레인으로 고정
|
|
167
|
+
_naive=$(grep -cE "^- date: *'?$D'?" "$LT/dir"/*.yaml 2>/dev/null | tr -d ' \n')
|
|
168
|
+
[ "$_naive" != 3 ] ; chk $? "컨트롤: naive grep -c 다중파일은 3을 못 낸다 (실제='$_naive' — 경로:개수 형태)"
|
|
169
|
+
|
|
136
170
|
echo ""
|
|
137
171
|
if [ "$FAILED" -ne 0 ]; then echo "DISPATCH-LOG LANES: FAIL"; exit 1; fi
|
|
138
172
|
echo "DISPATCH-LOG LANES: PASS ($PASS/$PASS)"
|