@chrono-meta/fh-gate 3.1.4 → 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 +26 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +8 -2
- package/CATALOG.md +4 -4
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +54 -10
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/docs/OUTPUT_EVIDENCE.md +2 -2
- 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/claude_md_gate_details.md +26 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +1 -1
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +76 -3
- package/knowledge/shared/harness-core/governance_engineering_definition.md +89 -0
- 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 +99 -0
- package/knowledge/shared/rules/auto_project_mapping.md +1 -1
- package/knowledge/shared/rules/modes_and_value.md +2 -2
- package/package.json +12 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/README.md +38 -0
- package/plugins/fh-commons/skills/preprep/SKILL.md +23 -0
- package/plugins/fh-commons/skills/preprep/fixtures/font_revert_probe.py +92 -0
- package/plugins/fh-commons/skills/preprep/lane_font.py +462 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +16 -1
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +17 -0
- package/plugins/fh-commons/skills/preprep/test_lane_font.py +452 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +164 -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/doc_claim_triad_scan.py +303 -0
- package/scripts/finding_fleet.sh +558 -0
- package/scripts/finding_pipeline.sh +572 -0
- package/scripts/finding_verifier.sh +173 -0
- package/scripts/finding_verify.py +468 -0
- package/scripts/frontier_digest_autopilot.sh +3 -3
- package/scripts/gate_pathspec_check.sh +1 -0
- package/scripts/gate_shape_scan.sh +120 -0
- package/scripts/selfcheck.sh +50 -0
- package/scripts/test_doc_claim_triad_lanes.sh +124 -0
- package/scripts/test_finding_pipeline_lanes.sh +2011 -0
- package/scripts/test_gate_shape_scan_lanes.sh +47 -0
- package/scripts/test_heavy_classifier_lanes.sh +13 -3
- package/scripts/test_marker_crossfamily_lanes.sh +75 -3
- package/scripts/test_marker_standpoint_lanes.sh +31 -6
- package/scripts/test_preprep_font_lanes.sh +87 -0
- package/templates/.git-hooks/pre-commit +158 -11
- package/templates/PRE-PUBLISH-CHECKLIST.md +29 -0
- package/templates/local_fh_context.md +1 -1
- package/templates/regression_guard.sh +1 -1
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# finding_fleet.sh — run a parallel, multi-family review fleet over one file and emit TYPED findings.
|
|
3
|
+
#
|
|
4
|
+
# WHY. FH's review output has always been prose, so nothing downstream could count it, filter it, or
|
|
5
|
+
# hand it to a second opinion. Measured 2026-09-08 over eight GHSA cases x3: FH made 52 claims with 5
|
|
6
|
+
# wrong about the code (9.6%); a sibling harness made 73 -- 40% more -- with 2 wrong (2.7%). The gap
|
|
7
|
+
# was not reading quality. That harness emits findings as typed records, has a different family stamp
|
|
8
|
+
# each one, and deletes the false positives with a filter. This script is the first half of that shape
|
|
9
|
+
# for FH: fan out to several families in parallel, each returning JSONL. The second half, the reject
|
|
10
|
+
# stage, is scripts/finding_verify.py, and it refuses to let a family verify its own findings.
|
|
11
|
+
#
|
|
12
|
+
# WHAT IS MECHANIZED: that each finding carries the family and role that produced it, that the members
|
|
13
|
+
# run in parallel and independently, and that a member which fails is recorded as failed rather than
|
|
14
|
+
# quietly contributing nothing. WHAT IS NOT: what counts as a defect. No rule about findings lives here.
|
|
15
|
+
#
|
|
16
|
+
# FLEET TABLE. One member per line, `family|role|command`. The command receives the review prompt BOTH
|
|
17
|
+
# on stdin and as a file whose path replaces the token PROMPT_FILE in the command — some CLIs take the
|
|
18
|
+
# prompt as an argument and their -p flag is variadic, so piping it silently turns the next flag into
|
|
19
|
+
# the prompt (measured 2026-09-08: `agy -p --model X` sent "--model" as the prompt and reported it).
|
|
20
|
+
# Must print JSONL findings on stdout. Override with --fleet <file>; the default is two external
|
|
21
|
+
# families so that neither is the Claude governor calling this script.
|
|
22
|
+
#
|
|
23
|
+
# Usage: bash scripts/finding_fleet.sh <target-file> --out <dir> [--fleet <table>] [--roles-only]
|
|
24
|
+
# bash scripts/finding_fleet.sh --selftest
|
|
25
|
+
# Exit: 0 = at least one member returned findings · 1 = all members failed (nothing was reviewed)
|
|
26
|
+
# 2 = usage error
|
|
27
|
+
set -uo pipefail
|
|
28
|
+
export LC_ALL=C
|
|
29
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
30
|
+
# Binaries are resolved from PATH, then from the usual install roots. Never hard-code a home path:
|
|
31
|
+
# this file ships in the npm package and a literal home directory is both a leak and wrong on the
|
|
32
|
+
# consumer's machine. Override with FH_CODEX_BIN / FH_AGY_BIN.
|
|
33
|
+
CODEX="${FH_CODEX_BIN:-$(command -v codex 2>/dev/null || echo "$HOME/.npm-global/bin/codex")}"
|
|
34
|
+
AGY="${FH_AGY_BIN:-$(command -v agy 2>/dev/null || echo "$HOME/.local/bin/agy")}"
|
|
35
|
+
|
|
36
|
+
default_fleet() {
|
|
37
|
+
cat <<'EOF'
|
|
38
|
+
codex|logic|CODEX_BIN exec --sandbox read-only --skip-git-repo-check -m gpt-6-astra -c model_reasoning_effort="high"
|
|
39
|
+
gemini|security|AGY_BIN --mode plan --sandbox --model gemini-3.8-flash-high --output-format text --print-timeout 20m -p "$(cat PROMPT_FILE)"
|
|
40
|
+
EOF
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
PROMPT_HEAD='You are one reviewer in a parallel fleet. Review the file below for defects in your assigned area.
|
|
44
|
+
|
|
45
|
+
Output ONLY JSON Lines, one object per finding, nothing else — no prose, no code fences:
|
|
46
|
+
{"title":"<short claim>","file":"<name>","line":<int>,"severity":"S|A|B","category":"<one word>","detail":"<what goes wrong and when>","defeater":"<what would be OBSERVED if this claim were wrong>","confidence":<0.0-1.0>}
|
|
47
|
+
|
|
48
|
+
S = exploitable or fail-open. A = real but non-blocking. B = minor.
|
|
49
|
+
`defeater` is required and must name something observable — a value, an output, a code path that
|
|
50
|
+
would be reached — not "if I misread it". A claim whose own falsification condition cannot be stated
|
|
51
|
+
is a claim you are not yet entitled to make.
|
|
52
|
+
Report only defects you can point to a specific line for. If you find none, output nothing.
|
|
53
|
+
|
|
54
|
+
Your assigned area: '
|
|
55
|
+
|
|
56
|
+
PROMPT_HEAD_R2='You reviewed this file already. Another reviewer, from a DIFFERENT model family,
|
|
57
|
+
reviewed the same file independently. Both lists are below.
|
|
58
|
+
|
|
59
|
+
🟥 THIS IS NOT AN ACCEPT/REJECT PASS. You are not being asked to approve or delete the other reviewer findings.
|
|
60
|
+
You are being asked to produce YOUR OWN final list, having now seen theirs.
|
|
61
|
+
|
|
62
|
+
Do all of these that apply:
|
|
63
|
+
- KEEP each of your findings that still holds. Do not drop one merely because the other reviewer
|
|
64
|
+
did not report it — they had a different assigned area.
|
|
65
|
+
- CORRECT any of yours that their list shows to be wrong, imprecise, or on the wrong line.
|
|
66
|
+
- SHARPEN a finding whose `defeater` you can now state more concretely.
|
|
67
|
+
- ADD findings you did not make the first time, including ones their list made you look again at.
|
|
68
|
+
A finding of theirs that you can now independently confirm against the source IS yours to state —
|
|
69
|
+
state it in your own words with your own defeater.
|
|
70
|
+
- If you now believe one of YOUR earlier findings was wrong, drop it AND say so in a final line
|
|
71
|
+
beginning `DROPPED:` naming the title and why. That line is prose and is expected.
|
|
72
|
+
|
|
73
|
+
Output ONLY JSON Lines in the same schema as before, one object per finding, then optionally the
|
|
74
|
+
DROPPED: lines. Your assigned area is unchanged: '
|
|
75
|
+
# F_r2ctrl(컨트롤) 머리 — 위와 같은 구조(KEEP/CORRECT/SHARPEN/ADD/DROPPED)에서 «다른 검토자» 언급만 뺐다.
|
|
76
|
+
PROMPT_HEAD_R2_BLIND='You reviewed this file already. Your own round-1 list is below.
|
|
77
|
+
|
|
78
|
+
🟥 THIS IS NOT AN ACCEPT/REJECT PASS. You are being asked to produce YOUR OWN final list on a second,
|
|
79
|
+
independent look at the same file.
|
|
80
|
+
|
|
81
|
+
Do all of these that apply:
|
|
82
|
+
- KEEP each of your findings that still holds.
|
|
83
|
+
- CORRECT any of yours that a second reading shows to be wrong, imprecise, or on the wrong line.
|
|
84
|
+
- SHARPEN a finding whose `defeater` you can now state more concretely.
|
|
85
|
+
- ADD findings you did not make the first time.
|
|
86
|
+
- If you now believe one of YOUR earlier findings was wrong, drop it AND say so in a final line
|
|
87
|
+
beginning `DROPPED:` naming the title and why. That line is prose and is expected.
|
|
88
|
+
|
|
89
|
+
Output ONLY JSON Lines in the same schema as before, one object per finding, then optionally the
|
|
90
|
+
DROPPED: lines. Your assigned area is unchanged: '
|
|
91
|
+
|
|
92
|
+
run_member_r2() { # $1=family $2=role $3=command $4=target $5=outdir
|
|
93
|
+
local fam="$1" role="$2" cmd="$3" tgt="$4" out="$5"
|
|
94
|
+
local q_codex q_agy q_pf
|
|
95
|
+
q_codex="$(printf '%q' "$CODEX")"; q_agy="$(printf '%q' "$AGY")"
|
|
96
|
+
cmd="${cmd//CODEX_BIN/$q_codex}"; cmd="${cmd//AGY_BIN/$q_agy}"
|
|
97
|
+
local raw="$out/raw2_${fam}_${role}.txt" pf="$out/prompt2_${fam}_${role}.txt"
|
|
98
|
+
for _p in "$raw" "$pf" "$out/err2_${fam}_${role}.txt" "$out/part2_${fam}_${role}.jsonl" "$out/peer_${fam}_${role}.txt"; do
|
|
99
|
+
[ -L "$_p" ] && { echo "finding_fleet: refusing to write through a symlink: $_p" >&2; return 1; }
|
|
100
|
+
done
|
|
101
|
+
# 자기 것과 «남의 것» 을 갈라서 보여준다 — 어느 쪽이 자기 것인지 모르면 수정할 수가 없다
|
|
102
|
+
# 🟥 R4 #8: «mine» is the MEMBER (family+role), not the family — two roles of one family were each
|
|
103
|
+
# told the other's findings were their own. R4 #5: own rows carry `id` so a kept/revised finding
|
|
104
|
+
# keeps its identity through round 2 (a declared seed otherwise turns ABSENT while surviving).
|
|
105
|
+
MINE_FAM="$fam" MINE_ROLE="$role" R2_BLIND="${ROUND2_BLIND:-0}" /usr/bin/python3 -c '
|
|
106
|
+
import sys, json, os
|
|
107
|
+
mine, kin, theirs = [], [], []
|
|
108
|
+
fam, role = os.environ["MINE_FAM"], os.environ["MINE_ROLE"]
|
|
109
|
+
blind = os.environ.get("R2_BLIND") == "1"
|
|
110
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
111
|
+
l = l.strip()
|
|
112
|
+
if not l: continue
|
|
113
|
+
try: d = json.loads(l)
|
|
114
|
+
except Exception: continue
|
|
115
|
+
row = {k: d.get(k) for k in ("title","file","line","severity","category","detail","defeater")}
|
|
116
|
+
if d.get("producer_family") == fam and d.get("producer_role") == role:
|
|
117
|
+
mine.append(dict(id=d.get("id"), **row))
|
|
118
|
+
elif d.get("producer_family") == fam:
|
|
119
|
+
kin.append(row)
|
|
120
|
+
else:
|
|
121
|
+
theirs.append(row)
|
|
122
|
+
with open(sys.argv[2], "w", encoding="utf-8") as w:
|
|
123
|
+
w.write("\n===== YOUR OWN ROUND-1 FINDINGS (keep the id field verbatim on any you KEEP or CORRECT) =====\n")
|
|
124
|
+
for r in mine: w.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
125
|
+
if not mine: w.write("(you reported none)\n")
|
|
126
|
+
if kin:
|
|
127
|
+
w.write("\n===== OTHER MEMBERS OF YOUR OWN FAMILY (not yours — do not keep/drop these) =====\n")
|
|
128
|
+
for r in kin: w.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
129
|
+
if blind:
|
|
130
|
+
# F_r2ctrl 컨트롤: 상대 목록을 «감춘다». 절 자체를 안 쓴다 — «(withheld)» 라고 쓰면 모델이 그 부재를 추론 재료로 쓴다
|
|
131
|
+
pass
|
|
132
|
+
else:
|
|
133
|
+
w.write("\n===== THE OTHER FAMILY ROUND-1 FINDINGS =====\n")
|
|
134
|
+
for r in theirs: w.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
135
|
+
if not theirs: w.write("(they reported none)\n")
|
|
136
|
+
' "$out/findings_r1.jsonl" "$out/peer_${fam}_${role}.txt" || return 1 # R3 #9: per-member — two roles of one family raced on a shared truncate/write
|
|
137
|
+
local head_r2="$PROMPT_HEAD_R2"; [ "${ROUND2_BLIND:-0}" = 1 ] && head_r2="$PROMPT_HEAD_R2_BLIND"
|
|
138
|
+
{ printf '%s%s\n\n===== FILE: %s =====\n' "$head_r2" "$role" "$(basename "$tgt")"
|
|
139
|
+
cat "$tgt"; cat "$out/peer_${fam}_${role}.txt"; } > "$pf"
|
|
140
|
+
q_pf="$(printf '%q' "$pf")"
|
|
141
|
+
cmd="${cmd//PROMPT_FILE/$q_pf}"
|
|
142
|
+
eval "$cmd" < "$pf" > "$raw" 2>"$out/err2_${fam}_${role}.txt"
|
|
143
|
+
local rc=$?
|
|
144
|
+
FAM="$fam" ROLE="$role" RC="$rc" /usr/bin/python3 - "$raw" "$out/part2_${fam}_${role}.jsonl" "$out/findings_r1.jsonl" <<'PYR2'
|
|
145
|
+
import json, os, sys
|
|
146
|
+
own_r1 = {} # own round-1 id -> its trusted member_id alias (or None)
|
|
147
|
+
try:
|
|
148
|
+
for _l in open(sys.argv[3], encoding="utf-8"):
|
|
149
|
+
try: _d = json.loads(_l)
|
|
150
|
+
except Exception: continue
|
|
151
|
+
if _d.get("producer_family") == os.environ["FAM"] and _d.get("producer_role") == os.environ["ROLE"] and _d.get("id") is not None:
|
|
152
|
+
own_r1[str(_d["id"])] = _d.get("member_id")
|
|
153
|
+
except OSError:
|
|
154
|
+
pass
|
|
155
|
+
own_r1_ids = set(own_r1)
|
|
156
|
+
own_r1_titles = {} # R8 #1: id -> title, so a DROPPED line citing the title (not the id) still accounts for it
|
|
157
|
+
try:
|
|
158
|
+
for _l in open(sys.argv[3], encoding="utf-8"):
|
|
159
|
+
try: _d = json.loads(_l)
|
|
160
|
+
except Exception: continue
|
|
161
|
+
if _d.get("producer_family") == os.environ["FAM"] and _d.get("producer_role") == os.environ["ROLE"] and _d.get("id") is not None:
|
|
162
|
+
own_r1_titles[str(_d["id"])] = str(_d.get("title") or "")
|
|
163
|
+
except OSError:
|
|
164
|
+
pass
|
|
165
|
+
withdrawn_ids = set()
|
|
166
|
+
all_ids = set() # R6 #2: every round-1 id of every member — the namespace new r2 ids must avoid
|
|
167
|
+
try:
|
|
168
|
+
for _l in open(sys.argv[3], encoding="utf-8"):
|
|
169
|
+
try: _d = json.loads(_l)
|
|
170
|
+
except Exception: continue
|
|
171
|
+
if isinstance(_d, dict) and _d.get("id") is not None:
|
|
172
|
+
all_ids.add(str(_d["id"]))
|
|
173
|
+
except OSError:
|
|
174
|
+
pass
|
|
175
|
+
minted = set()
|
|
176
|
+
kept_ids = set()
|
|
177
|
+
resp_titles = []
|
|
178
|
+
fam, role, rc = os.environ["FAM"], os.environ["ROLE"], os.environ["RC"]
|
|
179
|
+
src, dst = sys.argv[1], sys.argv[2]
|
|
180
|
+
n = 0
|
|
181
|
+
saw_bytes = False
|
|
182
|
+
bad_json = False
|
|
183
|
+
dropped = []
|
|
184
|
+
with open(dst, "w", encoding="utf-8") as w:
|
|
185
|
+
for line in open(src, encoding="utf-8", errors="replace"):
|
|
186
|
+
line = line.strip().lstrip("\ufeff")
|
|
187
|
+
if line.startswith("DROPPED:"):
|
|
188
|
+
_body = line[len("DROPPED:"):]
|
|
189
|
+
for _id, _t in own_r1_titles.items():
|
|
190
|
+
if _id in _body or (_t and _t in _body):
|
|
191
|
+
withdrawn_ids.add(_id)
|
|
192
|
+
# 🟥 saw_bytes 를 여기서 «올리지 않는다». DROPPED 는 2차 계약이 요구한 출력이므로
|
|
193
|
+
# 「계약 밖 응답」의 증거가 될 수 없다 (cross-family #5, codex 2026-09-10).
|
|
194
|
+
# 🟥 "DROPPED: none" 은 삭제가 아니다. 세면 «숨은 수확 손실» 계측이 그 자리에서 거짓이 된다.
|
|
195
|
+
body = line[len("DROPPED:"):].strip().strip("().").lower()
|
|
196
|
+
if body not in ("", "none", "nothing", "n/a", "na", "없음"):
|
|
197
|
+
dropped.append(line)
|
|
198
|
+
continue
|
|
199
|
+
if line:
|
|
200
|
+
saw_bytes = True
|
|
201
|
+
if not line.startswith("{"):
|
|
202
|
+
# R7 #1: a line that PARSES as JSON but is not an object (`[…]`, `null`, `true`, `42`) is a malformed
|
|
203
|
+
# survivor — skipping it as chatter let an array of findings vanish and a `null` certify «all withdrawn».
|
|
204
|
+
if line[:1] in "[ntf0123456789-\"": # R8 #2: a double-encoded finding (JSON string) is a malformed survivor too
|
|
205
|
+
try:
|
|
206
|
+
_v = json.loads(line)
|
|
207
|
+
if not isinstance(_v, dict):
|
|
208
|
+
bad_json = True
|
|
209
|
+
except json.JSONDecodeError:
|
|
210
|
+
pass
|
|
211
|
+
continue
|
|
212
|
+
try:
|
|
213
|
+
d = json.loads(line)
|
|
214
|
+
except json.JSONDecodeError:
|
|
215
|
+
bad_json = True # R5 #2: a survivor that failed to parse is evidence, not silence
|
|
216
|
+
continue
|
|
217
|
+
if not isinstance(d, dict) or not d.get("title"):
|
|
218
|
+
bad_json = True # R6 #1: a schema-invalid finding object is a malformed survivor, not silence
|
|
219
|
+
continue
|
|
220
|
+
n += 1
|
|
221
|
+
resp_titles.append(str(d.get("title") or ""))
|
|
222
|
+
# R4 #5: a KEPT/CORRECTED finding keeps its round-1 id (identity survives the rewrite);
|
|
223
|
+
# anything else gets a fresh routing id. Only THIS member's own r1 ids are honoured.
|
|
224
|
+
# R5 #1: the alias (member_id) comes from the TRUSTED round-1 row, never from the response —
|
|
225
|
+
# a rejected foreign routing id must not survive as a seed-matchable alias.
|
|
226
|
+
d.pop("member_id", None)
|
|
227
|
+
if str(d.get("id")) in own_r1_ids:
|
|
228
|
+
d["id"] = str(d["id"])
|
|
229
|
+
if d["id"] in kept_ids:
|
|
230
|
+
bad_json = True # R7 #2: the same round-1 id KEPT twice — two rows, one identity → invalid response
|
|
231
|
+
continue
|
|
232
|
+
kept_ids.add(d["id"])
|
|
233
|
+
if own_r1[d["id"]] is not None:
|
|
234
|
+
d["member_id"] = own_r1[d["id"]]
|
|
235
|
+
else:
|
|
236
|
+
# R6 #2: allocate against the COMPLETE existing id set — role «logic-r2» owns r1 id
|
|
237
|
+
# `codex-logic-r2-1`, which is exactly what role «logic» would mint here.
|
|
238
|
+
k = n
|
|
239
|
+
cand = f"{fam}-{role}-r2-{k}"
|
|
240
|
+
while cand in all_ids or cand in minted:
|
|
241
|
+
k += 1
|
|
242
|
+
cand = f"{fam}-{role}-r2-{k}"
|
|
243
|
+
d["id"] = cand
|
|
244
|
+
minted.add(cand)
|
|
245
|
+
d["producer_family"] = fam
|
|
246
|
+
d["producer_role"] = role
|
|
247
|
+
d["round"] = 2
|
|
248
|
+
w.write(json.dumps(d, ensure_ascii=False) + "\n")
|
|
249
|
+
# 🟥 R8 #1: 응답이 «성공» 이려면 자기 1차 발견 전부가 설명돼야 한다 — KEEP(id) 되거나 DROPPED 가 id/제목을 부르거나.
|
|
250
|
+
# «A 만 KEEP» 이 B 를 조용히 지웠고, «DROPPED: B» 한 줄이 A 없이 «전원 철회» 를 냈다. 설명 안 된 1차가 남으면
|
|
251
|
+
# 불완전 응답 → 폴백(ZERO_NONJSON 과 같은 처분). 새 발견(ADD)은 설명 대상이 아니다.
|
|
252
|
+
def _accounted(i):
|
|
253
|
+
if i in kept_ids or i in withdrawn_ids:
|
|
254
|
+
return True
|
|
255
|
+
t = own_r1_titles.get(i, "")
|
|
256
|
+
# 느슨한 설명: 응답 발견의 제목이 1차 제목을 잇거나(«G1» → «G1 revised») 그 반대 — id 를 안 실은 CORRECT 형태
|
|
257
|
+
return bool(t) and any((rt.startswith(t) or t.startswith(rt)) and len(rt) >= 2 for rt in resp_titles)
|
|
258
|
+
unaccounted = sorted(i for i in own_r1_ids if not _accounted(i))
|
|
259
|
+
if rc == "0" and not bad_json and unaccounted and (n > 0 or dropped):
|
|
260
|
+
bad_json = True
|
|
261
|
+
print(f"FLEET round2 note member={fam}/{role} unaccounted_r1={','.join(unaccounted)} — incomplete response, falling back")
|
|
262
|
+
# 🟥 «전부 스스로 내렸다» 는 정당한 2차 결과이고 «차단» 이 아니다 — 별 값을 준다.
|
|
263
|
+
# R6 #1: a response with ANY malformed survivor is not a trustworthy replacement of the member's round-1 list —
|
|
264
|
+
# `n > 0` used to win over bad_json, so «valid A + broken B» silently deleted B. bad_json outranks OK.
|
|
265
|
+
status = ("FAILED" if rc != "0"
|
|
266
|
+
else "ZERO_NONJSON" if bad_json # R5 #2 / R6 #1: malformed survivor → fallback, never OK nor «all withdrawn»
|
|
267
|
+
else "OK" if n > 0
|
|
268
|
+
else "ZERO_SELFDROPPED" if dropped
|
|
269
|
+
else "ZERO_NONJSON" if saw_bytes else "ZERO_EMPTY")
|
|
270
|
+
print(f"MEMBER2 family={fam} role={role} rc={rc} findings={n} self_dropped={len(dropped)} status={status}")
|
|
271
|
+
for l in dropped:
|
|
272
|
+
print(" " + l[:200])
|
|
273
|
+
PYR2
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
run_member() { # $1=family $2=role $3=command $4=target $5=outdir
|
|
277
|
+
local fam="$1" role="$2" cmd="$3" tgt="$4" out="$5"
|
|
278
|
+
# 🟥 치환값은 eval 되는 문자열 «안»으로 들어간다. 결박하지 않으면 «출력 디렉터리 이름»만으로도
|
|
279
|
+
# 명령이 실행된다 — cross-family security review 2026-09-09 가 무해한 printf 로 실증했다.
|
|
280
|
+
# 여기는 bash `eval` 이므로 `printf %q` 가 옳은 도구다(shell=True 로 /bin/sh 에 넘기는
|
|
281
|
+
# finding_pipeline.sh 와는 사정이 다르다 — 거기서는 %q 가 dash 에서 깨져서 argv 로 갔다).
|
|
282
|
+
local q_codex q_agy q_pf
|
|
283
|
+
q_codex="$(printf '%q' "$CODEX")"; q_agy="$(printf '%q' "$AGY")"
|
|
284
|
+
cmd="${cmd//CODEX_BIN/$q_codex}"; cmd="${cmd//AGY_BIN/$q_agy}"
|
|
285
|
+
local raw="$out/raw_${fam}_${role}.txt" pf="$out/prompt_${fam}_${role}.txt"
|
|
286
|
+
# 🟥 심링크를 통해 쓰면 남의 파일을 덮는다. 그리고 타깃이 심링크면 그 «내용»이 외부로 나간다.
|
|
287
|
+
for _p in "$raw" "$pf" "$out/err_${fam}_${role}.txt" "$out/part_${fam}_${role}.jsonl"; do
|
|
288
|
+
[ -L "$_p" ] && { echo "finding_fleet: refusing to write through a symlink: $_p" >&2; return 1; }
|
|
289
|
+
done
|
|
290
|
+
{ printf '%s%s\n\n===== FILE: %s =====\n' "$PROMPT_HEAD" "$role" "$(basename "$tgt")"; cat "$tgt"; } > "$pf"
|
|
291
|
+
q_pf="$(printf '%q' "$pf")"
|
|
292
|
+
cmd="${cmd//PROMPT_FILE/$q_pf}"
|
|
293
|
+
eval "$cmd" < "$pf" > "$raw" 2>"$out/err_${fam}_${role}.txt"
|
|
294
|
+
local rc=$?
|
|
295
|
+
FAM="$fam" ROLE="$role" RC="$rc" /usr/bin/python3 - "$raw" "$out/part_${fam}_${role}.jsonl" <<'PY'
|
|
296
|
+
import json, os, sys
|
|
297
|
+
fam, role, rc = os.environ["FAM"], os.environ["ROLE"], os.environ["RC"]
|
|
298
|
+
src, dst = sys.argv[1], sys.argv[2]
|
|
299
|
+
n = 0
|
|
300
|
+
saw_bytes = False
|
|
301
|
+
with open(dst, "w", encoding="utf-8") as w:
|
|
302
|
+
for line in open(src, encoding="utf-8", errors="replace"):
|
|
303
|
+
line = line.strip().lstrip("")
|
|
304
|
+
if line:
|
|
305
|
+
saw_bytes = True # 계열이 «무언가» 를 말했다는 사실은 findings 와 별개다
|
|
306
|
+
if not line.startswith("{"):
|
|
307
|
+
continue # tolerate banners and fences around the JSONL
|
|
308
|
+
try:
|
|
309
|
+
d = json.loads(line)
|
|
310
|
+
except json.JSONDecodeError:
|
|
311
|
+
continue
|
|
312
|
+
if not d.get("title"):
|
|
313
|
+
continue
|
|
314
|
+
n += 1
|
|
315
|
+
# 🟥 The member's own id is PRESERVED before renumbering. The fleet assigns a routing id
|
|
316
|
+
# (`family-role-n`) because member ids collide across members — but overwriting the original
|
|
317
|
+
# made the seeded control unusable end to end: a caller cannot declare `--seeded <id>` for an
|
|
318
|
+
# id that does not exist until after the run. Keeping the member id lets the control be
|
|
319
|
+
# declared in the caller's own vocabulary. (Found while writing the end-to-end lane; no
|
|
320
|
+
# review round named it — the lane did.)
|
|
321
|
+
# R6 #4: an incoming `member_id` is never trusted — the alias is derived ONLY from the response's own
|
|
322
|
+
# `id`. Without this, a response carrying member_id="seed-never-emitted" (and no id) kept that
|
|
323
|
+
# alias, and R5's «trusted round-1 alias» then carried it into round 2.
|
|
324
|
+
d.pop("member_id", None)
|
|
325
|
+
if d.get("id") is not None:
|
|
326
|
+
d["member_id"] = str(d["id"])
|
|
327
|
+
d["id"] = f"{fam}-{role}-{n}"
|
|
328
|
+
d["producer_family"] = fam
|
|
329
|
+
d["producer_role"] = role
|
|
330
|
+
w.write(json.dumps(d, ensure_ascii=False) + "\n")
|
|
331
|
+
# 🟥 «n==0» 은 «발견 0» 이 아니다. 이유를 단정하지 않고 «채널이 보여주는 것» 만 타입으로 적는다.
|
|
332
|
+
# 실측 2026-09-10 (F_typed case_g02.py r1~r3): gemini 가 안전필터 «차단» 문구를 냈는데 CLI 는
|
|
333
|
+
# rc=0 이었고, 파서는 JSON 이 없어 findings=0 으로 기록했다. 그 계열이 producer 목록에서 사라져
|
|
334
|
+
# 드라이버가 «다른 계열 없음» 으로 읽고 codex 발견 2건을 통째로 미검증 처리했다(coverage 0/2).
|
|
335
|
+
if rc != "0":
|
|
336
|
+
status = "FAILED" # CLI 자체가 실패 (한도 소진 등)
|
|
337
|
+
elif n > 0:
|
|
338
|
+
status = "OK"
|
|
339
|
+
elif saw_bytes:
|
|
340
|
+
status = "ZERO_NONJSON" # 응답은 «있는데» 계약(JSONL) 이 하나도 없다 — 검증 없는 0
|
|
341
|
+
else:
|
|
342
|
+
status = "ZERO_EMPTY" # 출력이 통째로 비었다
|
|
343
|
+
print(f"MEMBER family={fam} role={role} rc={rc} findings={n} status={status}")
|
|
344
|
+
PY
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
selftest() {
|
|
348
|
+
local d fails; d="$(mktemp -d 2>/dev/null)" || d=""
|
|
349
|
+
[ -n "$d" ] && [ -w "$d" ] || { echo "SELFTEST: ENV-BLOCKED (mktemp -d failed) — unmeasured, not a pass"; return 3; }
|
|
350
|
+
printf 'def f(x):\n return x\n' > "$d/t.py"
|
|
351
|
+
local fails=0
|
|
352
|
+
# Stub members are real scripts: they must consume stdin (a member that ignores it dies of SIGPIPE
|
|
353
|
+
# on a large file, which is a property of the harness, not of the member) and then print.
|
|
354
|
+
cat > "$d/m_ok.sh" <<'EOS'
|
|
355
|
+
#!/bin/sh
|
|
356
|
+
cat >/dev/null
|
|
357
|
+
echo '{"title":"t","file":"t.py","line":1,"severity":"B","confidence":0.5}'
|
|
358
|
+
EOS
|
|
359
|
+
cat > "$d/m_prose.sh" <<'EOS'
|
|
360
|
+
#!/bin/sh
|
|
361
|
+
cat >/dev/null
|
|
362
|
+
echo "I reviewed it and it looks fine to me."
|
|
363
|
+
EOS
|
|
364
|
+
cat > "$d/m_fail.sh" <<'EOS'
|
|
365
|
+
#!/bin/sh
|
|
366
|
+
cat >/dev/null
|
|
367
|
+
exit 1
|
|
368
|
+
EOS
|
|
369
|
+
chmod +x "$d/m_ok.sh" "$d/m_prose.sh" "$d/m_fail.sh"
|
|
370
|
+
printf 'stub|logic|sh %s\n' "$d/m_ok.sh" > "$d/fleet"
|
|
371
|
+
bash "$0" "$d/t.py" --out "$d/o1" --fleet "$d/fleet" >/dev/null 2>&1
|
|
372
|
+
if [ -s "$d/o1/findings.jsonl" ] && /usr/bin/grep -q '"producer_family": "stub"' "$d/o1/findings.jsonl"; then
|
|
373
|
+
echo " ✅ known-positive: a member's JSONL survives and is tagged with its family"
|
|
374
|
+
else echo " ❌ known-positive parse/tag"; fails=1; fi
|
|
375
|
+
printf 'proser|logic|sh %s\n' "$d/m_prose.sh" > "$d/fleet2"
|
|
376
|
+
bash "$0" "$d/t.py" --out "$d/o2" --fleet "$d/fleet2" >/dev/null 2>&1
|
|
377
|
+
if [ ! -s "$d/o2/findings.jsonl" ]; then echo " ✅ known-negative: prose contributes no findings"
|
|
378
|
+
else echo " ❌ known-negative: prose leaked into findings"; fails=1; fi
|
|
379
|
+
printf 'faily|logic|sh %s\n' "$d/m_fail.sh" > "$d/fleet3"
|
|
380
|
+
bash "$0" "$d/t.py" --out "$d/o3" --fleet "$d/fleet3" >/dev/null 2>&1
|
|
381
|
+
if /usr/bin/grep -q 'rc=1' "$d/o3/members.txt" 2>/dev/null; then echo " ✅ failing member recorded with its exit code"
|
|
382
|
+
else echo " ❌ failing member not recorded"; fails=1; fi
|
|
383
|
+
if [ ! -s "$d/o3/findings.jsonl" ] && ! bash "$0" "$d/t.py" --out "$d/o4" --fleet "$d/fleet3" >/dev/null 2>&1; then
|
|
384
|
+
echo " ✅ all-members-failed exits non-zero (empty list must not read as clean)"
|
|
385
|
+
else echo " ❌ all-members-failed did not exit non-zero"; fails=1; fi
|
|
386
|
+
/bin/rm -rf "$d"
|
|
387
|
+
[ "$fails" -eq 0 ] && { echo "SELFTEST: PASS"; return 0; } || { echo "SELFTEST: FAIL"; return 1; }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
[ $# -ge 1 ] || { echo "usage: $0 <target-file> --out <dir> [--fleet <table>] [--round2|--round2-blind] [--r1-only] [--reuse-r1 <dir>] | --selftest" >&2; exit 2; }
|
|
391
|
+
[ "$1" = "--selftest" ] && { selftest; exit $?; }
|
|
392
|
+
TARGET="$1"; shift
|
|
393
|
+
OUT=""; FLEET=""; ROUND2=0; ROUND2_BLIND=0; R1_ONLY=0; REUSE_R1=""
|
|
394
|
+
while [ $# -gt 0 ]; do
|
|
395
|
+
case "$1" in
|
|
396
|
+
--out) OUT="${2:-}"; shift 2 ;;
|
|
397
|
+
--fleet) FLEET="${2:-}"; shift 2 ;;
|
|
398
|
+
--round2) ROUND2=1; shift ;;
|
|
399
|
+
--round2-blind) ROUND2=1; ROUND2_BLIND=1; shift ;; # F_r2ctrl: 2차 패스는 돌리되 상대 계열 목록을 «감춘다» — «탈상관 효과» 와 «두 번 본 효과» 를 가른다
|
|
400
|
+
# ── 짝지음(paired) 설계 (F_pair, 2026-09-12) ────────────────────────────────
|
|
401
|
+
# 🟥 왜 있나: F_gen 과 F_r2ctrl 을 «독립 두 팔» 로 돌렸더니 round-1 기저가 146 vs 136 (7.4 %)
|
|
402
|
+
# 으로 이미 달랐다. 블라인드는 round-2 에만 걸렸는데 round-1 부터 다른 실행이라,
|
|
403
|
+
# r1→r2 증가율 비교가 «한 변수 비교» 가 아니었다 (RESULT §15 최대 잔여). 그래서:
|
|
404
|
+
# round-1 을 **한 번** 돌려 두고(--r1-only), 그 **같은** r1 에서 round-2 를 두 번 가른다
|
|
405
|
+
# (--reuse-r1 <r1dir> --round2 / --reuse-r1 <r1dir> --round2-blind).
|
|
406
|
+
# 부수 효과로 더 싸다 — r1 을 두 번 안 돌린다.
|
|
407
|
+
--r1-only) R1_ONLY=1; shift ;;
|
|
408
|
+
--reuse-r1) REUSE_R1="${2:-}"; shift 2 ;;
|
|
409
|
+
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
410
|
+
esac
|
|
411
|
+
done
|
|
412
|
+
[ -f "$TARGET" ] || { echo "no such file: $TARGET" >&2; exit 2; }
|
|
413
|
+
# 🟥 이 파일의 «내용»은 외부 모델로 전송된다. 심링크면 체크아웃 밖 자격증명을 가리킬 수 있고,
|
|
414
|
+
# 그러면 리뷰 대상 대신 그 비밀이 프롬프트가 된다 (residency 위반, cross-family 2026-09-09).
|
|
415
|
+
[ -L "$TARGET" ] && { echo "finding_fleet: target is a symlink — refusing (content is SENT to an external model)" >&2; exit 2; }
|
|
416
|
+
[ -r "$TARGET" ] || { echo "finding_fleet: target is not readable: $TARGET" >&2; exit 2; }
|
|
417
|
+
# 프롬프트 파일은 소스 전문을 담는다 — umask 022 면 0644 로 남아 다른 계정이 읽는다.
|
|
418
|
+
umask 077
|
|
419
|
+
[ -n "$OUT" ] || { echo "--out is required" >&2; exit 2; }
|
|
420
|
+
|
|
421
|
+
# ── 짝지음 플래그 상호 배타 검사 (fail-closed: 모순 조합은 조용히 한쪽을 고르지 않고 거부) ──
|
|
422
|
+
[ "$R1_ONLY" -eq 1 ] && [ -n "$REUSE_R1" ] && { echo "finding_fleet: --r1-only and --reuse-r1 are mutually exclusive" >&2; exit 2; }
|
|
423
|
+
[ "$R1_ONLY" -eq 1 ] && [ "$ROUND2" -eq 1 ] && { echo "finding_fleet: --r1-only cannot be combined with --round2/--round2-blind" >&2; exit 2; }
|
|
424
|
+
[ -n "$REUSE_R1" ] && [ "$ROUND2" -eq 0 ] && { echo "finding_fleet: --reuse-r1 requires --round2 or --round2-blind (otherwise it would do nothing)" >&2; exit 2; }
|
|
425
|
+
|
|
426
|
+
mkdir -p "$OUT"
|
|
427
|
+
|
|
428
|
+
if [ -n "$REUSE_R1" ]; then
|
|
429
|
+
# 공유 round-1 을 이 OUT 으로 씨앗 놓는다. round-1 멤버는 **돌리지 않는다**.
|
|
430
|
+
[ -d "$REUSE_R1" ] || { echo "finding_fleet: --reuse-r1 dir not found: $REUSE_R1" >&2; exit 2; }
|
|
431
|
+
_src="$(cd "$REUSE_R1" && pwd -P)"; _dst="$(cd "$OUT" && pwd -P)"
|
|
432
|
+
[ "$_src" = "$_dst" ] && { echo "finding_fleet: --reuse-r1 dir is the same as --out — refusing (would overwrite its own source)" >&2; exit 2; }
|
|
433
|
+
[ -s "$REUSE_R1/findings.jsonl" ] || { echo "finding_fleet: --reuse-r1 source has no non-empty findings.jsonl — an empty round-1 must not be reused as if measured" >&2; exit 2; }
|
|
434
|
+
[ -f "$REUSE_R1/fleet.txt" ] || { echo "finding_fleet: --reuse-r1 source has no fleet.txt — the roster must be identical across paired branches" >&2; exit 2; }
|
|
435
|
+
# 로스터가 갈리면 두 분기가 «같은 r1» 을 쓰는 의미가 사라진다 — 명시된 --fleet 와 불일치면 거부
|
|
436
|
+
if [ -n "$FLEET" ] && ! /usr/bin/cmp -s "$FLEET" "$REUSE_R1/fleet.txt"; then
|
|
437
|
+
echo "finding_fleet: --fleet differs from the reused r1 roster ($REUSE_R1/fleet.txt) — refusing (paired branches must share one roster)" >&2; exit 2
|
|
438
|
+
fi
|
|
439
|
+
set -- "$REUSE_R1"/part_*.jsonl
|
|
440
|
+
[ -e "$1" ] || { echo "finding_fleet: --reuse-r1 source has no part_*.jsonl (per-member round-1 output) — the per-member fallback would silently see nothing" >&2; exit 2; }
|
|
441
|
+
cp "$REUSE_R1/fleet.txt" "$OUT/fleet.txt"
|
|
442
|
+
cp "$REUSE_R1"/part_*.jsonl "$OUT/" 2>/dev/null || :
|
|
443
|
+
cp "$REUSE_R1/findings.jsonl" "$OUT/findings.jsonl"
|
|
444
|
+
# 🟥 이 OUT 에 남아 있을 수 있는 **옛 round-2 산출**은 지운다 — 재사용한 r1 위에 남의 2차가
|
|
445
|
+
# 섞이면 그 자리는 측정이 아니라 잔재다 (stale-artifact 부류).
|
|
446
|
+
/bin/rm -f "$OUT"/part2_*.jsonl "$OUT"/raw2_*.txt "$OUT"/err2_*.txt "$OUT"/peer_*.txt "$OUT/findings_r2merged.jsonl"
|
|
447
|
+
: > "$OUT/members.txt"
|
|
448
|
+
/bin/rm -f "$OUT/INTENTIONAL_EMPTY"
|
|
449
|
+
echo "FLEET r1 reused from=$REUSE_R1 (round-1 members NOT run here)" >> "$OUT/members.txt"
|
|
450
|
+
else
|
|
451
|
+
if [ -n "$FLEET" ]; then cp "$FLEET" "$OUT/fleet.txt"; else default_fleet > "$OUT/fleet.txt"; fi
|
|
452
|
+
|
|
453
|
+
: > "$OUT/members.txt"
|
|
454
|
+
/bin/rm -f "$OUT/INTENTIONAL_EMPTY" # R3 #8: a stale marker from an earlier run must not certify this one
|
|
455
|
+
while IFS='|' read -r fam role cmd; do
|
|
456
|
+
[ -n "${fam:-}" ] || continue
|
|
457
|
+
case "$fam" in \#*) continue ;; esac
|
|
458
|
+
run_member "$fam" "$role" "$cmd" "$TARGET" "$OUT" >> "$OUT/members.txt" 2>&1 &
|
|
459
|
+
done < "$OUT/fleet.txt"
|
|
460
|
+
wait
|
|
461
|
+
|
|
462
|
+
cat "$OUT"/part_*.jsonl > "$OUT/findings.jsonl" 2>/dev/null || : > "$OUT/findings.jsonl"
|
|
463
|
+
fi
|
|
464
|
+
R1_TOTAL=$(/usr/bin/wc -l < "$OUT/findings.jsonl" | /usr/bin/tr -d ' ')
|
|
465
|
+
|
|
466
|
+
# --r1-only: 공유 round-1 을 남기고 여기서 끝낸다. round-2 는 --reuse-r1 로 두 번 갈라 돈다.
|
|
467
|
+
if [ "$R1_ONLY" -eq 1 ]; then
|
|
468
|
+
echo "FLEET r1-only r1=$R1_TOTAL out=$OUT — reuse with: --reuse-r1 $OUT --round2 | --round2-blind"
|
|
469
|
+
exit 0
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
# ── 생성시점 탈상관 (2차 패스) ─────────────────────────────────────────────────
|
|
473
|
+
# 🟥 여기가 이 플래그의 논지다. 선별 시점에 계열을 교차시키면(«이 발견이 맞나» 를 남에게 묻는 것)
|
|
474
|
+
# 오류율은 내려가지만 **수확을 지불한다** — 실측: 8건 → 6건. 옥토는 같은 오류율 수준을
|
|
475
|
+
# 수확을 지불하지 않고 낸다. 차이는 계열을 «언제» 만나게 하느냐다: 걸러낼 때가 아니라 «쓸 때».
|
|
476
|
+
# 그래서 2차 패스는 수락/거부를 묻지 않는다 — 상대의 목록을 보고 **자기 목록을 다시 쓰게** 한다.
|
|
477
|
+
# 자기 발견을 스스로 내리면 `DROPPED:` 로 사유를 적게 하고, 그 수를 센다(숨은 수확 손실 계측).
|
|
478
|
+
if [ "$ROUND2" -eq 1 ]; then
|
|
479
|
+
if [ "$R1_TOTAL" -eq 0 ]; then
|
|
480
|
+
echo "FLEET round2 skipped — round 1 produced no findings (nothing to decorrelate against)"
|
|
481
|
+
else
|
|
482
|
+
for _p in "$OUT/findings_r1.jsonl" "$OUT/findings_r2merged.jsonl" "$OUT/INTENTIONAL_EMPTY"; do
|
|
483
|
+
[ -L "$_p" ] && { echo "finding_fleet: refusing to write through a symlink: $_p" >&2; exit 2; }
|
|
484
|
+
done
|
|
485
|
+
cp "$OUT/findings.jsonl" "$OUT/findings_r1.jsonl"
|
|
486
|
+
while IFS='|' read -r fam role cmd; do
|
|
487
|
+
[ -n "${fam:-}" ] || continue
|
|
488
|
+
case "$fam" in \#*) continue ;; esac
|
|
489
|
+
run_member_r2 "$fam" "$role" "$cmd" "$TARGET" "$OUT" >> "$OUT/members.txt" 2>&1 &
|
|
490
|
+
done < "$OUT/fleet.txt"
|
|
491
|
+
wait
|
|
492
|
+
# 🟥 교체는 **멤버별** 이다. 초판은 all-or-nothing 이었고 결함 둘을 동시에 만들었다
|
|
493
|
+
# (cross-family, codex 2026-09-10):
|
|
494
|
+
# ① `[ -s "$(ls part2_*.jsonl | head -1)" ]` 은 **glob 첫 파일만** 봤다. codex 것이 비고
|
|
495
|
+
# gemini 것이 차 있으면 «2차가 아무것도 못 냈다» 고 «거짓 보고» 하며 1차를 유지했다.
|
|
496
|
+
# ② 반대로 첫 파일이 차 있으면 **실패한 멤버의 1차 발견까지 통째로 사라졌다** — 그 멤버가
|
|
497
|
+
# 2차에서 일부만 내고 죽어도 교체가 일어났고, `failed_members` 는 `^MEMBER ` 만 세어
|
|
498
|
+
# MEMBER2 실패를 못 봐서 `failed_members=0 rc=0` 으로 나갔다. 조용한 수확 삭제다.
|
|
499
|
+
# ⇒ 2차가 «성공한» 멤버만 2차 목록으로 갈아끼우고, 실패한 멤버는 자기 1차를 그대로 쓴다.
|
|
500
|
+
R2_FALLBACK=0; R2_OK=0
|
|
501
|
+
: > "$OUT/findings_r2merged.jsonl"
|
|
502
|
+
while IFS='|' read -r _fam _role _cmd; do
|
|
503
|
+
[ -n "${_fam:-}" ] || continue
|
|
504
|
+
case "$_fam" in \#*) continue ;; esac
|
|
505
|
+
# 🟥 R2 #3: family/role 을 정규식에 보간하면 `logic.review` 가 `logicXreview` 의 OK 를 집는다.
|
|
506
|
+
# awk 로 «필드 리터럴» 비교 — 값이 데이터로만 다뤄진다.
|
|
507
|
+
# 🟥 R3 #4: awk had already split `role=logic review` into fields, so $3=="role="r never matched a
|
|
508
|
+
# role with a space → «ABSENT» → its valid round-2 discarded. Compare the fixed PREFIX
|
|
509
|
+
# `MEMBER2 family=F role=R rc=` as one literal span (index()==1); still no regex interpolation.
|
|
510
|
+
# 🟥 R4 #4: a PREFIX still aliased `role=logic` onto `role=logic rc=99 …` (executed). The fixed tail
|
|
511
|
+
# ` rc=N findings=N self_dropped=N status=S$` is stripped first and the WHOLE head is compared;
|
|
512
|
+
# ENVIRON instead of -v so a backslash in a role is not escape-interpreted.
|
|
513
|
+
_st=$(_FAM="$_fam" _ROLE="$_role" /usr/bin/awk 'BEGIN{p="MEMBER2 family=" ENVIRON["_FAM"] " role=" ENVIRON["_ROLE"]}
|
|
514
|
+
match($0, / rc=[^ ]+ findings=[^ ]+ self_dropped=[^ ]+ status=[^ ]+$/) && substr($0,1,RSTART-1)==p {v=$NF; sub(/^status=/,"",v)} END{print v}' "$OUT/members.txt")
|
|
515
|
+
_p2="$OUT/part2_${_fam}_${_role}.jsonl"; _p1="$OUT/part_${_fam}_${_role}.jsonl"
|
|
516
|
+
# 🟥 R2 #4: 차단(ZERO_NONJSON)·빈 응답(ZERO_EMPTY)·미실행(부재) 은 «2차가 없다» 이지 «2차가 비었다» 가 아니다
|
|
517
|
+
# → 자기 1차로 폴백. ZERO_SELFDROPPED 만이 «의도된 빈 2차» 다(#5).
|
|
518
|
+
case "${_st:-FAILED}" in
|
|
519
|
+
FAILED|ZERO_NONJSON|ZERO_EMPTY) _fallback=1 ;;
|
|
520
|
+
*) _fallback=0 ;;
|
|
521
|
+
esac
|
|
522
|
+
if [ "$_fallback" -eq 1 ]; then
|
|
523
|
+
[ -f "$_p1" ] && cat "$_p1" >> "$OUT/findings_r2merged.jsonl"
|
|
524
|
+
R2_FALLBACK=$((R2_FALLBACK + 1))
|
|
525
|
+
echo "FLEET round2 member=$_fam/$_role ${_st:-ABSENT} — keeping ITS round-1 findings (per-member fallback)"
|
|
526
|
+
else
|
|
527
|
+
[ -f "$_p2" ] && cat "$_p2" >> "$OUT/findings_r2merged.jsonl"
|
|
528
|
+
R2_OK=$((R2_OK + 1))
|
|
529
|
+
fi
|
|
530
|
+
done < "$OUT/fleet.txt"
|
|
531
|
+
if [ -s "$OUT/findings_r2merged.jsonl" ]; then
|
|
532
|
+
/bin/mv "$OUT/findings_r2merged.jsonl" "$OUT/findings.jsonl"
|
|
533
|
+
elif [ "$R2_OK" -gt 0 ]; then
|
|
534
|
+
# 🟥 R2 #5: 모든 멤버가 «성공적으로» 빈 2차(전량 자기 철회)를 냈다 — 그것은 결과이지 실패가 아니다.
|
|
535
|
+
# 1차로 되돌리면 정당한 철회가 무효화된다. 빈 결과를 그대로 낸다.
|
|
536
|
+
# 🟥 R3 #5: `&& R2_FALLBACK==0` 은 틀렸다 — 폴백 멤버는 이미 «자기 1차» 를 merged 에 얹었으므로
|
|
537
|
+
# merged 가 비었다는 것은 그 1차도 비었다는 뜻이다(codex 철회 + gemini 1차 0·2차 ZERO_EMPTY).
|
|
538
|
+
# 그 경우 1차 전체로 되돌리면 codex 의 정당한 철회가 되살아난다. 멤버별 병합이 «구성됐으면»
|
|
539
|
+
# 빈 것도 권위다. 1차로 돌아가는 것은 R2_OK==0 (아무 멤버도 2차를 못 냈을 때) 뿐이다.
|
|
540
|
+
: > "$OUT/findings.jsonl"
|
|
541
|
+
: > "$OUT/INTENTIONAL_EMPTY" # R3 #8: typed marker the driver reads — «reviewed, withdrawn» ≠ «unreviewed»
|
|
542
|
+
echo "FLEET round2 all members withdrew (ZERO_SELFDROPPED) — result is intentionally empty"
|
|
543
|
+
else
|
|
544
|
+
# 🟥 합쳐서도 비면 1차를 «대체» 하지 않는다 — 그러면 탈상관이 아니라 삭제다.
|
|
545
|
+
echo "FLEET round2 produced nothing — keeping round-1 findings (a failed 2nd pass must not delete the 1st)"
|
|
546
|
+
fi
|
|
547
|
+
echo "FLEET round2 members_ok=$R2_OK fallback=$R2_FALLBACK blind=$ROUND2_BLIND"
|
|
548
|
+
R2_TOTAL=$(/usr/bin/wc -l < "$OUT/findings.jsonl" | /usr/bin/tr -d ' ')
|
|
549
|
+
echo "FLEET round2 r1=$R1_TOTAL r2=$R2_TOTAL delta=$((R2_TOTAL - R1_TOTAL))"
|
|
550
|
+
fi
|
|
551
|
+
fi
|
|
552
|
+
TOTAL=$(/usr/bin/wc -l < "$OUT/findings.jsonl" | /usr/bin/tr -d ' ')
|
|
553
|
+
MEMBERS=$(/usr/bin/wc -l < "$OUT/fleet.txt" | /usr/bin/tr -d ' ')
|
|
554
|
+
OK=$(/usr/bin/grep -c 'rc=0' "$OUT/members.txt" 2>/dev/null); OK=${OK:-0} # 🟥 never `|| echo 0` here: grep prints 0 AND the fallback echoes 0, giving "0\n0"
|
|
555
|
+
echo "FLEET members=$MEMBERS ok=$OK findings=$TOTAL out=$OUT"
|
|
556
|
+
cat "$OUT/members.txt"
|
|
557
|
+
[ "$OK" -gt 0 ] || { echo "🟥 every member failed — nothing was reviewed, and an empty finding list here means UNREVIEWED, not clean"; exit 1; }
|
|
558
|
+
exit 0
|