@chrono-meta/fh-gate 1.4.89 → 1.4.91
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/judgment_circuits.txt +14 -0
- package/.claude/rules/fh_4axis_gate.md +7 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +25 -0
- package/CLAUDE.md +202 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
- package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
- package/package.json +23 -2
- 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/auto-decorrelation/SKILL.md +56 -8
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
- package/scripts/branch_claim.sh +266 -0
- package/scripts/chamber_run.sh +64 -2
- package/scripts/chamber_witness.sh +439 -0
- package/scripts/compaction_probe.sh +456 -0
- package/scripts/digest_landing_check.sh +385 -0
- package/scripts/directional_diff_gate.sh +459 -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/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +106 -0
- package/scripts/session_close_check.sh +134 -1
- package/scripts/test_branch_claim_lanes.sh +231 -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/scripts/test_marker_crossfamily_lanes.sh +132 -0
- package/scripts/test_marker_floor_lanes.sh +9 -8
- package/scripts/test_relay_channel_lanes.sh +583 -0
- package/scripts/test_reviewer_capability_conformance.sh +173 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/utterance_landing_check.sh +209 -0
- package/templates/.git-hooks/pre-commit +297 -13
- package/templates/settings.Compaction.snippet.json +56 -0
- package/templates/settings.FieldCanon.snippet.json +51 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# branch_claim.sh — 공유 체크아웃에서 «내가 믿는 브랜치»와 «실제 HEAD»가 갈린 채 커밋되는 것을 막는다.
|
|
3
|
+
#
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# WHY (2026-08-09 실측, FH 병렬 세션 2개, 한 체크아웃)
|
|
6
|
+
# `.git/HEAD` 는 워킹트리당 하나다. 세션 A 의 `git switch` 는 세션 B 의 발밑도 옮긴다.
|
|
7
|
+
# 17:35 B 가 브랜치를 땄다 (B 는 «작업 시작할 때 브랜치부터 따기»를 이미 했다)
|
|
8
|
+
# 17:54 A 가 브랜치를 따며 트리 이동
|
|
9
|
+
# 18:10 B 가 자기 브랜치인 줄 알고 **A 의 브랜치에 커밋**
|
|
10
|
+
# → 브랜치를 언제 따느냐는 무관하다. **HEAD 가 트리당 하나**인 게 원인이다.
|
|
11
|
+
#
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
# 판정 키 = «세션» 이지 «트리» 가 아니다 ★ 이게 이 스크립트의 핵심이고, 초판은 틀렸다
|
|
14
|
+
# 초판은 트리 단위 claim 하나를 두고 그것과 HEAD 를 비교했다. 적대검증이 실행으로 반증:
|
|
15
|
+
# **양쪽 세션이 다 프로토콜을 지키면 정확히 그 사고를 못 잡는다** — A 가 switch 후 claim 하면
|
|
16
|
+
# claim==HEAD 가 되어 B 의 check 가 통과한다. 채택률이 오를수록 무력해지는 게이트였다.
|
|
17
|
+
# 그래서 claim 을 **세션마다** 따로 둔다: `.git/fh-claims/<session_id>`.
|
|
18
|
+
# 비교는 **내 claim vs HEAD** 만 한다 — 남의 claim 은 내 판정에 안 들어오므로
|
|
19
|
+
# peer 로 인한 과차단이 구조적으로 없다.
|
|
20
|
+
#
|
|
21
|
+
# 세션 정체성은 배관이 필요 없다. 실측(2026-08-09): git 훅은 Claude Code 가 주입한
|
|
22
|
+
# `CLAUDE_CODE_SESSION_ID` · `CLAUDE_PID` 를 그대로 상속한다.
|
|
23
|
+
#
|
|
24
|
+
# WHAT THIS DOES / DOESN'T
|
|
25
|
+
# 막는다 : «내가 마지막으로 claim 한 브랜치» ≠ HEAD 인 채로 커밋
|
|
26
|
+
# 안 막는다 : `git switch` 자체 (git 에 switch 훅이 없다 — 막을 자리가 없다)
|
|
27
|
+
# 🟥 근본 해결이 아니다. 이건 **완화책**이다. 공유 체크아웃은 미커밋 워킹트리 혼입/유실도
|
|
28
|
+
# 낳고(그건 커밋 게이트로 안 닫힌다), 근본 처방은 **세션당 worktree** 다.
|
|
29
|
+
# (worktree 를 쓰면 이 claim 도 자동으로 트리별로 갈린다 — `--git-dir` 이 갈리므로.
|
|
30
|
+
# 단 FH 자산 커밋은 worktree 에서 4축 증거가 구조적으로 부재하다는 별개 문제가 있다.)
|
|
31
|
+
#
|
|
32
|
+
# 🟥 TOCTOU 잔여 (cross-family 지적, 미해결): `check` 와 git 이 실제로 ref 를 갱신하는 순간
|
|
33
|
+
# 사이에 창이 있다. 그 사이 peer 가 switch 하면 이 게이트를 통과한 채 사고가 난다.
|
|
34
|
+
# 창은 «35분»에서 «훅 실행시간»으로 줄지만 0 이 아니다. 정통 처방은 pre-commit 이 아니라
|
|
35
|
+
# **`reference-transaction` 훅** — 갱신 대상 ref 를 stdin 으로 받으므로 원자적으로 판정된다.
|
|
36
|
+
# 지금은 짓지 않았다(별개 훅 = 별개 변경). 완화로 훅 말미에 재검사를 한 번 더 돌린다.
|
|
37
|
+
#
|
|
38
|
+
# DEGRADE DIRECTION
|
|
39
|
+
# claim 없음 / 손상 / detached·rebase·bisect 중 → **통과**.
|
|
40
|
+
# 근거는 **«과차단은 override 를 습관화시켜 게이트를 죽인다»** 하나다(FH 실측 반복 기록).
|
|
41
|
+
# ⚠️ «커밋은 가역이니까» 를 근거로 쓰지 않는다 — 이 사고의 실제 복구는 공유 브랜치
|
|
42
|
+
# history rewrite 로 갔고, 그건 Surface-Class Degrade Invariant 가 fail-closed 로 두는 표면이다.
|
|
43
|
+
# 근거를 하나로 좁히지 않으면 나중에 «가역이니 여기도 완화하자» 로 잘못 일반화된다.
|
|
44
|
+
#
|
|
45
|
+
# 동시성 증거 없음(살아있는 peer 세션 claim 0개) → **경고만, 차단 안 함.**
|
|
46
|
+
# 1인 작업자에겐 이 게이트의 편익이 0이고 비용만 있다.
|
|
47
|
+
# 불일치 + 살아있는 peer 존재 → **차단.** override = FH_BRANCH_CLAIM_OK=1 (파일에 기록된다).
|
|
48
|
+
#
|
|
49
|
+
# USAGE
|
|
50
|
+
# bash scripts/branch_claim.sh claim [<label>] # 현재 HEAD 를 «내 브랜치»로 기록
|
|
51
|
+
# bash scripts/branch_claim.sh claim --if-absent [<label>] # 없을 때만 (세션시작 훅용)
|
|
52
|
+
# bash scripts/branch_claim.sh check # pre-commit 이 부르는 것 (0=통과 1=차단)
|
|
53
|
+
# bash scripts/branch_claim.sh show | release | reap
|
|
54
|
+
#
|
|
55
|
+
# ENV (운영)
|
|
56
|
+
# FH_BRANCH_CLAIM_OK=1 차단 1회 우회 — override 로그에 append 된다
|
|
57
|
+
# ENV (테스트 전용 — FH_CLAIM_TEST=1 없이는 전부 무시된다)
|
|
58
|
+
# FH_CLAIM_TEST=1 FH_CLAIM_DIR FH_CLAIM_NOW FH_CLAIM_STALE_HOURS FH_CLAIM_SESSION
|
|
59
|
+
# ↑ 게이팅하는 이유: 초판은 이 노브들이 프로덕션에서도 먹어서 **정직한 override 보다
|
|
60
|
+
# 조용한 우회가 더 쉬웠다**(FH_CLAIM_FILE=/dev/null → rc=0, 화면에 아무 표시 없음).
|
|
61
|
+
#
|
|
62
|
+
# bash 3.2(macOS) 호환: 연관배열·${v^^}·mapfile 안 씀.
|
|
63
|
+
# `stat` 안 씀 — epoch 을 파일 안에 적는다(BSD/GNU `stat -f` 분기 회피).
|
|
64
|
+
|
|
65
|
+
set -uo pipefail
|
|
66
|
+
|
|
67
|
+
_test_mode() { [ "${FH_CLAIM_TEST:-}" = "1" ]; }
|
|
68
|
+
_now() { if _test_mode && [ -n "${FH_CLAIM_NOW:-}" ]; then echo "$FH_CLAIM_NOW"; else date +%s; fi; }
|
|
69
|
+
_stale_hours() { if _test_mode && [ -n "${FH_CLAIM_STALE_HOURS:-}" ]; then echo "$FH_CLAIM_STALE_HOURS"; else echo 12; fi; }
|
|
70
|
+
|
|
71
|
+
# 세션 정체성. Claude Code 가 주입한다(실측). 없으면 셸 PID 로 폴백 — 그 경우
|
|
72
|
+
# «세션»은 사실상 «이 프로세스 트리»가 되고, 게이트는 여전히 동작하되 범위가 좁아진다.
|
|
73
|
+
_session_id() {
|
|
74
|
+
if _test_mode && [ -n "${FH_CLAIM_SESSION:-}" ]; then echo "$FH_CLAIM_SESSION"; return; fi
|
|
75
|
+
if [ -n "${CLAUDE_CODE_SESSION_ID:-}" ]; then echo "$CLAUDE_CODE_SESSION_ID"; return; fi
|
|
76
|
+
echo "pid-${PPID:-$$}"
|
|
77
|
+
}
|
|
78
|
+
_session_pid() { echo "${CLAUDE_PID:-${PPID:-$$}}"; }
|
|
79
|
+
|
|
80
|
+
# 절대경로 강제: 본 트리에서 `--git-dir` 은 상대 ".git" 이라, 서브디렉터리에서 실행하면
|
|
81
|
+
# 엉뚱한 곳에 claim 을 만들어 «점유 없음»으로 조용히 통과한다 = 게이트 무력화.
|
|
82
|
+
_git_dir() {
|
|
83
|
+
local d
|
|
84
|
+
d=$(git rev-parse --absolute-git-dir 2>/dev/null) && [ -n "$d" ] && { echo "$d"; return 0; }
|
|
85
|
+
local top; top=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
86
|
+
d=$(git rev-parse --git-dir 2>/dev/null) || return 1
|
|
87
|
+
case "$d" in /*) echo "$d" ;; *) echo "$top/$d" ;; esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_claim_dir() {
|
|
91
|
+
if _test_mode && [ -n "${FH_CLAIM_DIR:-}" ]; then echo "$FH_CLAIM_DIR"; return 0; fi
|
|
92
|
+
local gd; gd=$(_git_dir) || return 1
|
|
93
|
+
echo "$gd/fh-claims"
|
|
94
|
+
}
|
|
95
|
+
_my_claim() { local d; d=$(_claim_dir) || return 1; echo "$d/$(_session_id)"; }
|
|
96
|
+
|
|
97
|
+
_head_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
|
|
98
|
+
_field() { sed -n "s/^$2=//p" "$1" 2>/dev/null | head -1; }
|
|
99
|
+
|
|
100
|
+
# 출력하는 복구 명령에 브랜치명을 그대로 박으면 안 된다: git ref 는 공백은 못 쓰지만
|
|
101
|
+
# `;` `$` `(` `)` `&` `|` 백틱은 **허용**한다. 복붙하면 주입형 사고가 난다(cross-family 지적).
|
|
102
|
+
# bash 3.2 라 printf %q 대신 single-quote escape 로 감싼다.
|
|
103
|
+
_shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
|
104
|
+
|
|
105
|
+
# 브랜치가 «지금 어디에 얹히는가»의 질문이 성립하지 않는 상태들.
|
|
106
|
+
# 초판은 detached 를 차단해서 rebase --edit 중 커밋을 막았고, 제시한 세 선택지 중
|
|
107
|
+
# 하나는 실행 불가(claim 이 detached 를 거부), 하나는 rebase 를 깨는 switch 였다.
|
|
108
|
+
# 남는 게 override 뿐이면 그건 override 훈련이다.
|
|
109
|
+
_inapplicable_state() {
|
|
110
|
+
local b gd; b=$(_head_branch); gd=$(_git_dir) || return 1
|
|
111
|
+
[ "$b" = "HEAD" ] && return 0
|
|
112
|
+
for m in rebase-merge rebase-apply BISECT_LOG MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD; do
|
|
113
|
+
[ -e "$gd/$m" ] && return 0
|
|
114
|
+
done
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# 살아있는 **다른** 세션의 claim 개수 (동시성 증거). 죽은 세션 claim 은 안 센다.
|
|
119
|
+
_live_peers() {
|
|
120
|
+
local d me n=0 f sid pid
|
|
121
|
+
d=$(_claim_dir) || { echo 0; return; }
|
|
122
|
+
[ -d "$d" ] || { echo 0; return; }
|
|
123
|
+
me=$(_session_id)
|
|
124
|
+
for f in "$d"/*; do
|
|
125
|
+
[ -f "$f" ] || continue
|
|
126
|
+
sid=$(basename "$f"); [ "$sid" = "$me" ] && continue
|
|
127
|
+
pid=$(_field "$f" pid)
|
|
128
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then n=$((n+1)); fi
|
|
129
|
+
done
|
|
130
|
+
echo "$n"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_override_log() { local gd; gd=$(_git_dir) || return 1; echo "$gd/fh-branch-claim-overrides.log"; }
|
|
134
|
+
|
|
135
|
+
cmd_claim() {
|
|
136
|
+
# --if-absent: 이 세션의 기록이 이미 있으면 아무것도 하지 않는다(덮어쓰기 금지).
|
|
137
|
+
# 세션시작 훅이 부르는 형태 — 훅 안에 같은 로직을 복제하면 앵커를 못 걸어서 여기에 둔다.
|
|
138
|
+
local ifabsent=0
|
|
139
|
+
if [ "${1:-}" = "--if-absent" ]; then ifabsent=1; shift; fi
|
|
140
|
+
local label="${1:-${USER:-unknown}}" d f b now
|
|
141
|
+
d=$(_claim_dir) || { echo "branch-claim: git 저장소가 아니다 — 아무것도 안 한다"; return 0; }
|
|
142
|
+
b=$(_head_branch)
|
|
143
|
+
if [ -z "$b" ] || [ "$b" = "HEAD" ]; then
|
|
144
|
+
echo "branch-claim: HEAD 가 detached 라 기록하지 않는다 (브랜치 위에서 실행해라)" >&2
|
|
145
|
+
return 2
|
|
146
|
+
fi
|
|
147
|
+
mkdir -p "$d" || return 1
|
|
148
|
+
f=$(_my_claim); now=$(_now)
|
|
149
|
+
if [ "$ifabsent" = "1" ] && [ -f "$f" ]; then
|
|
150
|
+
echo "branch-claim: 이 세션의 기록이 이미 있다 — 그대로 둔다 ($(_field "$f" branch))"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
# 원자적 교체: 같은 디렉터리 내 rename. in-place truncate 는 동시 read 에서 필드가 찢어진다
|
|
154
|
+
# (실측: 동시 claim 300회 중 9회 owner 필드 파손).
|
|
155
|
+
printf 'branch=%s\nlabel=%s\npid=%s\nat_epoch=%s\n' "$b" "$label" "$(_session_pid)" "$now" \
|
|
156
|
+
> "$f.tmp.$$" && mv -f "$f.tmp.$$" "$f" || return 1
|
|
157
|
+
echo "✅ branch-claim: 이 세션의 브랜치를 '$b' 로 기록했다 (label=$label)"
|
|
158
|
+
local peers; peers=$(_live_peers)
|
|
159
|
+
[ "$peers" -gt 0 ] && echo " 살아있는 peer 세션 claim: $peers 개 — 게이트 활성"
|
|
160
|
+
return 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
cmd_release() {
|
|
164
|
+
local f; f=$(_my_claim) 2>/dev/null || return 0
|
|
165
|
+
if [ -f "$f" ]; then rm -f "$f"; echo "branch-claim: 이 세션의 기록을 지웠다"; else echo "branch-claim: 기록 없음"; fi
|
|
166
|
+
return 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# 죽은 세션의 claim 정리
|
|
170
|
+
cmd_reap() {
|
|
171
|
+
local d f sid pid n=0
|
|
172
|
+
d=$(_claim_dir) || return 0; [ -d "$d" ] || { echo "branch-claim: 기록 디렉터리 없음"; return 0; }
|
|
173
|
+
for f in "$d"/*; do
|
|
174
|
+
[ -f "$f" ] || continue
|
|
175
|
+
pid=$(_field "$f" pid)
|
|
176
|
+
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then rm -f "$f"; n=$((n+1)); fi
|
|
177
|
+
done
|
|
178
|
+
echo "branch-claim: 죽은 세션 기록 $n 개 정리"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
cmd_show() {
|
|
182
|
+
local d f me; d=$(_claim_dir) || { echo "(git 저장소 아님)"; return 0; }
|
|
183
|
+
me=$(_session_id)
|
|
184
|
+
echo "HEAD $(_head_branch)"
|
|
185
|
+
echo "세션ID $me"
|
|
186
|
+
echo "살아있는 peer claim: $(_live_peers)"
|
|
187
|
+
if [ -d "$d" ]; then
|
|
188
|
+
for f in "$d"/*; do
|
|
189
|
+
[ -f "$f" ] || continue
|
|
190
|
+
local sid pid alive; sid=$(basename "$f"); pid=$(_field "$f" pid)
|
|
191
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then alive=live; else alive=dead; fi
|
|
192
|
+
printf ' %s %s branch=%s label=%s (%s)\n' \
|
|
193
|
+
"$([ "$sid" = "$me" ] && echo '→' || echo ' ')" "$sid" \
|
|
194
|
+
"$(_field "$f" branch)" "$(_field "$f" label)" "$alive"
|
|
195
|
+
done
|
|
196
|
+
else echo " (기록 없음)"; fi
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# 0 = 통과, 1 = 차단
|
|
200
|
+
cmd_check() {
|
|
201
|
+
local f b cb cl ca now age lim peers
|
|
202
|
+
f=$(_my_claim) 2>/dev/null || return 0
|
|
203
|
+
b=$(_head_branch); [ -n "$b" ] || return 0
|
|
204
|
+
if _inapplicable_state; then return 0; fi # detached·rebase·bisect·merge 중 → 질문이 성립 안 함
|
|
205
|
+
[ -f "$f" ] || return 0 # 이 세션의 기록 없음 → 의견 없음 (부재 ≠ 위반)
|
|
206
|
+
|
|
207
|
+
cb=$(_field "$f" branch); cl=$(_field "$f" label); ca=$(_field "$f" at_epoch)
|
|
208
|
+
if [ -z "$cb" ]; then
|
|
209
|
+
echo " ⚠️ branch-claim: 내 기록을 파싱 못 했다 — 통과시킨다 (fail-open, 과차단 회피)"
|
|
210
|
+
return 0
|
|
211
|
+
fi
|
|
212
|
+
if [ "$cb" = "$b" ]; then
|
|
213
|
+
# 일치 → 갱신해서 나이가 «재임 기간»이 아니라 «유휴 시간»을 재게 한다.
|
|
214
|
+
# (초판은 갱신을 안 해서, 오래 붙어 있는 세션일수록 STALE 로 무방비가 됐다.)
|
|
215
|
+
now=$(_now)
|
|
216
|
+
printf 'branch=%s\nlabel=%s\npid=%s\nat_epoch=%s\n' "$cb" "$cl" "$(_session_pid)" "$now" \
|
|
217
|
+
> "$f.tmp.$$" 2>/dev/null && mv -f "$f.tmp.$$" "$f" 2>/dev/null
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
now=$(_now); lim=$(_stale_hours)
|
|
222
|
+
if [ -n "$ca" ] && [ "$ca" -eq "$ca" ] 2>/dev/null; then
|
|
223
|
+
age=$(( (now - ca) / 3600 ))
|
|
224
|
+
if [ "$age" -ge "$lim" ]; then
|
|
225
|
+
echo " ⚠️ branch-claim: 내 기록이 STALE(${age}h ≥ ${lim}h) — 차단하지 않는다."
|
|
226
|
+
echo " 기록='$cb' · HEAD='$b' → 맞으면: bash scripts/branch_claim.sh claim"
|
|
227
|
+
return 0
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
peers=$(_live_peers)
|
|
232
|
+
if [ "$peers" -eq 0 ]; then
|
|
233
|
+
echo " ⚠️ branch-claim: 기록('$cb')과 HEAD('$b')가 다르다 — 살아있는 peer 세션이 없어 차단하지 않는다."
|
|
234
|
+
echo " 의도한 전환이면: bash scripts/branch_claim.sh claim"
|
|
235
|
+
return 0
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
if [ "${FH_BRANCH_CLAIM_OK:-}" = "1" ]; then
|
|
239
|
+
local lg; lg=$(_override_log 2>/dev/null)
|
|
240
|
+
[ -n "$lg" ] && printf '%s override claim=%s head=%s session=%s peers=%s\n' \
|
|
241
|
+
"$(_now)" "$cb" "$b" "$(_session_id)" "$peers" >> "$lg" 2>/dev/null
|
|
242
|
+
echo " ⚠️ branch-claim: 불일치인데 FH_BRANCH_CLAIM_OK=1 로 우회한다."
|
|
243
|
+
echo " 기록='$cb' · HEAD='$b' · 로그: ${lg:-<기록 실패>}"
|
|
244
|
+
return 0
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
echo " ❌ branch-claim: 이 세션은 '$cb' 로 기록돼 있는데 HEAD 는 '$b' 다."
|
|
248
|
+
echo " 살아있는 peer 세션 $peers 개가 이 체크아웃을 같이 쓴다 — 그 중 하나가 브랜치를 옮겼다."
|
|
249
|
+
echo " ⚠️ 지금 커밋하면 '$b' 브랜치에 얹힌다 — 2026-08-09 에 실제로 난 사고다."
|
|
250
|
+
echo
|
|
251
|
+
echo " ▸ '$b' 가 맞다 : bash scripts/branch_claim.sh claim"
|
|
252
|
+
echo " ▸ '$cb' 로 돌아간다 : git switch -- $(_shq "$cb")"
|
|
253
|
+
echo " ⚠️ 돌아가면 지금 '$b' 를 쓰는 세션의 발밑을 뺀다. 먼저 한 줄 알려라."
|
|
254
|
+
echo " ▸ 근본 해결 : 세션당 worktree (이 게이트는 완화책이다)"
|
|
255
|
+
echo " ▸ 알고도 강행 : FH_BRANCH_CLAIM_OK=1 git commit …"
|
|
256
|
+
return 1
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "${1:-check}" in
|
|
260
|
+
claim) shift; cmd_claim "$@" ;;
|
|
261
|
+
release) cmd_release ;;
|
|
262
|
+
reap) cmd_reap ;;
|
|
263
|
+
show) cmd_show ;;
|
|
264
|
+
check) cmd_check ;;
|
|
265
|
+
*) echo "usage: $0 {claim [label]|check|show|release|reap}" >&2; exit 2 ;;
|
|
266
|
+
esac
|
package/scripts/chamber_run.sh
CHANGED
|
@@ -43,6 +43,25 @@ TODAY="$(date +%Y-%m-%d)"
|
|
|
43
43
|
_status() { [ -f "$STATUS_F" ] && cat "$STATUS_F" || echo "step-0"; }
|
|
44
44
|
_stamp() { printf '%s\n' "$1" > "$STATUS_F"; }
|
|
45
45
|
|
|
46
|
+
# ── 순서 증인 (ship_readiness_gate §② P1) ────────────────────────────────────
|
|
47
|
+
# 워크스페이스는 `tracks/**` 라 gitignored 다. 그래서 게이트 아티팩트의 **해시만**
|
|
48
|
+
# 추적되는 원장에 적어 커밋한다 — **본문** 유출 0(단 slug·파일명·시각·해시는 남는다),
|
|
49
|
+
# 순서는 로컬 커밋 그래프가 증언(되쓰기 방지는 push 후 서버 ruleset 이 준다).
|
|
50
|
+
# 기록 실패는 **런을 막지 않는다**(KILL 런까지 세우는 건 과차단이고, 과차단은
|
|
51
|
+
# override 를 습관화시킨다). 대신 증인 부재를 **말한다** — 조용히 넘어가지 않는다.
|
|
52
|
+
_WITNESS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/chamber_witness.sh"
|
|
53
|
+
_witness_record() { # $1 = artifact path
|
|
54
|
+
if [ ! -x "$_WITNESS" ] && [ ! -f "$_WITNESS" ]; then
|
|
55
|
+
echo " ⚠️ 순서 증인 미기록 — chamber_witness.sh 부재 (이 런은 ② 승격 근거로 못 쓴다)"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
if bash "$_WITNESS" record "$SLUG" "$1" >/dev/null 2>&1; then
|
|
59
|
+
echo " ↳ 순서 증인: $(basename "$1") 해시 기록됨 — **커밋해야** 증인이 된다"
|
|
60
|
+
else
|
|
61
|
+
echo " ⚠️ 순서 증인 기록 실패: $(basename "$1") (증인 없음 — 미측정이지 0이 아니다)"
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
if [ "$CMD" = "status" ]; then
|
|
47
66
|
echo "chamber run '$SLUG' → STATUS: $(_status)"
|
|
48
67
|
[ -d "$WS" ] && ls -1 "$WS" 2>/dev/null | sed 's/^/ /'
|
|
@@ -85,9 +104,18 @@ fi
|
|
|
85
104
|
# gate (a) is "artifact exists WITH real content", not just "placeholder removed" (Axis-2 LOW-6): require
|
|
86
105
|
# at least one non-empty numbered success condition so a gutted INTENT.md doesn't pass.
|
|
87
106
|
if ! awk '/^## Success conditions/{f=1;next} /^## /{f=0} f && /^[0-9]+\.[[:space:]]*[^[:space:]]/{print; exit}' "$WS/INTENT.md" | grep -q .; then
|
|
88
|
-
|
|
107
|
+
# 계약을 **에러 메시지가 가르친다.** 이 형식은 CHAMBER_RUN_SKELETON.md 에 한 글자도 없어서,
|
|
108
|
+
# 스켈레톤만 읽고 쓴 INTENT 가 반려됐다 — gate-locality(계약이 게이트 구현에만 산다).
|
|
109
|
+
# 같은 런에서 step 6 도 같은 이유로 막혔다(N=2). 문서는 드리프트하지만 에러는 코드와 같이 산다.
|
|
110
|
+
echo " ⛔ step 2 BLOCKED: $WS/INTENT.md has no filled success condition."
|
|
111
|
+
echo " Expected — an English heading, then NUMBERED lines with content:"
|
|
112
|
+
echo " ## Success conditions"
|
|
113
|
+
echo " 1. <condition> · [measured|mandatory-pass|judged]"
|
|
114
|
+
echo " 2. ..."
|
|
115
|
+
echo " A table alone does NOT satisfy this check. Re-run after adding it."; exit 1
|
|
89
116
|
fi
|
|
90
117
|
_stamp "step-2-done"; echo " ✓ step 2: INTENT.md present"
|
|
118
|
+
_witness_record "$WS/INTENT.md"
|
|
91
119
|
|
|
92
120
|
# STEP 3 — budget-entry gate (G2). Cannot invoke goal-quench from bash; MECHANICALLY require a recorded
|
|
93
121
|
# estimate before any (expensive) simulation runs. No ESTIMATE = no run — that IS the entry cap.
|
|
@@ -106,6 +134,7 @@ if grep -qE '^ESTIMATE:[[:space:]]*<' "$WS/BUDGET.md" || ! grep -qE '^ESTIMATE:[
|
|
|
106
134
|
echo " ⛔ step 3 BLOCKED: $WS/BUDGET.md ESTIMATE is empty/placeholder (G2 entry cap), then re-run."; exit 1
|
|
107
135
|
fi
|
|
108
136
|
_stamp "step-3-done"; echo " ✓ step 3: budget ESTIMATE recorded (entry cap satisfied)"
|
|
137
|
+
_witness_record "$WS/BUDGET.md"
|
|
109
138
|
|
|
110
139
|
# STEP 4 — persona simulation (G1-forcing gate). Dispatch is human/Claude-side (bash can't spawn Agents);
|
|
111
140
|
# gate on the ≥3 blind persona artifact. Isolation is the mechanism — the runner enforces the artifact, not the spawn.
|
|
@@ -126,6 +155,7 @@ if [ "$NPERS" -lt 3 ]; then
|
|
|
126
155
|
echo " ⛔ step 4 BLOCKED: SIM_NOTES.md has $NPERS/3 DISTINCT blind persona sections (need all of beginner + main-player + challenger)."; exit 1
|
|
127
156
|
fi
|
|
128
157
|
_stamp "step-4-done"; echo " ✓ step 4: $NPERS blind persona sections present (isolation-gate satisfied)"
|
|
158
|
+
_witness_record "$WS/SIM_NOTES.md"
|
|
129
159
|
|
|
130
160
|
# STEP 5 — Emission Gate. Require a VERDICT: EMIT | PARTIAL-EMIT | KILL.
|
|
131
161
|
if [ ! -f "$WS/EMISSION_VERDICT.md" ]; then
|
|
@@ -149,10 +179,36 @@ if [ -z "$VERDICT" ]; then
|
|
|
149
179
|
echo " ⛔ step 5 BLOCKED: no VERDICT (EMIT|PARTIAL-EMIT|KILL) found in $WS/EMISSION_VERDICT.md, re-run."; exit 1
|
|
150
180
|
fi
|
|
151
181
|
_stamp "step-5-done"; echo " ✓ step 5: Emission Gate verdict = $VERDICT"
|
|
182
|
+
_witness_record "$WS/EMISSION_VERDICT.md"
|
|
183
|
+
# 순서 판정을 **여기서 인쇄한다.** verdict 가 나온 자리가 "게이트가 결과를 알기 전에
|
|
184
|
+
# 돌았는가" 를 물어야 하는 유일한 자리다. 차단하지 않는 이유: KILL 런까지 막으면
|
|
185
|
+
# 과차단이고, 과차단은 override 를 습관화시켜 게이트 전체를 무장해제한다.
|
|
186
|
+
# 대신 **EMIT 일 때는 승격 사용 가부를 명시적으로 말한다** — 침묵이 곧 승인으로
|
|
187
|
+
# 읽히는 것이 이 채널이 존재하는 이유다.
|
|
188
|
+
if [ -f "$_WITNESS" ]; then
|
|
189
|
+
echo " ── 순서 증인 판정 ──"
|
|
190
|
+
bash "$_WITNESS" verify "$SLUG" "$WS" 2>&1 | sed 's/^/ /'
|
|
191
|
+
_wrc="${PIPESTATUS[0]}"
|
|
192
|
+
if [ "$VERDICT" = "EMIT" ] || [ "$VERDICT" = "PARTIAL-EMIT" ]; then
|
|
193
|
+
if [ "${_wrc:-9}" = "0" ]; then
|
|
194
|
+
echo " ✅ 이 EMIT 은 순서 증인을 가진다 — identity ② 승격 근거로 사용 가능"
|
|
195
|
+
else
|
|
196
|
+
echo " 🟥 이 EMIT 에는 순서 증인이 없다(rc=${_wrc:-?}) — identity ② 승격 근거로 쓰지 마라."
|
|
197
|
+
echo " (원장 해시를 커밋한 뒤 재실행하면 증인이 성립한다)"
|
|
198
|
+
# ★ 출력만 하고 exit 0 으로 끝내면, stdout 을 안 읽는 호출자에게는 증인 없는 EMIT 도
|
|
199
|
+
# **성공**이다(cross-family F8). EMIT 은 승격 주장이므로 기계적으로 구분되어야 한다.
|
|
200
|
+
WITNESS_EMIT_FAIL=1
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
152
204
|
|
|
153
205
|
# STEP 6 — actual cost / carry-forward record.
|
|
154
206
|
if grep -qE '^ACTUAL:[[:space:]]*<' "$WS/BUDGET.md" || ! grep -qE '^ACTUAL:[[:space:]]*\S' "$WS/BUDGET.md"; then
|
|
155
|
-
echo " ⛔ step 6 BLOCKED: record ACTUAL cost in $WS/BUDGET.md (actual-vs-estimate calibration)
|
|
207
|
+
echo " ⛔ step 6 BLOCKED: record ACTUAL cost in $WS/BUDGET.md (actual-vs-estimate calibration)."
|
|
208
|
+
echo " Expected — a LINE PREFIX, not a heading:"
|
|
209
|
+
echo " ACTUAL: <number/summary> ← this form is checked"
|
|
210
|
+
echo " ## ACTUAL ← a heading does NOT satisfy it"
|
|
211
|
+
echo " Re-run after adding the line."; exit 1
|
|
156
212
|
fi
|
|
157
213
|
_stamp "step-6-done"; echo " ✓ step 6: actual cost recorded"
|
|
158
214
|
|
|
@@ -181,4 +237,10 @@ case "$VERDICT" in
|
|
|
181
237
|
echo " No emit. Workspace stays as the evidence record; seen-filter will skip re-listing it." ;;
|
|
182
238
|
esac
|
|
183
239
|
echo "chamber run '$SLUG' COMPLETE (STATUS: step-7-done, verdict $VERDICT)."
|
|
240
|
+
# EMIT/PARTIAL-EMIT 인데 순서 증인이 없으면 **비영 종료**한다. KILL 은 영향 없다 —
|
|
241
|
+
# KILL 까지 실패로 세우면 과차단이고, 과차단은 override 를 습관화시킨다.
|
|
242
|
+
if [ "${WITNESS_EMIT_FAIL:-0}" = "1" ]; then
|
|
243
|
+
echo "⚠️ exit 3 — EMIT 이지만 순서 증인 미성립. 승격 주장에 쓰지 마라."
|
|
244
|
+
exit 3
|
|
245
|
+
fi
|
|
184
246
|
exit 0
|