@chrono-meta/fh-gate 3.2.0 → 3.5.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 +9 -3
- package/AGENTS.md +2 -2
- package/CATALOG.md +4 -4
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +4 -4
- package/README.md +2 -2
- package/docs/OUTPUT_EVIDENCE.md +1 -1
- package/docs/STANDARDS_ALIGNMENT.md +1 -1
- package/docs/codex-compat.md +2 -2
- 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 +120 -4
- package/knowledge/shared/rules/modes_and_value.md +2 -2
- package/package.json +9 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-commons/README.md +38 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +117 -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-preprep/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-preprep/README.md +24 -0
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/SKILL.md +75 -3
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/mk_slide_fixtures.py +152 -1
- package/plugins/fh-preprep/skills/preprep/lane_attr_consistency.py +370 -0
- package/plugins/fh-preprep/skills/preprep/lane_geometry.py +339 -0
- package/plugins/fh-preprep/skills/preprep/lane_screen_parity.py +232 -0
- package/plugins/fh-preprep/skills/preprep/oox.py +305 -0
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep.py +52 -4
- package/plugins/fh-preprep/skills/preprep/safe_install.py +369 -0
- package/plugins/{fh-commons → fh-preprep}/skills/preprep/surfaces.example.yaml +29 -0
- package/plugins/fh-preprep/skills/preprep/test_preprep_lanes_rp.py +1417 -0
- package/plugins/fh-preprep/skills/preprep/usage_ledger.py +191 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/fh-run.sh +13 -0
- package/scripts/field_canon_preload.sh +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/selfcheck.sh +12 -8
- package/scripts/test_fh_run_resolver_lanes.sh +116 -0
- package/scripts/test_files_manifest_shipping_lanes.sh +31 -0
- 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/scripts/test_preprep_adjacent_dup_lanes.sh +1 -1
- package/scripts/test_preprep_diagram_lanes.sh +1 -1
- package/scripts/test_preprep_drift_anchor.sh +27 -12
- package/scripts/test_preprep_drift_anchor_lanes.sh +4 -2
- package/scripts/test_preprep_font_lanes.sh +7 -7
- package/scripts/test_preprep_progression_lanes.sh +1 -1
- package/scripts/test_preprep_promise_lanes.sh +1 -1
- package/scripts/test_preprep_retired_lanes.sh +45 -2
- package/scripts/test_preprep_slide_refs_lanes.sh +5 -5
- package/scripts/test_usage_ledger_lanes.sh +243 -0
- package/scripts/usage_report.py +211 -0
- package/templates/.git-hooks/pre-commit +152 -6
- package/templates/local_fh_context.md +1 -1
- package/templates/regression_guard.sh +1 -1
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +0 -181
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +0 -184
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/README.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/canon_terms.example.yaml +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/diagram_from_json.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_negative.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_positive.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/font_revert_probe.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_negative.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_positive.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_negative.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_positive.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/interslide_deps.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/jargon_terms.example.yaml +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_adjacent_dup.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_diagram.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_font.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_progression.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_promise.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_refs.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_relations.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/c1_baseline.txt +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/gate.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep_wire.py +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/presentation_checklist.md +0 -0
- /package/plugins/{fh-commons → fh-preprep}/skills/preprep/test_lane_font.py +0 -0
|
@@ -33,6 +33,11 @@ INPUT JSONL, one finding per line:
|
|
|
33
33
|
`id` and `title` are required; the rest are optional and pass through untouched.
|
|
34
34
|
When `producer_family` is present and equals the verifier's family, that finding is stamped
|
|
35
35
|
`unverified` rather than judged -- see the note above VERDICTS.
|
|
36
|
+
EXIT 0 verified, survivors · 1 verified, nothing survived · 2 usage/schema ·
|
|
37
|
+
3 UNVERIFIED (something was never judged) · 4 drops never audited ·
|
|
38
|
+
5 SEEDED control degraded (a known-true finding was deleted), inconclusive
|
|
39
|
+
(the verifier abstained on one) or absent
|
|
40
|
+
(the control never entered the run — which is not the same as passing).
|
|
36
41
|
AUDITOR Optional, and required for the drop-side number to exist. Same protocol as the verifier, but
|
|
37
42
|
it receives only the DROPPED findings and answers {"id","verdict":"correct-drop|wrong-drop|
|
|
38
43
|
uncertain","why"}. A `wrong-drop` finding is moved back into confirmed.jsonl with
|
|
@@ -59,6 +64,13 @@ AUDIT_VERDICTS = ("correct-drop", "wrong-drop", "uncertain")
|
|
|
59
64
|
# decide whether the claim is true, only that the party answering must not be the party asking.
|
|
60
65
|
|
|
61
66
|
|
|
67
|
+
def _schema_error(msg):
|
|
68
|
+
# R4 #3: a string SystemExit exits 1 — the documented code for "verified, nothing survived".
|
|
69
|
+
# Schema rejection is exit 2, and a consumer treating 0/1 as "completed" must not see 1 here.
|
|
70
|
+
print(msg, file=sys.stderr)
|
|
71
|
+
sys.exit(2)
|
|
72
|
+
|
|
73
|
+
|
|
62
74
|
def read_findings(path):
|
|
63
75
|
out, seen = [], set()
|
|
64
76
|
src = sys.stdin if path == "-" else open(path, encoding="utf-8")
|
|
@@ -69,13 +81,23 @@ def read_findings(path):
|
|
|
69
81
|
try:
|
|
70
82
|
d = json.loads(line)
|
|
71
83
|
except json.JSONDecodeError as e:
|
|
72
|
-
|
|
84
|
+
_schema_error(f"finding_verify: line {n} is not JSON: {e}")
|
|
85
|
+
if not isinstance(d, dict):
|
|
86
|
+
# R5 #3: `[]` / `null` parse fine and then crash on .get() with exit 1 (= "nothing survived")
|
|
87
|
+
_schema_error(f"finding_verify: line {n} is not a JSON object")
|
|
73
88
|
for k in REQUIRED:
|
|
74
89
|
if not d.get(k):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
_schema_error(f"finding_verify: line {n} missing required field '{k}'")
|
|
91
|
+
# 🟥 R4 #7: audit/verdict metadata is OURS to write — a producer row arriving with
|
|
92
|
+
# `reinstated: true` was counted as recovered coverage by the driver (probe: 1/2 → 2/2).
|
|
93
|
+
for k in ("verdict", "verify_note", "reinstated", "drop_verdict", "audit_note", "audit_why"):
|
|
94
|
+
d.pop(k, None)
|
|
95
|
+
# 🟥 R3 #1: verdicts are keyed by str(id) (run_verifier), so 101 and "101" are ONE key
|
|
96
|
+
# there — uniqueness here must use the same form, or both rows take whichever verdict
|
|
97
|
+
# came last (reproduced: false-positive + confirmed → confirmed=2, exit 0).
|
|
98
|
+
if str(d["id"]) in seen:
|
|
99
|
+
_schema_error(f"finding_verify: duplicate id {d['id']!r} on line {n} (ids compare as strings)")
|
|
100
|
+
seen.add(str(d["id"]))
|
|
79
101
|
out.append(d)
|
|
80
102
|
return out
|
|
81
103
|
|
|
@@ -101,7 +123,7 @@ def run_verifier(cmd, findings, allowed=VERDICTS):
|
|
|
101
123
|
shell = isinstance(cmd, str)
|
|
102
124
|
try:
|
|
103
125
|
p = subprocess.run(cmd, shell=shell, input=payload, capture_output=True,
|
|
104
|
-
text=True, timeout=int(os.environ.get("FH_VERIFY_TIMEOUT", "
|
|
126
|
+
text=True, timeout=int(os.environ.get("FH_VERIFY_TIMEOUT", "1200")))
|
|
105
127
|
except Exception as e: # noqa: BLE001 - degrade on anything
|
|
106
128
|
return {}, f"verifier did not run: {e}"
|
|
107
129
|
if p.returncode != 0:
|
|
@@ -116,7 +138,13 @@ def run_verifier(cmd, findings, allowed=VERDICTS):
|
|
|
116
138
|
except json.JSONDecodeError:
|
|
117
139
|
continue
|
|
118
140
|
if d.get("id") and d.get("verdict") in allowed:
|
|
119
|
-
|
|
141
|
+
# 🟥 ids are keyed AS STRINGS. A JSONL row may carry an integer id and a verifier may
|
|
142
|
+
# answer with the string form (or vice versa); `verdicts.get(101)` then misses
|
|
143
|
+
# `{"101": ...}` and the finding comes back `unverified` forever — a finding with an
|
|
144
|
+
# integer id could never be verified at all. Found while writing the seeded-control
|
|
145
|
+
# lane for the same type mismatch (cross-family round 4, finding 7; this second half was
|
|
146
|
+
# not in the report — the lane surfaced it).
|
|
147
|
+
got[str(d["id"])] = d
|
|
120
148
|
if not got:
|
|
121
149
|
return {}, "verifier returned no parseable verdict"
|
|
122
150
|
return got, None
|
|
@@ -137,6 +165,12 @@ def main():
|
|
|
137
165
|
help="model family of the verifier, recorded verbatim and never checked")
|
|
138
166
|
ap.add_argument("--audit-verifier", default=os.environ.get("FH_AUDIT_CMD", ""),
|
|
139
167
|
help="command that re-checks the DROPPED findings; without it the run is UNAUDITED")
|
|
168
|
+
ap.add_argument("--seeded", default="",
|
|
169
|
+
help="comma-separated finding ids that are KNOWN-TRUE. They are ordinary rows in "
|
|
170
|
+
"the input; the verifier is never told which they are. A filter that buys "
|
|
171
|
+
"precision by deleting reports itself by deleting these.")
|
|
172
|
+
ap.add_argument("--seeded-file", default="",
|
|
173
|
+
help="file with one known-true finding id per line (same meaning as --seeded)")
|
|
140
174
|
ap.add_argument("--audit-family", default=os.environ.get("FH_AUDIT_FAMILY", "unstated"),
|
|
141
175
|
help="model family of the auditor; must differ from the verifier's")
|
|
142
176
|
a = ap.parse_args()
|
|
@@ -156,6 +190,29 @@ def main():
|
|
|
156
190
|
a.verifier = _as_argv(a.verifier_argv, "--verifier-argv") or a.verifier
|
|
157
191
|
a.audit_verifier = _as_argv(a.audit_verifier_argv, "--audit-verifier-argv") or a.audit_verifier
|
|
158
192
|
|
|
193
|
+
# 🟥 The seeded control is parsed BEFORE anything runs. Parsing it at the end meant a bad
|
|
194
|
+
# control file surfaced only after the verifier had run and the output files and the VERIFIED
|
|
195
|
+
# summary were already written — and a UnicodeDecodeError there escaped `except OSError` and
|
|
196
|
+
# exited 1, which is this CLI's documented code for "verified, nothing survived". A consumer
|
|
197
|
+
# accepting 0 and 1 would have read a configuration failure as a completed run.
|
|
198
|
+
# (cross-family review 2026-09-09, findings 3 and 4, both reproduced.)
|
|
199
|
+
seeded = [x.strip() for x in a.seeded.split(",") if x.strip()]
|
|
200
|
+
if a.seeded_file:
|
|
201
|
+
try:
|
|
202
|
+
with open(a.seeded_file, encoding="utf-8") as fh:
|
|
203
|
+
from_file = [ln.strip() for ln in fh if ln.strip() and not ln.startswith("#")]
|
|
204
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
205
|
+
print("finding_verify: --seeded-file unusable: %s" % e, file=sys.stderr)
|
|
206
|
+
return 2
|
|
207
|
+
# 🟥 "option supplied but it yielded nothing" is NOT "option omitted". A repository-controlled
|
|
208
|
+
# control file that goes empty would otherwise silently turn calibration off and still exit 0.
|
|
209
|
+
if not from_file:
|
|
210
|
+
print("finding_verify: --seeded-file %r yielded no ids — a control file that declares "
|
|
211
|
+
"nothing is a disabled control, not an absent one" % a.seeded_file, file=sys.stderr)
|
|
212
|
+
return 2
|
|
213
|
+
seeded += from_file
|
|
214
|
+
seeded = sorted(set(seeded))
|
|
215
|
+
|
|
159
216
|
# 문자열이든 리스트든 «비어 있나»를 같은 방법으로 묻는다 — 리스트에 .strip() 은 없다.
|
|
160
217
|
def _configured(cmd):
|
|
161
218
|
return bool(cmd) if isinstance(cmd, list) else bool(str(cmd or "").strip())
|
|
@@ -170,7 +227,7 @@ def main():
|
|
|
170
227
|
|
|
171
228
|
confirmed, dropped, debate, unverified = [], [], 0, 0
|
|
172
229
|
for f in findings:
|
|
173
|
-
v = verdicts.get(f["id"])
|
|
230
|
+
v = verdicts.get(str(f["id"]))
|
|
174
231
|
if v is None:
|
|
175
232
|
# Degraded, or the verifier skipped this one. Keep it, mark it, never drop it silently.
|
|
176
233
|
f = dict(f, verdict="unverified",
|
|
@@ -214,6 +271,8 @@ def main():
|
|
|
214
271
|
# the drop, and moves a reversed drop back. The refusal to report a bare precision number when this
|
|
215
272
|
# did not run is the mechanized part.
|
|
216
273
|
audited = wrong_drops = reinstated = 0
|
|
274
|
+
# 감사가 `dropped` 를 재할당하기 전에 «필터가 무엇을 지웠나» 를 얼려 둔다. SEEDED 는 이것을 읽는다.
|
|
275
|
+
pre_audit_dropped_ids = [f.get("id") for f in dropped]
|
|
217
276
|
audit_status = "UNAUDITED"
|
|
218
277
|
audit_note = ""
|
|
219
278
|
if dropped and _configured(a.audit_verifier):
|
|
@@ -227,7 +286,7 @@ def main():
|
|
|
227
286
|
else:
|
|
228
287
|
kept = []
|
|
229
288
|
for d in dropped:
|
|
230
|
-
r = av.get(d["id"])
|
|
289
|
+
r = av.get(str(d["id"])) # R3 #7: same normalization as the verify lookup
|
|
231
290
|
if r is None:
|
|
232
291
|
kept.append(dict(d, drop_verdict="unaudited"))
|
|
233
292
|
continue
|
|
@@ -238,7 +297,17 @@ def main():
|
|
|
238
297
|
if r["verdict"] == "wrong-drop":
|
|
239
298
|
wrong_drops += 1
|
|
240
299
|
reinstated += 1
|
|
241
|
-
|
|
300
|
+
# 🟥 A reinstated row used to keep its ORIGINAL top-level
|
|
301
|
+
# `verdict: "false-positive"` while moving into confirmed.jsonl. Every
|
|
302
|
+
# consumer that counts decisions by top-level verdict then lost it from both
|
|
303
|
+
# sides — the driver's coverage read 0% on a run that was fully judged and
|
|
304
|
+
# audited. The row's verdict must state the decision that now stands; the
|
|
305
|
+
# superseded one is kept under its own key rather than deleted.
|
|
306
|
+
# (cross-family round 4, gemini family, A severity — three codex rounds
|
|
307
|
+
# missed it because they were the same family that wrote the counting fix.)
|
|
308
|
+
confirmed.append(dict(d, reinstated=True,
|
|
309
|
+
pre_audit_verdict=d.get("verdict"),
|
|
310
|
+
verdict="confirmed"))
|
|
242
311
|
else:
|
|
243
312
|
kept.append(d)
|
|
244
313
|
dropped = kept
|
|
@@ -262,9 +331,31 @@ def main():
|
|
|
262
331
|
for r in rows:
|
|
263
332
|
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
264
333
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
334
|
+
# 🟥 COVERAGE IS NOT OPTIONAL. An error rate computed over judged findings while the unjudged
|
|
335
|
+
# ones sit outside the denominator is a rate at an unstated operating point, and two arms with
|
|
336
|
+
# different abstention rates are then not comparable at all. This is a named, documented flaw in
|
|
337
|
+
# the selective-classification literature (evaluation "assumes fixed working points",
|
|
338
|
+
# arXiv:2407.01032), and our own five-arm table is an instance of it: UNVERIFIABLE was ~half the
|
|
339
|
+
# claims and was silently dropped from the denominator. So the line carries coverage
|
|
340
|
+
# unconditionally, exactly like DROPS does — same discipline, second application.
|
|
341
|
+
# 🟥 `needs-debate` IS NOT A DECISION. Counting it as covered let every finding come back
|
|
342
|
+
# `needs-debate` and still print coverage=100% — the exact thing coverage exists to prevent
|
|
343
|
+
# (cross-family review 2026-09-09, finding 2, reproduced). Decision coverage = findings that got
|
|
344
|
+
# a RESOLVED truth judgment; debate and unverified are both abstentions, of different kinds.
|
|
345
|
+
# Percentage is FLOORED, never rounded: 200/201 must not print 100%.
|
|
346
|
+
judged = len(findings) - unverified - debate
|
|
347
|
+
pct = (judged * 100) // len(findings) if findings else 0
|
|
348
|
+
# 🟥 `VERIFIED` must not be stamped on a run in which nothing was decided. An all-debate run
|
|
349
|
+
# left `unverified == 0`, so the summary said VERIFIED while coverage said 0% — the exit code was
|
|
350
|
+
# already fixed to 3 but the human-readable half still lied.
|
|
351
|
+
# (cross-family round 5, gemini family, A severity — a half-fix that stopped at the exit code.)
|
|
352
|
+
status = "UNVERIFIED" if (unverified or (findings and judged == 0)) else "VERIFIED"
|
|
353
|
+
print("FINDINGS in={} confirmed={} dropped={} debate={} unverified={} coverage={}/{} ({}%) "
|
|
354
|
+
"family={} status={}{}".format(
|
|
355
|
+
# `confirmed=` excludes BOTH abstention kinds. Debate rows live in the confirmed list for
|
|
356
|
+
# output purposes, but counting them as confirmations double-reports them beside `debate=`.
|
|
357
|
+
len(findings), len(confirmed) - unverified - debate, len(dropped), debate, unverified,
|
|
358
|
+
judged, len(findings), pct,
|
|
268
359
|
a.family, status, "" if not err else " reason=" + err.replace("\n", " ")))
|
|
269
360
|
# 🟥 The drop line is unconditional. A survivor-side number without it is a precision claim made by
|
|
270
361
|
# deleting, and this pipeline does not let a reader compute one without seeing whether the
|
|
@@ -272,8 +363,102 @@ def main():
|
|
|
272
363
|
print("DROPS dropped={} audited={} wrong_drops={} reinstated={} auditor={} drop_audit={}{}".format(
|
|
273
364
|
len(dropped), audited, wrong_drops, reinstated, a.audit_family, audit_status,
|
|
274
365
|
"" if not audit_note else " reason=" + audit_note.replace("\n", " ")))
|
|
366
|
+
# 🟥 SEEDED — the known-pair discipline applied to the FILTER, not to a scanner.
|
|
367
|
+
# This repo has required known-pair calibration of instruments for a long time and had never
|
|
368
|
+
# once applied it to the deletion stage, which is also an instrument. Known-true findings are
|
|
369
|
+
# mixed into the input as ordinary rows; their ids live only in this process and never reach the
|
|
370
|
+
# verifier's prompt. A stage that buys precision by deleting therefore reports itself.
|
|
371
|
+
if not seeded:
|
|
372
|
+
seed_status, s_present, s_kept, s_dropped, s_abstained = "NOT_PROVIDED", 0, 0, 0, 0
|
|
373
|
+
else:
|
|
374
|
+
# 🟥 ids are compared AS STRINGS on both sides. A JSONL row may legitimately carry an
|
|
375
|
+
# integer id, and `"101" in {101}` is False in Python — the control then reported ABSENT
|
|
376
|
+
# (exit 5) on a run where the seed was right there. (cross-family round 4, finding 7.)
|
|
377
|
+
# 씨앗은 «라우팅 id» 로도 «원래 멤버 id» 로도 선언할 수 있다. fleet 이 id 를 재번호하므로
|
|
378
|
+
# 호출자 어휘로 선언하려면 후자가 필요하다.
|
|
379
|
+
ids_in = {str(f.get("id")) for f in findings}
|
|
380
|
+
ids_in |= {str(f["member_id"]) for f in findings if f.get("member_id") is not None}
|
|
381
|
+
present = [i for i in seeded if str(i) in ids_in]
|
|
382
|
+
def _row_matches(f, sid):
|
|
383
|
+
# R8 #3: a missing alias must not become the string "None" — a legitimate seed named "None"
|
|
384
|
+
# matched every alias-less row and reported AMBIGUOUS.
|
|
385
|
+
mid = f.get("member_id")
|
|
386
|
+
return str(f.get("id")) == str(sid) or (mid is not None and str(mid) == str(sid))
|
|
387
|
+
# 🟥 A SEED MUST RESOLVE TO EXACTLY ONE ROW. `member_id` is the member's own id and is only
|
|
388
|
+
# locally unique — two fleet members can both emit `1`. Binding the seed to every matching
|
|
389
|
+
# row then makes an unrelated member's drop read as "the control was deleted", and a
|
|
390
|
+
# perfectly legitimate run fails closed with exit 5. A false alarm on a fail-closed surface
|
|
391
|
+
# is not a safe default: it trains the override. So an ambiguous declaration is reported AS
|
|
392
|
+
# ambiguous, by name, instead of being silently resolved the pessimistic way.
|
|
393
|
+
# (cross-family round 6, gemini family, A severity.)
|
|
394
|
+
ambiguous = [i for i in present if sum(1 for f in findings if _row_matches(f, i)) > 1]
|
|
395
|
+
# 🟥 THE PRE-AUDIT DELETION SET, not the post-audit one. `dropped` is reassigned when the
|
|
396
|
+
# auditor reinstates a wrong drop, so reading it here meant: verifier deletes the known-true
|
|
397
|
+
# seed → auditor puts it back → SEEDED prints CLEAN, exit 0. The filter demonstrably deleted
|
|
398
|
+
# a control and the control said it passed. Reinstatement repairs the OUTPUT; it does not
|
|
399
|
+
# establish that the FILTER passed, and the filter is what this control measures.
|
|
400
|
+
# (cross-family review 2026-09-09, finding 1 — A severity, reproduced.)
|
|
401
|
+
dropped_ids = {str(i) for i in pre_audit_dropped_ids}
|
|
402
|
+
# 🟥 SURVIVING IS NOT PASSING. The first version asked only "was the seed deleted?", so a
|
|
403
|
+
# verifier that ABSTAINED on a known-true finding (`needs-debate`, or unverified) reported
|
|
404
|
+
# kept=1 status=CLEAN exit 0 — precision bought by not deciding instead of by deleting,
|
|
405
|
+
# which is the same purchase through a different door. A seed passes only when it received a
|
|
406
|
+
# positive decision. (cross-family round 4, gemini family, A severity.)
|
|
407
|
+
abstained_verdicts = {"needs-debate", "unverified"}
|
|
408
|
+
verdict_of = {}
|
|
409
|
+
for f in confirmed:
|
|
410
|
+
verdict_of[str(f.get("id"))] = f.get("verdict")
|
|
411
|
+
s_present = len(present)
|
|
412
|
+
dropped_rows = [f for f in findings if str(f.get("id")) in dropped_ids]
|
|
413
|
+
s_dropped = len([i for i in present if any(_row_matches(f, i) for f in dropped_rows)])
|
|
414
|
+
s_abstained = len([i for i in present
|
|
415
|
+
if not any(_row_matches(f, i) for f in dropped_rows)
|
|
416
|
+
and any(_row_matches(f, i) and f.get("verdict") in abstained_verdicts
|
|
417
|
+
for f in confirmed)])
|
|
418
|
+
s_kept = s_present - s_dropped - s_abstained
|
|
419
|
+
if ambiguous:
|
|
420
|
+
seed_status = "AMBIGUOUS"
|
|
421
|
+
print("finding_verify: seed(s) %s match more than one finding — member ids are only "
|
|
422
|
+
"locally unique; declare the routing id instead" % ",".join(ambiguous),
|
|
423
|
+
file=sys.stderr)
|
|
424
|
+
elif len(present) < len(seeded):
|
|
425
|
+
# A control that never entered the run is not a passing control. It looks exactly like a
|
|
426
|
+
# clean one from the outside, which is the whole reason this branch exists.
|
|
427
|
+
# 🟥 R3 #2: `not present` only caught TOTAL absence — z1 present + `missing` absent
|
|
428
|
+
# printed CLEAN exit 0. Any declared seed missing is ABSENT.
|
|
429
|
+
seed_status = "ABSENT"
|
|
430
|
+
elif s_dropped:
|
|
431
|
+
seed_status = "DEGRADED"
|
|
432
|
+
elif s_abstained:
|
|
433
|
+
seed_status = "INCONCLUSIVE"
|
|
434
|
+
else:
|
|
435
|
+
seed_status = "CLEAN"
|
|
436
|
+
print("SEEDED declared={} present={} kept={} dropped={} abstained={} status={}".format(
|
|
437
|
+
len(seeded), s_present, s_kept, s_dropped,
|
|
438
|
+
s_abstained if seeded else 0, seed_status))
|
|
439
|
+
|
|
440
|
+
# 🟥 THE SEED VERDICT IS CHECKED FIRST. It used to sit after the two exit-3 branches, so a
|
|
441
|
+
# single unrelated `unverified` finding anywhere in the batch masked a DEGRADED control: the
|
|
442
|
+
# split returned 3, and in the driver rank_of(3) < rank_of(5), so "the filter deleted a
|
|
443
|
+
# known-true finding" was suppressed into a generic unverified exit. The calibration verdict is
|
|
444
|
+
# the more specific and the more serious fact, and it is reported as such.
|
|
445
|
+
# (cross-family round 5, gemini family, A severity.)
|
|
446
|
+
# 🟥 A VERIFIER THAT DID NOT RUN IS AN EXECUTION FAILURE, NOT A CONTROL AMBIGUITY. When the
|
|
447
|
+
# verifier command crashes, every finding degrades to `unverified`, the seed among them becomes
|
|
448
|
+
# `INCONCLUSIVE`, and the run used to exit 5 — reporting a calibration problem for what is
|
|
449
|
+
# actually "the tool did not execute". The execution fact wins. (cross-family round 6.)
|
|
450
|
+
if err and unverified:
|
|
451
|
+
return 3
|
|
452
|
+
if seed_status in ("ABSENT", "DEGRADED", "INCONCLUSIVE", "AMBIGUOUS"):
|
|
453
|
+
return 5
|
|
275
454
|
if unverified:
|
|
276
455
|
return 3
|
|
456
|
+
# 🟥 ZERO DECISIONS IS NOT A PASS. Every finding coming back `needs-debate` left `unverified=0`,
|
|
457
|
+
# so status stamped VERIFIED and the run exited 0 while coverage said 0% — a caller reading exit
|
|
458
|
+
# codes saw a completed run in which nothing was actually judged.
|
|
459
|
+
# (cross-family round 4, gemini family, A severity.)
|
|
460
|
+
if findings and judged == 0:
|
|
461
|
+
return 3 # a known-true finding was deleted, or the control never ran
|
|
277
462
|
if audit_status in ("UNAUDITED", "PARTIAL"):
|
|
278
463
|
return 4 # drops happened and nobody checked them: not a completed run
|
|
279
464
|
return 0 if confirmed else 1
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
# BACK-END SHIPPING DOCTRINE (operator, 2026-08-15): "개발의 앞단-영혼심기, 중간단-탈상관 가속화,
|
|
23
23
|
# 뒷단 출하전-4단검증 및 하네스오너 리뷰" — the back end of shipping is 4-axis verification AND a
|
|
24
24
|
# standpoint-axis review, not 4-axis alone. CORRECTED same session (an earlier draft of this
|
|
25
|
-
# header named fh-meta:
|
|
25
|
+
# header named fh-meta:harness-pr-reviewer here — wrong skill, caught by the operator: "내가 말한건
|
|
26
26
|
# 하네스오너(너) 아니라 그 하네스에 에이전트가 들어가서 그 입장에서 리뷰한다는거야"). "하네스오너
|
|
27
27
|
# 리뷰" is NOT the human operator (that gate is merge, unconditionally human, unchanged) NOR
|
|
28
|
-
#
|
|
28
|
+
# harness-pr-reviewer (which checks FH's diff against FH's OWN conventions — same-repo
|
|
29
29
|
# self-consistency, a different question). It is the STANDPOINT AXIS
|
|
30
30
|
# (`knowledge/shared/harness-core/field_verdict_crossfamily_gate.md §7`, the mechanism behind the
|
|
31
31
|
# if(kakao)26 keynote's p15 "(c) 탈상관의 확장" slide — "계열을 늘려도 못 잡는 결함이 있습니다,
|
|
@@ -197,7 +197,7 @@ BACK-END CHECKPOINTS (operator instruction, 2026-08-15 - required steps on this
|
|
|
197
197
|
2. STANDPOINT AXIS (knowledge/shared/harness-core/field_verdict_crossfamily_gate.md §7 - read it before applying this step, this summary is not the full spec): does the diff alter ANOTHER harness's actual behavior, gate outcome, or interaction contract (not merely touch a path that happens to be synced elsewhere - the trigger is behavioral, not file-class)? Most ordinary FH self-improvement from a digest signal will correctly land on standpoint: not-applicable - that is a correct, expected answer, not a shortfall to fix. This is a genuinely different axis from "harness-owner reviewing FH's own conventions" - it means an agent actually running the diff's effect FROM the standpoint of the OTHER harness's own repo (family diversity alone does not catch this: the mechanism behind the if(kakao)26 keynote's p15 slide "(c) 탈상관의 확장" - "계열을 늘려도 못 잡는 결함이 있습니다, 입장을 바꾸면 보입니다" - §7 is built on three field incidents where full cross-family review missed a defect that only one execution-from-the-target's-own-repo caught).
|
|
198
198
|
- If NOT applicable (the common case): record standpoint: not-applicable in the marker/signal, state briefly what was checked (per the spec's own discipline - asserting non-applicability without naming what was checked is indistinguishable from not having looked at all), and move on.
|
|
199
199
|
- If applicable: check whether a local clone of the target harness exists on this machine (e.g. under the parent of ${FH_DIR} - sibling directories like pmh-dev, qasp-dev, or similar). If one exists, run the change against THAT repo's own content/rules from its own standpoint (tier2) - this must happen BEFORE any push or gh pr create for THIS diff, same non-negotiable pre-push timing as the irreversibility check above and for the identical reason (PR #370, 2026-08-14: a post-PR standpoint review still caught 2 residency leaks that had already sat in public view before the fix - public exposure is effectively irreversible, so this cannot run after the diff is visible). If it finds anything, fix it in the local diff and re-run until clean - only then push and open the PR (or, if step 1 already routed to hold, fold the finding into that signal file instead). If no local clone of the target harness is reachable, record standpoint: DEGRADED_NO_TARGET_ACCESS (could not, not did not) and proceed - do not block indefinitely on a target you structurally cannot reach, but do not silently claim not-applicable either when it actually is applicable and merely unreachable.
|
|
200
|
-
Do NOT conflate this with fh-meta:challenger (family/adversarial-correctness axis, already required by the 4-axis gate above) or with fh-meta:
|
|
200
|
+
Do NOT conflate this with fh-meta:challenger (family/adversarial-correctness axis, already required by the 4-axis gate above) or with fh-meta:harness-pr-reviewer (checks FH's own diff against FH's OWN baseline conventions - a same-repo self-consistency check, not a standpoint-axis review at all). All three are different lenses; running one is not a substitute for another.
|
|
201
201
|
|
|
202
202
|
3. If the diff is neither irreversible/load-bearing (step 1: NO) nor standpoint-applicable (step 2: NO or DEGRADED) - the common case, ordinary small reversible self-improvement: skip straight to branch/commit/push/PR below.
|
|
203
203
|
|
|
@@ -46,7 +46,15 @@ scan_one() { # $1=file → prints hits, returns 0 hit / 1 none / 3 unscannable
|
|
|
46
46
|
if /usr/bin/file -b --mime-encoding "$f" 2>/dev/null | /usr/bin/grep -q '^binary$'; then
|
|
47
47
|
echo "UNSCANNABLE $f (binary)"; return 3; fi
|
|
48
48
|
# one pass: drop comment-led lines, then classify (VERDICT wins over EXPOSURE over IRREV per line)
|
|
49
|
-
|
|
49
|
+
# 🟥 pmh-dev #76 (2026-09-11): 여러 줄 docstring 의 «안쪽» 줄은 줄머리가 따옴표가 아니라 COMMENT_RE 를 통과했다 —
|
|
50
|
+
# 실물: 파이썬 테스트의 docstring 안 «fail-safe … 봉인» 문장이 GATE-SHAPED 로 지목됨. 줄 단위 grep 앞에
|
|
51
|
+
# """ / ''' 짝 상태를 awk 로 추적해 열린 동안의 줄을 전부 버린다(여는 줄·닫는 줄 포함). 한 줄 docstring 은
|
|
52
|
+
# 짝수라 상태가 안 바뀌고 종전 COMMENT_RE 가 그대로 거른다. 잔여(이름으로): JS 템플릿 리터럴(`) · 일반 문자열 안의 """.
|
|
53
|
+
body=$(/usr/bin/awk -v q="'''" -v t='"""' '
|
|
54
|
+
{ line=$0; n=gsub(t, "&", line); n+=gsub(q, "&", line)
|
|
55
|
+
if (indoc) { if (n % 2 == 1) indoc=0; next }
|
|
56
|
+
if (n % 2 == 1) { indoc=1; next }
|
|
57
|
+
print NR ":" $0 }' "$f" 2>/dev/null | /usr/bin/grep -Ev "^[0-9]+:${COMMENT_RE#^}" || true)
|
|
50
58
|
v=$(printf '%s\n' "$body" | /usr/bin/grep -Ei "$VERDICT_RE" || true)
|
|
51
59
|
v2=$(printf '%s\n' "$body" | /usr/bin/grep -E "$ENUM_RE" || true)
|
|
52
60
|
e=$(printf '%s\n' "$body" | /usr/bin/grep -E "$EXPOSURE_RE" || true)
|
|
@@ -64,7 +72,7 @@ scan_one() { # $1=file → prints hits, returns 0 hit / 1 none / 3 unscannable
|
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
selftest() { # known pair — positive must hit, negative must not, comment-only must not
|
|
67
|
-
local d rc_pos rc_pos2 rc_neg rc_cmt rc_bin rc_irr rc_bnd rc_star fails=0
|
|
75
|
+
local d rc_pos rc_pos2 rc_neg rc_cmt rc_bin rc_irr rc_bnd rc_star rc_doc rc_doc2 fails=0
|
|
68
76
|
d="$(mktemp -d 2>/dev/null)" || d=""
|
|
69
77
|
[ -n "$d" ] && [ -w "$d" ] || { echo "SELFTEST: ENV-BLOCKED (mktemp -d failed) — result unmeasured, not a pass"; return 3; }
|
|
70
78
|
printf 'export class S {\n start() {\n this.app.listen(this.config.port, () => {});\n }\n}\n' > "$d/pos_exposure.ts"
|
|
@@ -75,6 +83,8 @@ selftest() { # known pair — positive must hit, negative must not, comment-only
|
|
|
75
83
|
printf 'set -e\ngit push origin main \\\n --force\n' > "$d/pos_irrev.sh"
|
|
76
84
|
printf 'authored_by = "x"\nallowance = 3\nauthor = "y"\n' > "$d/neg_boundary.py"
|
|
77
85
|
printf 'int f(int *allow) {\n *allow = 1;\n return 0;\n}\n' > "$d/pos_star.c"
|
|
86
|
+
printf 'def t():\n """docstring\n PASS allow true — fail-safe 계통은 봉인\n """\n return 1\n' > "$d/neg_docstring.py" # pmh-dev #76
|
|
87
|
+
printf 'def t():\n """docstring\n PASS allow true\n """\n return Verdict.ALLOW\n' > "$d/pos_after_docstring.py"
|
|
78
88
|
scan_one "$d/pos_exposure.ts" >/dev/null; rc_pos=$?
|
|
79
89
|
scan_one "$d/pos_verdict.py" >/dev/null; rc_pos2=$?
|
|
80
90
|
scan_one "$d/neg_util.py" >/dev/null; rc_neg=$?
|
|
@@ -83,6 +93,8 @@ selftest() { # known pair — positive must hit, negative must not, comment-only
|
|
|
83
93
|
scan_one "$d/pos_irrev.sh" >/dev/null; rc_irr=$?
|
|
84
94
|
scan_one "$d/neg_boundary.py" >/dev/null; rc_bnd=$?
|
|
85
95
|
scan_one "$d/pos_star.c" >/dev/null; rc_star=$?
|
|
96
|
+
scan_one "$d/neg_docstring.py" >/dev/null; rc_doc=$?
|
|
97
|
+
scan_one "$d/pos_after_docstring.py" >/dev/null; rc_doc2=$?
|
|
86
98
|
[ "$rc_pos" -eq 0 ] && echo " ✅ known-positive exposure (listen()) → GATE-SHAPED" || { echo " ❌ known-positive exposure rc=$rc_pos"; fails=1; }
|
|
87
99
|
[ "$rc_pos2" -eq 0 ] && echo " ✅ known-positive verdict (ALLOW/DENY) → GATE-SHAPED" || { echo " ❌ known-positive verdict rc=$rc_pos2"; fails=1; }
|
|
88
100
|
[ "$rc_neg" -eq 1 ] && echo " ✅ known-negative util (docstring 'Pass') → NOT" || { echo " ❌ known-negative util rc=$rc_neg"; fails=1; }
|
|
@@ -91,6 +103,8 @@ selftest() { # known pair — positive must hit, negative must not, comment-only
|
|
|
91
103
|
[ "$rc_irr" -eq 0 ] && echo " ✅ known-positive irreversible (continued --force line) → GATE-SHAPED" || { echo " ❌ known-positive irrev rc=$rc_irr"; fails=1; }
|
|
92
104
|
[ "$rc_bnd" -eq 1 ] && echo " ✅ boundary: author/authored/allowance → NOT" || { echo " ❌ boundary rc=$rc_bnd"; fails=1; }
|
|
93
105
|
[ "$rc_star" -eq 0 ] && echo " ✅ code line starting with *allow (not a comment) → GATE-SHAPED" || { echo " ❌ star-code rc=$rc_star"; fails=1; }
|
|
106
|
+
[ "$rc_doc" -eq 1 ] && echo " ✅ multi-line docstring interior ('PASS allow') → NOT (pmh-dev #76)" || { echo " ❌ docstring interior rc=$rc_doc"; fails=1; }
|
|
107
|
+
[ "$rc_doc2" -eq 0 ] && echo " ✅ control: real verdict line AFTER the docstring → GATE-SHAPED" || { echo " ❌ after-docstring rc=$rc_doc2"; fails=1; }
|
|
94
108
|
/bin/rm -rf "$d"
|
|
95
109
|
[ "$fails" -eq 0 ] && { echo "SELFTEST: PASS"; return 0; } || { echo "SELFTEST: FAIL"; return 3; }
|
|
96
110
|
}
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -649,6 +649,10 @@ fi
|
|
|
649
649
|
# resolves to "SKIP (subject not in files[], and absent)" — the anchor arm is never reached.
|
|
650
650
|
_LANE_TO=""; command -v timeout >/dev/null 2>&1 && _LANE_TO="timeout 300"
|
|
651
651
|
for _pair in \
|
|
652
|
+
`# 사용 원장 (2026-09-13 신설). SUBJECT 를 원장 모듈로 잡는다 — 배선이 끊기는 자리는 preprep.py 의 __main__ 한 줄이 아니라 모듈이 사라지는 쪽이고, import 가 try/except 라 모듈이 없으면 조용히 무동작으로 돌아간다. 그 무음이 이 레인이 잡을 것이다.` \
|
|
653
|
+
"plugins/fh-preprep/skills/preprep/usage_ledger.py|scripts/test_usage_ledger_lanes.sh" \
|
|
654
|
+
`# fh-run 의 이름→스킬 해석기 (2026-09-13 신설). preprep 승격이 --skill preprep 을 조용히 깨뜨렸고, fh-qp 의 4개는 애초부터 안 닿고 있었다. 소비자 대면 경로인데 커버리지가 0이었다.` \
|
|
655
|
+
"scripts/fh-run.sh|scripts/test_fh_run_resolver_lanes.sh" \
|
|
652
656
|
"scripts/degrade_probe_capability.sh|scripts/test_capability_entrypoint_shipping.sh" \
|
|
653
657
|
"scripts/chamber_run.sh|scripts/test_chamber_run_lanes.sh" \
|
|
654
658
|
"scripts/chamber_candidate_collect.sh|scripts/test_chamber_sig_lanes.sh" \
|
|
@@ -676,7 +680,7 @@ for _pair in \
|
|
|
676
680
|
"templates/.git-hooks/pre-commit|scripts/test_gate_two_verdicts_lanes.sh" \
|
|
677
681
|
`# ── fh-qp (QP) — chamber run #18 EMIT 2026-09-05: qp_tools.sh known-pair + residency lanes ──` \
|
|
678
682
|
"plugins/fh-qp/scripts/qp_tools.sh|scripts/test_fh_qp_lanes.sh" \
|
|
679
|
-
"plugins/fh-
|
|
683
|
+
"plugins/fh-preprep/skills/preprep/diagram_from_json.py|scripts/test_preprep_diagram_lanes.sh" \
|
|
680
684
|
`# ── action.yml — the GitHub Action wrapper: its exit-code mapping is where a typed verdict could become a boolean ──` \
|
|
681
685
|
"action.yml|scripts/test_action_yml_lanes.sh" \
|
|
682
686
|
"scripts/sim_isolated_run.sh|scripts/test_sim_path_isolation_lanes.sh" \
|
|
@@ -707,13 +711,13 @@ for _pair in \
|
|
|
707
711
|
"scripts/fh-goal.sh|scripts/test_fh_goal_change_detection_lanes.sh" \
|
|
708
712
|
"scripts/utterance_skill_probe.sh|scripts/test_utterance_skill_probe_lanes.sh" \
|
|
709
713
|
`# ── preprep 스킬(2026-08-29). 주체는 스킬 안의 모듈이라 scripts/ 밖이다 ──` \
|
|
710
|
-
"plugins/fh-
|
|
711
|
-
"plugins/fh-
|
|
712
|
-
"plugins/fh-
|
|
713
|
-
"plugins/fh-
|
|
714
|
-
"plugins/fh-
|
|
715
|
-
"plugins/fh-
|
|
716
|
-
"plugins/fh-
|
|
714
|
+
"plugins/fh-preprep/skills/preprep/preprep.py|scripts/test_preprep_retired_lanes.sh" \
|
|
715
|
+
"plugins/fh-preprep/skills/preprep/lane_progression.py|scripts/test_preprep_progression_lanes.sh" \
|
|
716
|
+
"plugins/fh-preprep/skills/preprep/lane_adjacent_dup.py|scripts/test_preprep_adjacent_dup_lanes.sh" \
|
|
717
|
+
"plugins/fh-preprep/skills/preprep/lane_promise.py|scripts/test_preprep_promise_lanes.sh" \
|
|
718
|
+
"plugins/fh-preprep/skills/preprep/lane_slide_refs.py|scripts/test_preprep_slide_refs_lanes.sh" \
|
|
719
|
+
"plugins/fh-preprep/skills/preprep/lane_font.py|scripts/test_preprep_font_lanes.sh" \
|
|
720
|
+
"plugins/fh-preprep/skills/preprep/SKILL.md|scripts/test_preprep_drift_anchor.sh" \
|
|
717
721
|
"scripts/test_preprep_drift_anchor.sh|scripts/test_preprep_drift_anchor_lanes.sh" \
|
|
718
722
|
"scripts/field_canon_preload.sh|scripts/test_skill_canon_preload_lanes.sh" \
|
|
719
723
|
`# ── round/ 회차 계기 4종(2026-09-01). 넷 다 한 스위트가 잡는다 — 주체별로 행을 둔다 ──` \
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fh-run 의 «이름 → 스킬 파일» 해석기 앵커.
|
|
3
|
+
#
|
|
4
|
+
# ## 왜 생겼나 (2026-09-13)
|
|
5
|
+
#
|
|
6
|
+
# preprep 을 자기 플러그인으로 승격하면서 `--skill preprep` 가 **조용히 깨졌다**.
|
|
7
|
+
# 해석기가 `fh-meta` · `fh-commons` 둘만 하드코딩하고 있었기 때문이다. 🟥 그리고 그건
|
|
8
|
+
# 새 결함이 아니라 **선재 갭**이었다 — `fh-qp` 의 스킬 4개는 **처음부터** 이름만으로 안 잡혔다
|
|
9
|
+
# (`--skill qp` → «unable to resolve»). 레포의 어떤 검사도 이 경로를 안 보고 있었고,
|
|
10
|
+
# cross-family(codex) 가 그 이동 리뷰에서 지목했다.
|
|
11
|
+
#
|
|
12
|
+
# 🟥 이 레인은 **스킬을 실행하지 않는다.** `FH_DRY_RUN=1` 로 조립된 프롬프트만 뽑는다.
|
|
13
|
+
# (옵션 `--dry-run` 은 존재하지 않는다 — 그렇게 부르면 프롬프트로 먹혀 **실제로 돈다**.
|
|
14
|
+
# 첫 검증에서 그걸 겪었다.)
|
|
15
|
+
#
|
|
16
|
+
# 실행: bash scripts/test_fh_run_resolver_lanes.sh
|
|
17
|
+
# 🟥 이 파일의 모든 «$VAR» 는 `${VAR}` 로 쓴다 — 변수 뒤에 ASCII 가 아닌 글자(» 등)가 바로
|
|
18
|
+
# 붙으면 셸이 그 바이트를 **변수 이름에 먹어** `VAR?: unbound variable` 로 죽는다.
|
|
19
|
+
# 이 세션에 같은 결함을 두 번 냈다(한 번 고치고 새 파일에 또 썼다). 중괄호가 그 경계다.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
cd "$HERE" || exit 2
|
|
23
|
+
PASS=0; FAIL=0
|
|
24
|
+
ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
|
|
25
|
+
ng(){ printf ' ❌ %s\n' "$1"; FAIL=$((FAIL+1)); }
|
|
26
|
+
|
|
27
|
+
# 해석된 파일 경로만 뽑는다. 못 찾으면 빈 문자열.
|
|
28
|
+
# 🟥 `--backend codex` 를 **명시**한다. 안 그러면 `fh-run` 이 해석보다 **먼저** 백엔드를
|
|
29
|
+
# 자동탐지하고, `codex`·`claude` 가 없는 환경(=CI 러너)에서 `exit 10` 으로 죽는다.
|
|
30
|
+
# 초판 레인이 그걸 빼먹어 **로컬 6/6 · CI 0/6** 이 났다 — 컨트롤(F3)까지 빨개서 «해석 전에
|
|
31
|
+
# 죽는다»가 드러났고 그래서 오진하지 않았다. 명시하면 `FH_DRY_RUN=1` 이 실제 호출 전에
|
|
32
|
+
# 돌아오므로 바이너리가 없어도 된다.
|
|
33
|
+
# ⚠️ 명명된 잔여: 「백엔드 부재」가 「스킬 못 찾음」을 **가린다**(순서상 먼저 죽는다).
|
|
34
|
+
# 더 쓸모 있는 순서는 해석이 먼저지만, 그건 이 PR 의 범위가 아니라 별건이다.
|
|
35
|
+
resolve(){
|
|
36
|
+
FH_DRY_RUN=1 timeout 60 bash "$HERE/scripts/fh-run.sh" --backend codex --skill "$1" 2>&1 \
|
|
37
|
+
| /usr/bin/grep -oE 'plugins/[A-Za-z0-9_-]+/skills/[A-Za-z0-9_.-]+/SKILL\.md' | head -1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
echo "== 이름만으로 어느 플러그인이든 닿나 =="
|
|
41
|
+
R=$(resolve preprep)
|
|
42
|
+
case "$R" in
|
|
43
|
+
plugins/fh-preprep/skills/preprep/SKILL.md) ok "F1 preprep → $R (승격 후에도 이름으로 닿는다)" ;;
|
|
44
|
+
"") ng "F1 preprep 미해결 — 승격이 소비자 표면을 깼다" ;;
|
|
45
|
+
*) ng "F1 preprep 이 엉뚱한 곳으로: $R" ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
R=$(resolve qp)
|
|
49
|
+
case "$R" in
|
|
50
|
+
plugins/fh-qp/skills/qp/SKILL.md) ok "F2 qp → $R (원래 못 닿던 선재 갭도 같이 닫혔다)" ;;
|
|
51
|
+
"") ng "F2 qp 미해결 — 일반화가 fh-qp 에 안 닿는다" ;;
|
|
52
|
+
*) ng "F2 qp 가 엉뚱한 곳으로: $R" ;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
echo
|
|
56
|
+
echo "== 🟥 컨트롤 — 아무 이름이나 닿으면 위 둘은 아무것도 증명 안 한다 =="
|
|
57
|
+
OUT=$(FH_DRY_RUN=1 timeout 60 bash "$HERE/scripts/fh-run.sh" --backend codex --skill zz_no_such_skill_probe 2>&1); RC=$?
|
|
58
|
+
if [ "$RC" -ne 0 ] && printf '%s' "$OUT" | /usr/bin/grep -q 'unable to resolve'; then
|
|
59
|
+
ok "F3 없는 이름 → rc=$RC · «unable to resolve» (판별력 있음)"
|
|
60
|
+
else
|
|
61
|
+
ng "F3 없는 이름이 통과했다 (rc=$RC) — 해석기가 아무거나 잡는다"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
echo
|
|
65
|
+
echo "== 우선순위가 안 뒤집혔나 (일반화의 대가) =="
|
|
66
|
+
# fh-meta 에 실재하는 이름은 여전히 fh-meta 로 가야 한다.
|
|
67
|
+
M=$(ls plugins/fh-meta/skills 2>/dev/null | head -1)
|
|
68
|
+
if [ -n "$M" ]; then
|
|
69
|
+
R=$(resolve "$M")
|
|
70
|
+
case "$R" in
|
|
71
|
+
plugins/fh-meta/*) ok "F4 fh-meta 스킬 «${M}» 은 여전히 fh-meta 로 (우선순위 보존)" ;;
|
|
72
|
+
*) ng "F4 «${M}» 이 $R 로 갔다 — 일반화가 해석 순서를 바꿨다" ;;
|
|
73
|
+
esac
|
|
74
|
+
else
|
|
75
|
+
ng "F4 fh-meta 스킬을 못 찾았다 — 계기 오류(레포가 헐었거나 경로가 틀렸다)"
|
|
76
|
+
fi
|
|
77
|
+
C=$(ls plugins/fh-commons/skills 2>/dev/null | head -1)
|
|
78
|
+
if [ -n "$C" ]; then
|
|
79
|
+
R=$(resolve "$C")
|
|
80
|
+
case "$R" in
|
|
81
|
+
plugins/fh-commons/*) ok "F5 fh-commons 스킬 «${C}» 은 여전히 fh-commons 로" ;;
|
|
82
|
+
*) ng "F5 «${C}» 이 $R 로 갔다" ;;
|
|
83
|
+
esac
|
|
84
|
+
else
|
|
85
|
+
ng "F5 fh-commons 스킬을 못 찾았다 — 계기 오류"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo
|
|
89
|
+
echo "== 명시 지정이 여전히 이긴다 =="
|
|
90
|
+
R=$(resolve "fh-preprep:preprep")
|
|
91
|
+
case "$R" in
|
|
92
|
+
plugins/fh-preprep/skills/preprep/SKILL.md) ok "F6 plugin:name 형태도 해석된다" ;;
|
|
93
|
+
*) ng "F6 명시 지정 실패: ${R:-(없음)}" ;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
echo
|
|
97
|
+
echo "== 백엔드 바이너리가 없는 환경에서도 같은 답인가 (CI 형태) =="
|
|
98
|
+
# 🟥 이 레인이 있는 이유: 초판이 로컬에서만 통과하고 CI 에서 6/6 빨갰다. PATH 를 깎아
|
|
99
|
+
# codex·claude 를 **실제로 없앤** 상태에서 재는 것이 그 차이를 로컬에서도 재현한다.
|
|
100
|
+
# 🟥 `timeout` 이 `env` «앞»이어야 한다 — GNU coreutils 의 timeout 은 /usr/bin 에 없어서
|
|
101
|
+
# PATH 를 깎은 «뒤»에 찾으면 command-not-found 로 죽는다. 초판이 그렇게 써서 F7 이
|
|
102
|
+
# 로컬에서도 빨갰고, 그게 사실은 «이 레인이 계기 고장을 잡았다»는 증거다.
|
|
103
|
+
R7=$(timeout 60 env PATH=/usr/bin:/bin FH_DRY_RUN=1 bash "$HERE/scripts/fh-run.sh" \
|
|
104
|
+
--backend codex --skill preprep 2>&1 \
|
|
105
|
+
| /usr/bin/grep -oE 'plugins/[A-Za-z0-9_-]+/skills/[A-Za-z0-9_.-]+/SKILL\.md' | head -1)
|
|
106
|
+
case "${R7}" in
|
|
107
|
+
plugins/fh-preprep/skills/preprep/SKILL.md) ok "F7 백엔드 바이너리 0개인 환경에서도 해석된다 (CI 와 같은 답)" ;;
|
|
108
|
+
"") ng "F7 백엔드 없는 환경에서 해석 실패 — 이 레인은 로컬 전용이다(= CI 에서 빨개진다)" ;;
|
|
109
|
+
*) ng "F7 엉뚱한 곳으로: ${R7}" ;;
|
|
110
|
+
esac
|
|
111
|
+
|
|
112
|
+
echo
|
|
113
|
+
echo "════════════════════════════════════════"
|
|
114
|
+
printf ' PASS %s FAIL %s\n' "$PASS" "$FAIL"
|
|
115
|
+
echo "════════════════════════════════════════"
|
|
116
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
@@ -253,6 +253,37 @@ else
|
|
|
253
253
|
fi
|
|
254
254
|
rm -rf "$D10"
|
|
255
255
|
|
|
256
|
+
# ── P 갈래 (2026-09-13) — 파이썬 바이트코드가 발행 집합에 실리지 않나 ──────────────────
|
|
257
|
+
#
|
|
258
|
+
# 실사고: `plugins/*/skills` 가 디렉터리 항목이라 그 아래 `__pycache__/*.pyc` 를 통째로 끌어갔고,
|
|
259
|
+
# .pyc 안에는 **빌드한 머신의 절대 경로와 계정명**이 박혀 있다. 로컬 `npm pack` 이 그걸 실었다.
|
|
260
|
+
# 🟢 발행된 3.4.0·3.1.4 는 깨끗했다 — CI 가 «깨끗한 체크아웃»에서 패킹하기 때문이고, 그건
|
|
261
|
+
# **우연히 안전했던 것**이지 막혀 있던 게 아니다(폴백 = 로컬 발행 경로는 그대로 샜다).
|
|
262
|
+
# 🟥 `files[]` 가 있으면 `.npmignore` 도 `files[]` 부정 패턴도 이 자리에 안 먹는다(둘 다 실측).
|
|
263
|
+
# 그래서 막는 자리는 **`prepack`** 뿐이다.
|
|
264
|
+
echo
|
|
265
|
+
echo "== P: 발행 집합에 .pyc 가 없나 =="
|
|
266
|
+
_P_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
267
|
+
if ! /usr/bin/grep -q '"prepack"' "$_P_ROOT/package.json"; then
|
|
268
|
+
bad "P1 package.json 에 prepack 이 없다 — .pyc 를 막는 유일한 자리가 비었다"
|
|
269
|
+
else
|
|
270
|
+
ok "P1 prepack 선언 실재"
|
|
271
|
+
_PD="$_P_ROOT/plugins/fh-preprep/skills/preprep/__pycache__"
|
|
272
|
+
mkdir -p "$_PD" && printf 'x' > "$_PD/_probe_lane.cpython-313.pyc"
|
|
273
|
+
_PT=$(mktemp -d)
|
|
274
|
+
if ( cd "$_P_ROOT" && npm pack --pack-destination "$_PT" >/dev/null 2>&1 ); then
|
|
275
|
+
_n=$(tar tzf "$_PT"/*.tgz 2>/dev/null | /usr/bin/grep -c '[.]pyc$')
|
|
276
|
+
_c=$(tar tzf "$_PT"/*.tgz 2>/dev/null | /usr/bin/grep -c 'skills/preprep/lane_')
|
|
277
|
+
if [ "${_n:-x}" = "0" ]; then ok "P2 심어둔 .pyc 가 패킹본에 없다 (실측 ${_n}개)"
|
|
278
|
+
else bad "P2 .pyc ${_n}개가 실렸다 — prepack 이 안 돌거나 범위를 못 덮는다"; fi
|
|
279
|
+
if [ "${_c:-0}" -ge 5 ]; then ok "P3 컨트롤: 실물 lane 모듈은 그대로 실린다 (${_c}개) — 과삭제 아님"
|
|
280
|
+
else bad "P3 컨트롤 죽음: lane 모듈이 ${_c}개뿐 — prepack 이 너무 많이 지운다"; fi
|
|
281
|
+
else
|
|
282
|
+
bad "P2 npm pack 실패 — 계기 오류(판정 아님)"
|
|
283
|
+
fi
|
|
284
|
+
rm -rf "$_PT"; rm -f "$_PD/_probe_lane.cpython-313.pyc"
|
|
285
|
+
fi
|
|
286
|
+
|
|
256
287
|
echo "files-manifest-shipping lanes: $pass passed, $fail failed"
|
|
257
288
|
if [ "$fail" -gt 0 ]; then
|
|
258
289
|
exit 1
|