@chrono-meta/fh-gate 3.2.0 → 3.4.0
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/registry/agent_cards.json +1 -1
- package/.claude/rules/fh_4axis_gate.md +25 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +2 -2
- package/CATALOG.md +4 -4
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +4 -4
- package/docs/OUTPUT_EVIDENCE.md +1 -1
- package/docs/STANDARDS_ALIGNMENT.md +1 -1
- package/docs/codex-compat.md +1 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +2 -2
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +30 -3
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +1 -1
- package/knowledge/shared/harness-core/skill_quality_rubric.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +31 -0
- package/knowledge/shared/rules/modes_and_value.md +2 -2
- package/package.json +2 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/README.md +38 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +95 -0
- package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +40 -0
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +1 -1
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +43 -5
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL.md +75 -3
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/harvest-loop/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
- package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +1 -1
- package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/finding_fleet.sh +391 -13
- package/scripts/finding_pipeline.sh +370 -11
- package/scripts/finding_verifier.sh +31 -2
- package/scripts/finding_verify.py +198 -13
- package/scripts/frontier_digest_autopilot.sh +3 -3
- package/scripts/gate_shape_scan.sh +16 -2
- package/scripts/test_finding_pipeline_lanes.sh +1556 -4
- package/scripts/test_gate_shape_scan_lanes.sh +11 -0
- package/scripts/test_marker_crossfamily_lanes.sh +75 -3
- package/scripts/test_marker_standpoint_lanes.sh +31 -6
- package/templates/.git-hooks/pre-commit +152 -6
- package/templates/local_fh_context.md +1 -1
- package/templates/regression_guard.sh +1 -1
|
@@ -30,13 +30,20 @@ set -uo pipefail
|
|
|
30
30
|
|
|
31
31
|
SUPPORTED_FAMILIES="codex gemini" # must match finding_verifier.sh's own case statement
|
|
32
32
|
|
|
33
|
-
TARGET=""; OUT=""; FLEET=""
|
|
34
|
-
usage() { echo "usage: finding_pipeline.sh <target-file> --out <dir> [--fleet <table>]" >&2; exit 2; }
|
|
33
|
+
TARGET=""; OUT=""; FLEET=""; SEEDED_IDS=""; SEEDED_FILE=""; SEEDS_ROUTED=""; ROUND2=0; ROUND2_BLIND=0; R1_ONLY=0; REUSE_R1=""
|
|
34
|
+
usage() { echo "usage: finding_pipeline.sh <target-file> --out <dir> [--fleet <table>] [--round2|--round2-blind] [--r1-only] [--reuse-r1 <dir>] [--seeded <ids>] [--seeded-file <path>]" >&2; exit 2; }
|
|
35
35
|
need() { [ $# -ge 2 ] || { echo "finding_pipeline: $1 needs a value" >&2; exit 2; }; }
|
|
36
36
|
[ $# -ge 1 ] || usage
|
|
37
37
|
TARGET="$1"; shift
|
|
38
38
|
while [ $# -gt 0 ]; do
|
|
39
39
|
case "$1" in
|
|
40
|
+
--round2) ROUND2=1; shift ;;
|
|
41
|
+
--round2-blind) ROUND2=1; ROUND2_BLIND=1; shift ;;
|
|
42
|
+
# 짝지음(paired) — 플래그만 fleet 으로 통과시킨다(측정 층은 fleet 이다). RESULT §15 잔여 참조.
|
|
43
|
+
--r1-only) R1_ONLY=1; shift ;;
|
|
44
|
+
--reuse-r1) REUSE_R1="${2:-}"; shift 2 ;;
|
|
45
|
+
--seeded) need "$@"; SEEDED_IDS="$2"; shift 2 ;;
|
|
46
|
+
--seeded-file) need "$@"; SEEDED_FILE="$2"; shift 2 ;;
|
|
40
47
|
--out) need "$@"; OUT="$2"; shift 2 ;; # `shift 2` on a trailing flag consumes nothing and
|
|
41
48
|
--fleet) need "$@"; FLEET="$2"; shift 2 ;; # spins forever; codex reproduced the hang.
|
|
42
49
|
*) usage ;;
|
|
@@ -57,13 +64,31 @@ umask 077
|
|
|
57
64
|
mkdir -p "$OUT" || { echo "finding_pipeline: cannot create --out" >&2; exit 2; }
|
|
58
65
|
# 🟥 미리 깔린 심링크를 따라가면 «출력»이 남의 파일 truncate 가 된다. 리다이렉션도 open() 도
|
|
59
66
|
# 심링크를 따라간다 — 그래서 쓰기 «전»에 거부한다(cross-family review 2026-09-09).
|
|
60
|
-
for _p in "$OUT" "$OUT/fleet" "$OUT/confirmed.jsonl" "$OUT/dropped.jsonl" "$OUT/splits.txt" "$OUT/families.txt"; do
|
|
67
|
+
for _p in "$OUT" "$OUT/fleet" "$OUT/confirmed.jsonl" "$OUT/dropped.jsonl" "$OUT/splits.txt" "$OUT/families.txt" "$OUT/roster.txt" "$OUT/roster.err" "$OUT/families.err"; do
|
|
61
68
|
if [ -L "$_p" ]; then
|
|
62
69
|
echo "finding_pipeline: refusing to write through a symlink: $_p" >&2; exit 2
|
|
63
70
|
fi
|
|
64
71
|
done
|
|
65
72
|
|
|
66
73
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
74
|
+
# 씨앗 선언을 «작업 시작 전» 에 확정한다 — verify 쪽과 같은 규율(빈 파일·디코드 오류는 설정 오류다)
|
|
75
|
+
ALL_SEEDS="$SEEDED_IDS"
|
|
76
|
+
if [ -n "$SEEDED_FILE" ]; then
|
|
77
|
+
_FROM_FILE=$(/usr/bin/python3 -c '
|
|
78
|
+
import sys
|
|
79
|
+
try:
|
|
80
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
81
|
+
ids = [l.strip() for l in fh if l.strip() and not l.startswith("#")]
|
|
82
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
83
|
+
print("finding_pipeline: --seeded-file unusable: %s" % e, file=sys.stderr); sys.exit(2)
|
|
84
|
+
if not ids:
|
|
85
|
+
print("finding_pipeline: --seeded-file %r yielded no ids — a control file that declares "
|
|
86
|
+
"nothing is a disabled control, not an absent one" % sys.argv[1], file=sys.stderr)
|
|
87
|
+
sys.exit(2)
|
|
88
|
+
print(",".join(ids))' "$SEEDED_FILE") || exit 2
|
|
89
|
+
ALL_SEEDS="${ALL_SEEDS:+$ALL_SEEDS,}$_FROM_FILE"
|
|
90
|
+
fi
|
|
91
|
+
|
|
67
92
|
FLEET_SH="$HERE/finding_fleet.sh"; VERIFY_PY="$HERE/finding_verify.py"; VERIFIER_SH="$HERE/finding_verifier.sh"
|
|
68
93
|
for f in "$FLEET_SH" "$VERIFY_PY" "$VERIFIER_SH"; do
|
|
69
94
|
[ -f "$f" ] || { echo "finding_pipeline: missing $f — skipped, NOT passed" >&2; exit 3; }
|
|
@@ -86,9 +111,68 @@ argv_json() { # each argument becomes one JSON string — no shell ever parses
|
|
|
86
111
|
# the second family and make a single-family run look cross-verified.
|
|
87
112
|
/bin/rm -rf "$OUT/fleet"
|
|
88
113
|
FA=(); [ -n "$FLEET" ] && FA=(--fleet "$FLEET")
|
|
89
|
-
|
|
114
|
+
# 생성시점 탈상관은 fleet 층의 일이다 — 드라이버는 플래그만 통과시킨다.
|
|
115
|
+
R2ARGS=(); [ "$ROUND2" -eq 1 ] && R2ARGS=(--round2); [ "$ROUND2_BLIND" -eq 1 ] && R2ARGS=(--round2-blind)
|
|
116
|
+
|
|
117
|
+
# ── 짝지음 인자 해석 ────────────────────────────────────────────────────────────────
|
|
118
|
+
# 🟥 --r1-only 는 검증·드롭감사·씨앗 판정을 «돌리지 않는다». 그러므로 씨앗을 선언한 채로는
|
|
119
|
+
# 거부한다 — 선언된 컨트롤이 돈 적 없이 통과로 읽히는 것이 이 레포가 이름 붙인 결함이다.
|
|
120
|
+
PAIRARGS=()
|
|
121
|
+
if [ "$R1_ONLY" -eq 1 ]; then
|
|
122
|
+
[ "$ROUND2" -eq 1 ] && { echo "finding_pipeline: --r1-only cannot be combined with --round2/--round2-blind" >&2; exit 2; }
|
|
123
|
+
[ -n "$REUSE_R1" ] && { echo "finding_pipeline: --r1-only and --reuse-r1 are mutually exclusive" >&2; exit 2; }
|
|
124
|
+
[ -n "$ALL_SEEDS" ] && { echo "finding_pipeline: --r1-only runs no verification — refusing to accept seeds that would never be judged" >&2; exit 2; }
|
|
125
|
+
PAIRARGS=(--r1-only)
|
|
126
|
+
fi
|
|
127
|
+
if [ -n "$REUSE_R1" ]; then
|
|
128
|
+
[ "$ROUND2" -eq 1 ] || { echo "finding_pipeline: --reuse-r1 requires --round2 or --round2-blind" >&2; exit 2; }
|
|
129
|
+
# 호출자가 파이프라인 out 을 줬으면 그 안의 fleet/ 로 내려간다. 어느 쪽을 골랐는지 «찍는다».
|
|
130
|
+
_R1SRC="$REUSE_R1"
|
|
131
|
+
[ -s "$REUSE_R1/fleet/findings.jsonl" ] && _R1SRC="$REUSE_R1/fleet"
|
|
132
|
+
[ -s "$_R1SRC/findings.jsonl" ] || { echo "finding_pipeline: --reuse-r1 resolved to '$_R1SRC' which has no non-empty findings.jsonl" >&2; exit 2; }
|
|
133
|
+
echo "PIPELINE reuse_r1 resolved=$_R1SRC sha256=$(/usr/bin/shasum -a 256 "$_R1SRC/findings.jsonl" | /usr/bin/cut -c1-16)"
|
|
134
|
+
PAIRARGS=(--reuse-r1 "$_R1SRC")
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
bash "$FLEET_SH" "$TARGET" --out "$OUT/fleet" ${FA[@]+"${FA[@]}"} ${R2ARGS[@]+"${R2ARGS[@]}"} ${PAIRARGS[@]+"${PAIRARGS[@]}"} 2>&1 | tee "$OUT/fleet_run.log"
|
|
90
138
|
FLEET_RC=${PIPESTATUS[0]}
|
|
139
|
+
|
|
140
|
+
# --r1-only: 공유 round-1 만 남기고 끝낸다(검증 단계 없음 — 위에서 씨앗을 이미 거부했다)
|
|
141
|
+
if [ "$R1_ONLY" -eq 1 ]; then
|
|
142
|
+
# 🟥 `|| echo 0` 을 쓰면 안 된다 — `grep -c .` 는 빈 파일에서 «0 을 찍고 rc=1» 이라 폴백이
|
|
143
|
+
# 한 줄을 더 붙여 `_n1="0\n0"` 이 되고, `[ "$_n1" -eq 0 ]` 은 **구문오류로 거짓**이 되어
|
|
144
|
+
# 빈 round-1 이 «사용 가능» 으로 통과한다(fail-OPEN). cross-family codex 가 적발, 자력 0.
|
|
145
|
+
# [[feedback_pipefail_fallback_disarms_guard]] — 질문은 «폴백 붙였나» 가 아니라
|
|
146
|
+
# «실패 시 stdout 이 비는가» 다. 여기서는 안 빈다.
|
|
147
|
+
_n1=$(/usr/bin/grep -c . "$OUT/fleet/findings.jsonl" 2>/dev/null)
|
|
148
|
+
case "${_n1:-}" in ''|*[!0-9]*) _n1=0 ;; esac
|
|
149
|
+
if [ "$FLEET_RC" -ne 0 ] || [ "$_n1" -eq 0 ]; then
|
|
150
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=$FLEET_RC findings=$_n1 status=R1_UNUSABLE rc=3"
|
|
151
|
+
exit 3
|
|
152
|
+
fi
|
|
153
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=0 findings=$_n1 status=R1_ONLY r1_dir=$OUT/fleet sha256=$(/usr/bin/shasum -a 256 "$OUT/fleet/findings.jsonl" | /usr/bin/cut -c1-16) rc=0"
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
91
156
|
FINDINGS="$OUT/fleet/findings.jsonl"
|
|
157
|
+
if [ "$FLEET_RC" -eq 0 ] && [ ! -s "$FINDINGS" ] && [ -f "$OUT/fleet/INTENTIONAL_EMPTY" ]; then
|
|
158
|
+
# 🟥 R3 #8: every member completed round 2 and withdrew everything — that is a REVIEWED empty result,
|
|
159
|
+
# not a missing review. The typed marker (not the empty file) is what distinguishes the two.
|
|
160
|
+
# R4 #6: a reused --out kept the previous run's aggregates beside this early return — reset them here.
|
|
161
|
+
for _p in "$OUT/confirmed.jsonl" "$OUT/dropped.jsonl" "$OUT/splits.txt"; do
|
|
162
|
+
[ -L "$_p" ] && { echo "finding_pipeline: refusing to write through a symlink: $_p" >&2; exit 2; }
|
|
163
|
+
: > "$_p"
|
|
164
|
+
done
|
|
165
|
+
# R4 #1: a declared control that never entered the run is ABSENT on this path too — WITHDRAWN must not
|
|
166
|
+
# pass a seed check it never ran. Same exit (5) and same SEEDED line the verifier would print.
|
|
167
|
+
if [ -n "$ALL_SEEDS" ]; then
|
|
168
|
+
_n=$(printf '%s' "$ALL_SEEDS" | /usr/bin/tr ',' '\n' | /usr/bin/grep -c .)
|
|
169
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=0 findings=0 status=WITHDRAWN confirmed=0 dropped=0 coverage=0/0 rc=5"
|
|
170
|
+
echo "SEEDED declared=$_n present=0 kept=0 dropped=0 abstained=0 status=ABSENT"
|
|
171
|
+
exit 5
|
|
172
|
+
fi
|
|
173
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=0 findings=0 status=WITHDRAWN confirmed=0 dropped=0 coverage=0/0 rc=0"
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
92
176
|
if [ "$FLEET_RC" -ne 0 ] || [ ! -s "$FINDINGS" ]; then
|
|
93
177
|
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=$FLEET_RC findings=0 status=UNREVIEWED"
|
|
94
178
|
echo " 🟥 an empty finding list is UNREVIEWED, not clean (finding_fleet.sh says so and this agrees)" >&2
|
|
@@ -97,6 +181,9 @@ fi
|
|
|
97
181
|
# A fleet is "ok" when ONE member succeeded. A member that crashed after emitting a few findings still
|
|
98
182
|
# leaves its family present, so the split routing below sees two families and the run looks complete.
|
|
99
183
|
FAILED_MEMBERS=$(grep -c '^MEMBER .* rc=[^0]' "$OUT/fleet_run.log" 2>/dev/null); FAILED_MEMBERS=${FAILED_MEMBERS:-0}
|
|
184
|
+
# 🟥 rc=0 인데 계약 출력이 0 인 멤버 — 안전필터 차단이 이 얼굴이다. failed_members 로는 안 보인다.
|
|
185
|
+
# «0건» 이 아니라 «검증 없는 0» 이므로 별 칸으로 표면화한다(기존 필드 의미는 안 건드린다).
|
|
186
|
+
BLOCKED_MEMBERS=$(grep -c '^MEMBER2\{0,1\} .*status=ZERO_NONJSON$' "$OUT/fleet_run.log" 2>/dev/null); BLOCKED_MEMBERS=${BLOCKED_MEMBERS:-0}
|
|
100
187
|
|
|
101
188
|
# ── 2. split by producer, verify each half with the other family ──────────────────────────────────
|
|
102
189
|
# Families are read into a newline-delimited list and validated. An unquoted space-joined expansion
|
|
@@ -121,17 +208,62 @@ if [ "$FAMRC" -ne 0 ] || [ ! -s "$OUT/families.txt" ]; then
|
|
|
121
208
|
echo "finding_pipeline: findings carry no usable producer_family — cannot route" >&2; exit 3
|
|
122
209
|
fi
|
|
123
210
|
|
|
211
|
+
# 🟥 검증자 후보 풀은 «발견을 낸 계열» 이 아니라 «실제로 돌아간 함대 명부» 다.
|
|
212
|
+
# families.txt 는 producer_family 에서 만들어지므로 0건을 낸 계열은 목록에서 사라진다. 그러면
|
|
213
|
+
# pick_verifier 가 «다른 계열이 없다» 로 읽고 그 발견들을 통째로 미검증 처리한다 — 검증자로
|
|
214
|
+
# 쓸 수 있는 계열이 멀쩡히 돌고 있었는데도. 측정이 스스로 찾아낸 결함이다:
|
|
215
|
+
# F_typed 팔 case_g02.py r1~r3 이 정확히 이 형태로 coverage 0/2 (0%) 였다(2026-09-10).
|
|
216
|
+
# 분할 루프는 바뀌지 않는다 — 여전히 발견자별이다. 바뀌는 것은 «누가 검증할 수 있나» 뿐이다.
|
|
217
|
+
# rc!=0 인 멤버는 제외한다: 안 돌아간 CLI 는 검증도 못 한다(한도 소진이 그 형태).
|
|
218
|
+
ROSTER="$OUT/roster.txt"
|
|
219
|
+
/usr/bin/python3 -c '
|
|
220
|
+
import re,sys
|
|
221
|
+
seen=[]
|
|
222
|
+
for l in open(sys.argv[1],encoding="utf-8",errors="replace"):
|
|
223
|
+
# 🟥 role 값에 공백이 들어갈 수 있다(fleet 표가 허용한다). 접두 전체를 한 패턴으로 묶으면
|
|
224
|
+
# 그런 줄이 통째로 무시되고 명부가 «빈 채로 성공» 한다 — 두 필드를 따로 잡는다.
|
|
225
|
+
# 🟥 R2 #6: `\brc=` 는 role 안의 rc= 를 먼저 잡는다(role=rc=1 rc=0 → 오탈락, 실행 확인).
|
|
226
|
+
# 레코드 형식이 고정이므로 «뒤에서» 잡는다 — rc= 다음에 findings= 가 오는 것이 앵커다.
|
|
227
|
+
# 🟥 R3 #6: «findings= 가 뒤에 온다» 는 접미 경계가 아니었다 — role=`rc=0 findings=x` 가 먼저 잡혔다.
|
|
228
|
+
# 레코드 끝(`rc=N findings=N [self_dropped=N] status=S$`)에 앵커한다.
|
|
229
|
+
fm = re.search(r"\bfamily=(\S+)", l); rm = re.search(r" rc=(\S+) findings=\S+(?: self_dropped=\S+)? status=\S+\s*$", l)
|
|
230
|
+
if not l.startswith("MEMBER") or not fm or not rm: continue
|
|
231
|
+
fam, rc = fm.group(1), rm.group(1)
|
|
232
|
+
if rc != "0": continue
|
|
233
|
+
if not re.fullmatch(r"[A-Za-z0-9_.-]+", fam):
|
|
234
|
+
sys.stderr.write("finding_pipeline: illegal family %r in fleet roster — refusing to route\n" % (fam,))
|
|
235
|
+
sys.exit(2)
|
|
236
|
+
if fam not in seen: seen.append(fam)
|
|
237
|
+
if not seen:
|
|
238
|
+
# 🟥 빈 목록을 «성공» 으로 내보내면 안 된다. `print("\n".join([]))` 는 개행 1바이트를 쓰고
|
|
239
|
+
# `[ -s ]` 는 그걸 «내용 있음» 으로 읽는다 — 그러면 DEGRADE 분기가 통째로 건너뛰어지고
|
|
240
|
+
# 아무도 검증자를 못 받는다(cross-family #4, codex 2026-09-10).
|
|
241
|
+
sys.stderr.write("finding_pipeline: fleet roster parsed 0 usable families\n"); sys.exit(4)
|
|
242
|
+
print("\n".join(seen))' "$OUT/fleet/members.txt" > "$ROSTER" 2>"$OUT/roster.err"
|
|
243
|
+
ROSTRC=$?
|
|
244
|
+
_ROSTER_N=$(/usr/bin/grep -c . "$ROSTER" 2>/dev/null); _ROSTER_N=${_ROSTER_N:-0}
|
|
245
|
+
if [ "$ROSTRC" -ne 0 ] || [ "$_ROSTER_N" -eq 0 ]; then
|
|
246
|
+
cat "$OUT/roster.err" >&2
|
|
247
|
+
# 🟥 degrade 는 «조용히 넘어감» 이 아니라 «좁은 풀로 되돌아감 + 그 사실을 말함» 이다.
|
|
248
|
+
# 없는 검증자를 만들어내지 않으므로 방향은 안전하다(과소검증 쪽으로 실패한다).
|
|
249
|
+
echo " ⚠️ fleet roster unusable (rc=$ROSTRC) — verifier pool DEGRADED to producer families; a family that produced 0 findings cannot be picked" >&2
|
|
250
|
+
/bin/cp "$OUT/families.txt" "$ROSTER" 2>/dev/null || : > "$ROSTER"
|
|
251
|
+
ROSTER_SRC="families(DEGRADED)"
|
|
252
|
+
else
|
|
253
|
+
ROSTER_SRC="fleet"
|
|
254
|
+
fi
|
|
255
|
+
|
|
124
256
|
supported() { case " $SUPPORTED_FAMILIES " in *" $1 "*) return 0;; *) return 1;; esac; }
|
|
125
257
|
pick_verifier() { # $1=producer — a DIFFERENT family the wrapper can actually run
|
|
126
258
|
local p="$1" f
|
|
127
|
-
while IFS= read -r f; do [ -n "$f" ] && [ "$f" != "$p" ] && supported "$f" && { echo "$f"; return; }; done < "$
|
|
259
|
+
while IFS= read -r f; do [ -n "$f" ] && [ "$f" != "$p" ] && supported "$f" && { echo "$f"; return; }; done < "$ROSTER"
|
|
128
260
|
echo ""
|
|
129
261
|
}
|
|
130
262
|
pick_auditor() { # $1=producer $2=verifier — prefer a third party; fall back to producer (appeal)
|
|
131
263
|
local p="$1" v="$2" f
|
|
132
264
|
while IFS= read -r f; do
|
|
133
265
|
[ -n "$f" ] && [ "$f" != "$p" ] && [ "$f" != "$v" ] && supported "$f" && { echo "$f"; return; }
|
|
134
|
-
done < "$
|
|
266
|
+
done < "$ROSTER"
|
|
135
267
|
supported "$p" && { echo "$p"; return; }
|
|
136
268
|
echo ""
|
|
137
269
|
}
|
|
@@ -140,7 +272,11 @@ pick_auditor() { # $1=producer $2=verifier — prefer a third party; fall back t
|
|
|
140
272
|
# run's verdict while a confirmed survivor sat in the other split. Rank them explicitly instead, and
|
|
141
273
|
# derive 1-vs-0 from the survivor count at the end, never from a split.
|
|
142
274
|
WORST_RANK=0; WORST_CODE=0
|
|
143
|
-
|
|
275
|
+
# 🟥 exit 5 (SEEDED DEGRADED/INCONCLUSIVE/ABSENT) had NO CASE here, so it fell to the default 0
|
|
276
|
+
# and the driver exited 0 while a split had just reported that the filter deleted a known-true
|
|
277
|
+
# finding. The entire seeded control was invisible at the level a reader actually reads.
|
|
278
|
+
# (cross-family round 4, gemini family, S severity — three same-family rounds missed it.)
|
|
279
|
+
rank_of() { case "$1" in 2) echo 5;; 5) echo 4;; 3) echo 3;; 4) echo 2;; *) echo 0;; esac; }
|
|
144
280
|
note_rc() { local r; r=$(rank_of "$1"); if [ "$r" -gt "$WORST_RANK" ]; then WORST_RANK="$r"; WORST_CODE="$1"; fi; }
|
|
145
281
|
|
|
146
282
|
: > "$OUT/confirmed.jsonl"; : > "$OUT/dropped.jsonl"; : > "$OUT/splits.txt"
|
|
@@ -169,16 +305,84 @@ for l in open(sys.argv[1],encoding="utf-8"):
|
|
|
169
305
|
printf 'split producer=%s verifier=%s rc=0 (no findings)\n' "$PROD" "$VER" >> "$OUT/splits.txt"; continue
|
|
170
306
|
fi
|
|
171
307
|
|
|
308
|
+
# 🟥 검증 패스의 CLI stderr(토큰 회계 «tokens used» 포함)가 mktemp 에 쓰이고 cleanup 이 지웠다.
|
|
309
|
+
# 그래서 «검증 패스 토큰 = 0바이트» 로 보였는데 0 이 아니라 «버려진 것» 이다. --keep 은 이미
|
|
310
|
+
# 있는 채널이고 파이프라인이 안 넘겼을 뿐이다. split 디렉터리에 남긴다(2026-09-10).
|
|
311
|
+
# 🟥 R2 #7: 검증과 감사가 같은 keep 을 쓰면 드롭이 있는 런에서 감사가 검증 err.txt 를 덮는다 —
|
|
312
|
+
# 토큰 회계가 «바로 그 런» 에서 또 버려진다. 갈라 둔다. #1: 잎 경로 심링크도 여기서 거부.
|
|
313
|
+
for _p in "$SD/keep_verify" "$SD/keep_audit"; do
|
|
314
|
+
[ -L "$_p" ] && { echo "finding_pipeline: refusing to write through a symlink: $_p" >&2; note_rc 2; continue 2; }
|
|
315
|
+
mkdir -p "$_p"
|
|
316
|
+
for _leaf in raw.txt err.txt prompt.txt findings.jsonl; do
|
|
317
|
+
[ -L "$_p/$_leaf" ] && { echo "finding_pipeline: refusing to write through a symlink: $_p/$_leaf" >&2; note_rc 2; continue 3; }
|
|
318
|
+
done
|
|
319
|
+
done
|
|
320
|
+
export FH_VERIFIER_KEEP="$SD" # verifier 가 MODE 별로 keep_verify/ keep_audit/ 를 «스스로» 고른다 (R2 #7)
|
|
172
321
|
AUDARGS=()
|
|
173
322
|
if [ -n "$AUD" ]; then
|
|
174
323
|
AUDARGS=(--audit-verifier-argv "$(argv_json bash "$VERIFIER_SH" --family "$AUD" --target "$TARGET" --audit)" --audit-family "$AUD")
|
|
175
324
|
else
|
|
176
325
|
echo " ⚠️ split producer=$PROD — no supported auditor; any drop will come back UNAUDITED" >&2
|
|
177
326
|
fi
|
|
327
|
+
# 🟥 Seeds are forwarded PER SPLIT, filtered to the ids that actually live in that split's input.
|
|
328
|
+
# Forwarding the whole declared set to every split would make each split report ABSENT for the
|
|
329
|
+
# seeds that belong to the other producer — a false alarm manufactured by the routing, not by the
|
|
330
|
+
# filter. The global "did any declared seed enter at all?" question is answered once, below.
|
|
331
|
+
SEEDARGS=()
|
|
332
|
+
if [ -n "$ALL_SEEDS" ]; then
|
|
333
|
+
# 🟥 R6 #3: resolve each declared seed against the WHOLE fleet result BEFORE partitioning — two rows in
|
|
334
|
+
# different splits sharing one member_id each looked unique inside their split (CLEAN twice) while the
|
|
335
|
+
# unsplit verifier calls the same declaration AMBIGUOUS. Global multiplicity is decided once, here.
|
|
336
|
+
_AMBIG=$(ALL_SEEDS="$ALL_SEEDS" /usr/bin/python3 -c '
|
|
337
|
+
import sys, json, os, collections
|
|
338
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
339
|
+
cnt = collections.Counter()
|
|
340
|
+
try:
|
|
341
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
342
|
+
l = l.strip()
|
|
343
|
+
if not l: continue
|
|
344
|
+
try: d = json.loads(l)
|
|
345
|
+
except Exception: continue
|
|
346
|
+
if not isinstance(d, dict): continue
|
|
347
|
+
keys = {str(d[k]) for k in ("id", "member_id") if d.get(k) is not None}
|
|
348
|
+
for k in keys & want: cnt[k] += 1
|
|
349
|
+
except OSError: pass
|
|
350
|
+
print(",".join(sorted(k for k, c in cnt.items() if c > 1)))' "$FINDINGS")
|
|
351
|
+
if [ -n "$_AMBIG" ]; then
|
|
352
|
+
echo "PIPELINE target=$(basename "$TARGET") status=SEED_AMBIGUOUS seeds=$_AMBIG rc=5"
|
|
353
|
+
echo "SEEDED declared=$(printf '%s' "$ALL_SEEDS" | /usr/bin/tr ',' '\n' | /usr/bin/grep -c .) present=0 kept=0 dropped=0 abstained=0 status=AMBIGUOUS"
|
|
354
|
+
echo " 🟥 seed(s) $_AMBIG match more than one fleet row — member ids are only locally unique; declare the routing id instead" >&2
|
|
355
|
+
exit 5
|
|
356
|
+
fi
|
|
357
|
+
_SPLIT_SEEDS=$(ALL_SEEDS="$ALL_SEEDS" /usr/bin/python3 -c '
|
|
358
|
+
import sys, json, os
|
|
359
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
360
|
+
have = set()
|
|
361
|
+
try:
|
|
362
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
363
|
+
l = l.strip()
|
|
364
|
+
if not l: continue
|
|
365
|
+
try: d = json.loads(l)
|
|
366
|
+
except Exception: continue
|
|
367
|
+
if isinstance(d, dict):
|
|
368
|
+
if d.get("id") is not None: have.add(str(d["id"]))
|
|
369
|
+
if d.get("member_id") is not None: have.add(str(d["member_id"]))
|
|
370
|
+
except OSError: pass
|
|
371
|
+
print(",".join(sorted(want & have)))' "$SD/in.jsonl")
|
|
372
|
+
if [ -n "$_SPLIT_SEEDS" ]; then
|
|
373
|
+
SEEDARGS=(--seeded "$_SPLIT_SEEDS")
|
|
374
|
+
fi
|
|
375
|
+
fi
|
|
178
376
|
/usr/bin/python3 "$VERIFY_PY" "$SD/in.jsonl" --out "$SD" \
|
|
179
377
|
--verifier-argv "$(argv_json bash "$VERIFIER_SH" --family "$VER" --target "$TARGET")" --family "$VER" \
|
|
180
|
-
${AUDARGS[@]+"${AUDARGS[@]}"} > "$SD/summary.txt" 2>"$SD/err.txt"
|
|
378
|
+
${SEEDARGS[@]+"${SEEDARGS[@]}"} ${AUDARGS[@]+"${AUDARGS[@]}"} > "$SD/summary.txt" 2>"$SD/err.txt"
|
|
181
379
|
RC=$?
|
|
380
|
+
# 🟥 «라우팅했다» 를 «검증됐다» 로 세면 안 된다. 초판은 verify 를 부르기 «전» 에 기록해서,
|
|
381
|
+
# verify 가 크래시해도 그 씨앗은 «돌았다» 로 남았다(cross-family round 6). 요약을 실제로
|
|
382
|
+
# 낸 실행에 대해서만 기록한다.
|
|
383
|
+
if [ -n "${_SPLIT_SEEDS:-}" ] && [ -s "$SD/summary.txt" ]; then
|
|
384
|
+
SEEDS_ROUTED="${SEEDS_ROUTED:+$SEEDS_ROUTED,}$_SPLIT_SEEDS"
|
|
385
|
+
fi
|
|
182
386
|
note_rc "$RC"
|
|
183
387
|
cat "$SD/summary.txt"
|
|
184
388
|
printf 'split producer=%s verifier=%s auditor=%s rc=%s\n' "$PROD" "$VER" "${AUD:-NONE}" "$RC" >> "$OUT/splits.txt"
|
|
@@ -196,6 +400,154 @@ TOTAL_UNVER=$(count_lines '"verdict": *"unverified"' "$OUT/confirmed.jsonl")
|
|
|
196
400
|
# becomes true by wording alone.
|
|
197
401
|
TOTAL_AUD=$(count_lines '"drop_verdict": *"\(correct-drop\|wrong-drop\|uncertain\)"' "$OUT/dropped.jsonl")
|
|
198
402
|
TOTAL_WRONG=$(count_lines '"reinstated": *true' "$OUT/confirmed.jsonl")
|
|
403
|
+
# 🟥 COVERAGE ALSO RIDES THE DRIVER LINE, not only the per-split one. `finding_verify.py` records it
|
|
404
|
+
# per split, but the driver's summary is what a reader actually reads — a field recorded in a place
|
|
405
|
+
# nobody reads is the half-externalisation shape this repo keeps re-finding (a slot with zero
|
|
406
|
+
# consumers always reports "done", because presence is doing the judging).
|
|
407
|
+
#
|
|
408
|
+
# 🟥 THREE things this number got wrong on the first attempt, all found by cross-family round 2:
|
|
409
|
+
# ⓐ it subtracted `unverified` but NOT `needs-debate` — an all-debate run printed 100%. The
|
|
410
|
+
# verifier had already been fixed for exactly this and the driver had not: a half-fix that
|
|
411
|
+
# stopped at the propagation boundary, which is the failure mode this repo has a name for.
|
|
412
|
+
# ⓑ the DENOMINATOR was `confirmed + dropped`, i.e. what came OUT. A partition that is skipped
|
|
413
|
+
# (unsupported family) then vanishes from numerator and denominator alike and coverage reads
|
|
414
|
+
# 100% while findings were never judged at all. The denominator must be what came IN.
|
|
415
|
+
# ⓒ the counters were line greps, so passthrough metadata containing `"verdict": "unverified"`
|
|
416
|
+
# anywhere in a row counted as an abstention. Verdicts are read from the TOP LEVEL of each row.
|
|
417
|
+
# 🟥 판정 수는 «뺄셈» 이 아니라 «양의 계수» 로 구한다. 초판은 입력 총수에서 기권을 뺐는데,
|
|
418
|
+
# 그러면 **출력에 아예 안 나타난 것이 판정된 것으로 계수된다** — 건너뛴 파티션도, 읽기 실패도,
|
|
419
|
+
# 깨진 JSON 도 전부 «100%» 가 된다(cross-family R3 가 A급 둘로 지목, 재현됨).
|
|
420
|
+
# 판정 = 「해결된 verdict 를 실제로 들고 있는 행」의 수다. 없으면 0 이고, 그것이 fail-closed 다.
|
|
421
|
+
_count_json() { # $1 = 파일, $2 = 필드, $3.. = 셀 값들. 실패는 «0» 이 아니라 비-영 종료로 낸다.
|
|
422
|
+
/usr/bin/python3 -c '
|
|
423
|
+
import sys, json
|
|
424
|
+
path, field, want = sys.argv[1], sys.argv[2], set(sys.argv[3:])
|
|
425
|
+
n = 0
|
|
426
|
+
try:
|
|
427
|
+
fh = open(path, encoding="utf-8")
|
|
428
|
+
except FileNotFoundError:
|
|
429
|
+
print(0); sys.exit(0) # 파일 자체가 없는 것은 «행이 0» 이다
|
|
430
|
+
except OSError as e:
|
|
431
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
432
|
+
with fh:
|
|
433
|
+
try:
|
|
434
|
+
for l in fh:
|
|
435
|
+
l = l.strip()
|
|
436
|
+
if not l: continue
|
|
437
|
+
d = json.loads(l) # 깨진 줄은 «건너뛰기» 가 아니라 계측 오류다
|
|
438
|
+
if not isinstance(d, dict): raise ValueError("row is not an object")
|
|
439
|
+
v = d.get(field)
|
|
440
|
+
if v is not None and not isinstance(v, str): raise ValueError("non-string %s" % field)
|
|
441
|
+
if v in want: n += 1
|
|
442
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
443
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
444
|
+
print(n)' "$@"
|
|
445
|
+
}
|
|
446
|
+
_count_lines_json() { # 행 수 — 같은 실패 규율
|
|
447
|
+
/usr/bin/python3 -c '
|
|
448
|
+
import sys
|
|
449
|
+
n = 0
|
|
450
|
+
try:
|
|
451
|
+
fh = open(sys.argv[1], encoding="utf-8")
|
|
452
|
+
except FileNotFoundError:
|
|
453
|
+
print(0); sys.exit(0)
|
|
454
|
+
except OSError as e:
|
|
455
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
456
|
+
with fh:
|
|
457
|
+
try:
|
|
458
|
+
for l in fh:
|
|
459
|
+
if l.strip(): n += 1
|
|
460
|
+
except UnicodeDecodeError as e:
|
|
461
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
462
|
+
print(n)' "$1"
|
|
463
|
+
}
|
|
464
|
+
COUNT_ERR=0
|
|
465
|
+
TOTAL_IN=$(_count_lines_json "$FINDINGS") || COUNT_ERR=1
|
|
466
|
+
TOTAL_UNVER=$(_count_json "$OUT/confirmed.jsonl" verdict unverified) || COUNT_ERR=1
|
|
467
|
+
TOTAL_DEBATE=$(_count_json "$OUT/confirmed.jsonl" verdict needs-debate) || COUNT_ERR=1
|
|
468
|
+
# 🟥 양의 계수: 실제로 «해결된» 판정을 들고 있는 행만 센다.
|
|
469
|
+
# 복권된 행은 이제 verdict=confirmed 를 단다(verify 쪽 근원 수리). 그 전에 만들어진 산출물과의
|
|
470
|
+
# 호환을 위해 «reinstated 이면서 false-positive» 도 판정으로 센다 — 그 행은 감사가 «되돌렸다» 는
|
|
471
|
+
# 결정을 받은 것이지 미판정이 아니다.
|
|
472
|
+
JUDGED_CONF=$(_count_json "$OUT/confirmed.jsonl" verdict confirmed) || COUNT_ERR=1
|
|
473
|
+
JUDGED_REINST=$(/usr/bin/python3 -c '
|
|
474
|
+
import sys, json
|
|
475
|
+
n = 0
|
|
476
|
+
try:
|
|
477
|
+
fh = open(sys.argv[1], encoding="utf-8")
|
|
478
|
+
except FileNotFoundError:
|
|
479
|
+
print(0); sys.exit(0)
|
|
480
|
+
except OSError as e:
|
|
481
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
482
|
+
with fh:
|
|
483
|
+
try:
|
|
484
|
+
for l in fh:
|
|
485
|
+
l = l.strip()
|
|
486
|
+
if not l: continue
|
|
487
|
+
d = json.loads(l)
|
|
488
|
+
if isinstance(d, dict) and d.get("reinstated") is True and d.get("verdict") != "confirmed":
|
|
489
|
+
n += 1
|
|
490
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
491
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
492
|
+
print(n)' "$OUT/confirmed.jsonl") || COUNT_ERR=1
|
|
493
|
+
JUDGED_DROP=$(_count_json "$OUT/dropped.jsonl" verdict false-positive) || COUNT_ERR=1
|
|
494
|
+
TOTAL_IN=${TOTAL_IN:-0}; TOTAL_UNVER=${TOTAL_UNVER:-0}; TOTAL_DEBATE=${TOTAL_DEBATE:-0}
|
|
495
|
+
JUDGED_CONF=${JUDGED_CONF:-0}; JUDGED_DROP=${JUDGED_DROP:-0}
|
|
496
|
+
JUDGED_REINST=${JUDGED_REINST:-0}
|
|
497
|
+
TOTAL_JUDGED=$(( JUDGED_CONF + JUDGED_DROP + JUDGED_REINST ))
|
|
498
|
+
# 🟥 Split-level ABSENT cannot fire once seeds are filtered per split, so the "did the control run
|
|
499
|
+
# at all?" question is asked ONCE, globally, against the whole fleet output.
|
|
500
|
+
if [ -n "$ALL_SEEDS" ]; then
|
|
501
|
+
_SEEDS_SEEN=$(ALL_SEEDS="$ALL_SEEDS" /usr/bin/python3 -c '
|
|
502
|
+
import sys, json, os
|
|
503
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
504
|
+
have = set()
|
|
505
|
+
try:
|
|
506
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
507
|
+
l = l.strip()
|
|
508
|
+
if not l: continue
|
|
509
|
+
try: d = json.loads(l)
|
|
510
|
+
except Exception: continue
|
|
511
|
+
if isinstance(d, dict):
|
|
512
|
+
if d.get("id") is not None: have.add(str(d["id"]))
|
|
513
|
+
if d.get("member_id") is not None: have.add(str(d["member_id"]))
|
|
514
|
+
except OSError: pass
|
|
515
|
+
print(len(want & have))' "$FINDINGS")
|
|
516
|
+
if [ "${_SEEDS_SEEN:-0}" -eq 0 ]; then
|
|
517
|
+
echo " ⚠️ declared seeds are not present anywhere in the fleet output — the control never ran" >&2
|
|
518
|
+
note_rc 5
|
|
519
|
+
fi
|
|
520
|
+
# 🟥 «하나라도 들어왔나» 로는 부족하다. 선언한 씨앗 중 **어떤 split 에도 안 실린 것**이 있으면
|
|
521
|
+
# 그 씨앗은 검증기를 통과한 적이 없고, 그런데도 나머지가 CLEAN 이면 드라이버는 0 을 낸다.
|
|
522
|
+
# 두 경로로 그렇게 된다: ⓐ fleet 출력에 애초에 없다 ⓑ 있는데 producer_family 가 없거나
|
|
523
|
+
# 지원되지 않아 라우팅에서 빠진다. 둘 다 «통제가 그 씨앗에 대해 안 돌았다» 는 같은 사실이다.
|
|
524
|
+
# (cross-family round 5, gemini 계열.)
|
|
525
|
+
_SEEDS_MISSED=$(ALL_SEEDS="$ALL_SEEDS" ROUTED="${SEEDS_ROUTED:-}" /usr/bin/python3 -c '
|
|
526
|
+
import os
|
|
527
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
528
|
+
got = {x.strip() for x in os.environ.get("ROUTED", "").split(",") if x.strip()}
|
|
529
|
+
missed = sorted(want - got)
|
|
530
|
+
print(",".join(missed))')
|
|
531
|
+
if [ -n "$_SEEDS_MISSED" ]; then
|
|
532
|
+
echo " ⚠️ declared seeds never reached any verified split: $_SEEDS_MISSED — the control did not run for them" >&2
|
|
533
|
+
note_rc 5
|
|
534
|
+
fi
|
|
535
|
+
fi
|
|
536
|
+
if [ "$COUNT_ERR" -ne 0 ]; then
|
|
537
|
+
echo " ⚠️ coverage 계측이 실패했다 — 이 런의 coverage 는 판정이 아니라 «못 잼» 이다" >&2
|
|
538
|
+
note_rc 3
|
|
539
|
+
fi
|
|
540
|
+
# 🟥 회계 불일치는 clamp 로 덮지 않는다 — 판정이 입력보다 많으면 그건 «0으로 눌러서 정상처럼
|
|
541
|
+
# 보이게 할 것» 이 아니라 계측 결함이다(R3 지적).
|
|
542
|
+
if [ "$TOTAL_JUDGED" -gt "$TOTAL_IN" ]; then
|
|
543
|
+
echo " ⚠️ coverage 회계 불일치: 판정 $TOTAL_JUDGED > 입력 $TOTAL_IN" >&2
|
|
544
|
+
note_rc 3
|
|
545
|
+
fi
|
|
546
|
+
if [ "$TOTAL_IN" -gt 0 ]; then
|
|
547
|
+
COV_PCT=$(( TOTAL_JUDGED * 100 / TOTAL_IN )) # floored, never rounded
|
|
548
|
+
else
|
|
549
|
+
COV_PCT=0
|
|
550
|
+
fi
|
|
199
551
|
|
|
200
552
|
if [ "$FAILED_MEMBERS" -gt 0 ]; then
|
|
201
553
|
echo " ⚠️ $FAILED_MEMBERS fleet member(s) exited non-zero — a member that crashed after emitting some findings leaves its family looking complete" >&2
|
|
@@ -205,9 +557,16 @@ fi
|
|
|
205
557
|
RC_FINAL="$WORST_CODE"
|
|
206
558
|
[ "$WORST_RANK" -eq 0 ] && [ "$TOTAL_CONF" -eq 0 ] && RC_FINAL=1
|
|
207
559
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
560
|
+
# 🟥 `confirmed=` prints the RESOLVED confirmations, not the raw line count of confirmed.jsonl.
|
|
561
|
+
# That file also holds debate and unverified rows, so the driver printed `confirmed=2 debate=2
|
|
562
|
+
# coverage=0/2` — three numbers in one line contradicting each other. The verifier had already been
|
|
563
|
+
# fixed for exactly this; the driver had not. **Third recurrence of the same half-fix shape in this
|
|
564
|
+
# change** (cross-family round 6). Survivor semantics for rc stay on TOTAL_CONF.
|
|
565
|
+
printf 'PIPELINE target=%s families=%s roster=%s(%s) confirmed=%s dropped=%s unverified=%s debate=%s coverage=%s/%s (%s%%) audited_drops=%s reinstated=%s failed_members=%s blocked_members=%s rc=%s\n' \
|
|
566
|
+
"$(basename "$TARGET")" "$(tr '\n' ',' < "$OUT/families.txt" | sed 's/,$//')" \
|
|
567
|
+
"$(tr '\n' ',' < "$ROSTER" | sed 's/,$//')" "$ROSTER_SRC" "$JUDGED_CONF" "$TOTAL_DROP" \
|
|
568
|
+
"$TOTAL_UNVER" "$TOTAL_DEBATE" "$TOTAL_JUDGED" "$TOTAL_IN" "$COV_PCT" "$TOTAL_AUD" "$TOTAL_WRONG" \
|
|
569
|
+
"$FAILED_MEMBERS" "$BLOCKED_MEMBERS" "$RC_FINAL"
|
|
211
570
|
# `unverified` is reported on its own line rather than folded into `confirmed`, because folding it is
|
|
212
571
|
# exactly the "not found rendered as zero" family this repo keeps re-finding.
|
|
213
572
|
exit "$RC_FINAL"
|
|
@@ -22,6 +22,9 @@ CODEX="${FH_CODEX_BIN:-$(command -v codex 2>/dev/null || echo "$HOME/.npm-global
|
|
|
22
22
|
AGY="${FH_AGY_BIN:-$(command -v agy 2>/dev/null || echo "$HOME/.local/bin/agy")}"
|
|
23
23
|
|
|
24
24
|
FAMILY=""; TARGET=""; MODE="verify"; KEEP=""
|
|
25
|
+
# 호출자(파이프라인)가 env 로 보존 경로를 줄 수 있다 — 검증 패스의 CLI stderr(토큰 회계)가 여기서 산다.
|
|
26
|
+
# --keep 플래그가 뒤에서 덮으므로 env 는 «폴백» 이지 강제가 아니다. (2026-09-10: 0바이트 ≠ 0 토큰)
|
|
27
|
+
KEEP="${FH_VERIFIER_KEEP:-}"
|
|
25
28
|
usage() { echo "usage: finding_verifier.sh --family codex|gemini --target <file> [--audit] [--keep <dir>]" >&2; exit 2; }
|
|
26
29
|
# `shift 2` with only one word left FAILS and consumes nothing, so the loop spins forever. Under
|
|
27
30
|
# `set -uo pipefail` (no errexit) nothing stops it. codex reproduced a hang on a trailing --family.
|
|
@@ -52,7 +55,33 @@ fi
|
|
|
52
55
|
umask 077
|
|
53
56
|
|
|
54
57
|
WORK="$(mktemp -d 2>/dev/null)" || { echo "finding_verifier: mktemp failed" >&2; exit 3; }
|
|
55
|
-
|
|
58
|
+
# 🟥 R2 #7: 검증과 감사가 같은 keep 을 쓰면 감사가 검증 err.txt(토큰 회계)를 덮는다 — 드롭이 있는 런에서
|
|
59
|
+
# «바로 그 런» 의 회계가 또 버려진다. MODE 별 하위 디렉터리로 가른다. R2 #1: 심링크 잎은 복사 전에 거부.
|
|
60
|
+
# 🟥 R3 #3: the root is checked BEFORE the mode dir is appended — appended first, a symlink root became
|
|
61
|
+
# an ancestor and passed both the dir check and the leaf check (path probe: INPUT_IS_SYMLINK → GUARD_ALLOWS).
|
|
62
|
+
# R4 #2: `link/` makes -L false (the trailing slash resolves through the link) — strip separators first, keep "/" intact.
|
|
63
|
+
# R6 #5: `link/.` and `link//.` also resolve through the link — strip trailing "/." and "/" until stable.
|
|
64
|
+
while [ "${#KEEP}" -gt 1 ]; do
|
|
65
|
+
case "$KEEP" in
|
|
66
|
+
*/.) KEEP="${KEEP%/.}" ;;
|
|
67
|
+
*/) KEEP="${KEEP%/}" ;;
|
|
68
|
+
*) break ;;
|
|
69
|
+
esac
|
|
70
|
+
done
|
|
71
|
+
if [ -n "$KEEP" ] && [ -L "$KEEP" ]; then echo "finding_verifier: --keep root is a symlink: $KEEP — refusing" >&2; exit 2; fi
|
|
72
|
+
if [ -n "$KEEP" ] && [ -z "${FH_VERIFIER_KEEP_FLAT:-}" ]; then KEEP="$KEEP/keep_$MODE"; fi
|
|
73
|
+
cleanup() {
|
|
74
|
+
if [ -n "$KEEP" ]; then
|
|
75
|
+
[ -L "$KEEP" ] && { echo "finding_verifier: refusing to keep through a symlink: $KEEP" >&2; rm -rf "$WORK"; return; }
|
|
76
|
+
mkdir -p "$KEEP" 2>/dev/null
|
|
77
|
+
for _f in "$WORK"/*; do
|
|
78
|
+
_dst="$KEEP/$(basename "$_f")"
|
|
79
|
+
[ -L "$_dst" ] && { echo "finding_verifier: refusing to keep through a symlink: $_dst" >&2; continue; }
|
|
80
|
+
cp "$_f" "$_dst" 2>/dev/null
|
|
81
|
+
done
|
|
82
|
+
fi
|
|
83
|
+
rm -rf "$WORK"
|
|
84
|
+
}
|
|
56
85
|
trap cleanup EXIT
|
|
57
86
|
|
|
58
87
|
cat > "$WORK/findings.jsonl"
|
|
@@ -95,7 +124,7 @@ fi
|
|
|
95
124
|
case "$FAMILY" in
|
|
96
125
|
codex) "$CODEX" exec --sandbox read-only --skip-git-repo-check -m gpt-6-astra \
|
|
97
126
|
-c model_reasoning_effort="high" < "$WORK/prompt.txt" > "$WORK/raw.txt" 2>"$WORK/err.txt" ;;
|
|
98
|
-
gemini) "$AGY" --model gemini-3.8-flash-high --output-format text --print-timeout
|
|
127
|
+
gemini) "$AGY" --mode plan --sandbox --model gemini-3.8-flash-high --output-format text --print-timeout 20m \
|
|
99
128
|
-p "$(cat "$WORK/prompt.txt")" < /dev/null > "$WORK/raw.txt" 2>"$WORK/err.txt" ;;
|
|
100
129
|
*) echo "finding_verifier: unknown family '$FAMILY' (codex|gemini)" >&2; exit 2 ;;
|
|
101
130
|
esac
|