@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.
Files changed (106) hide show
  1. package/.claude/registry/agent_cards.json +1 -1
  2. package/.claude/rules/fh_4axis_gate.md +25 -0
  3. package/.claude-plugin/marketplace.json +9 -3
  4. package/AGENTS.md +2 -2
  5. package/CATALOG.md +4 -4
  6. package/CHEATSHEET.md +1 -1
  7. package/CLAUDE.md +4 -4
  8. package/README.md +2 -2
  9. package/docs/OUTPUT_EVIDENCE.md +1 -1
  10. package/docs/STANDARDS_ALIGNMENT.md +1 -1
  11. package/docs/codex-compat.md +2 -2
  12. package/knowledge/shared/harness-core/agents_md_runtime_details.md +2 -2
  13. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +30 -3
  14. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +1 -1
  15. package/knowledge/shared/harness-core/skill_quality_rubric.md +1 -1
  16. package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -4
  17. package/knowledge/shared/rules/modes_and_value.md +2 -2
  18. package/package.json +9 -1
  19. package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
  20. package/plugins/fh-commons/README.md +38 -0
  21. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  22. package/plugins/fh-meta/CHANGELOG.md +117 -0
  23. package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
  24. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +40 -0
  25. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +1 -1
  26. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +43 -5
  27. package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL.md +75 -3
  28. package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL_detail.md +2 -2
  29. package/plugins/fh-meta/skills/harvest-loop/SKILL_detail.md +2 -2
  30. package/plugins/fh-meta/skills/install-doctor/SKILL.md +1 -1
  31. package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
  32. package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
  33. package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +1 -1
  34. package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +1 -1
  35. package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
  36. package/plugins/fh-preprep/.claude-plugin/plugin.json +22 -0
  37. package/plugins/fh-preprep/README.md +24 -0
  38. package/plugins/{fh-commons → fh-preprep}/skills/preprep/SKILL.md +75 -3
  39. package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/mk_slide_fixtures.py +152 -1
  40. package/plugins/fh-preprep/skills/preprep/lane_attr_consistency.py +370 -0
  41. package/plugins/fh-preprep/skills/preprep/lane_geometry.py +339 -0
  42. package/plugins/fh-preprep/skills/preprep/lane_screen_parity.py +232 -0
  43. package/plugins/fh-preprep/skills/preprep/oox.py +305 -0
  44. package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep.py +52 -4
  45. package/plugins/fh-preprep/skills/preprep/safe_install.py +369 -0
  46. package/plugins/{fh-commons → fh-preprep}/skills/preprep/surfaces.example.yaml +29 -0
  47. package/plugins/fh-preprep/skills/preprep/test_preprep_lanes_rp.py +1417 -0
  48. package/plugins/fh-preprep/skills/preprep/usage_ledger.py +191 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
  50. package/scripts/fh-run.sh +13 -0
  51. package/scripts/field_canon_preload.sh +1 -1
  52. package/scripts/finding_fleet.sh +391 -13
  53. package/scripts/finding_pipeline.sh +370 -11
  54. package/scripts/finding_verifier.sh +31 -2
  55. package/scripts/finding_verify.py +198 -13
  56. package/scripts/frontier_digest_autopilot.sh +3 -3
  57. package/scripts/gate_shape_scan.sh +16 -2
  58. package/scripts/selfcheck.sh +12 -8
  59. package/scripts/test_fh_run_resolver_lanes.sh +116 -0
  60. package/scripts/test_files_manifest_shipping_lanes.sh +31 -0
  61. package/scripts/test_finding_pipeline_lanes.sh +1556 -4
  62. package/scripts/test_gate_shape_scan_lanes.sh +11 -0
  63. package/scripts/test_marker_crossfamily_lanes.sh +75 -3
  64. package/scripts/test_marker_standpoint_lanes.sh +31 -6
  65. package/scripts/test_preprep_adjacent_dup_lanes.sh +1 -1
  66. package/scripts/test_preprep_diagram_lanes.sh +1 -1
  67. package/scripts/test_preprep_drift_anchor.sh +27 -12
  68. package/scripts/test_preprep_drift_anchor_lanes.sh +4 -2
  69. package/scripts/test_preprep_font_lanes.sh +7 -7
  70. package/scripts/test_preprep_progression_lanes.sh +1 -1
  71. package/scripts/test_preprep_promise_lanes.sh +1 -1
  72. package/scripts/test_preprep_retired_lanes.sh +45 -2
  73. package/scripts/test_preprep_slide_refs_lanes.sh +5 -5
  74. package/scripts/test_usage_ledger_lanes.sh +243 -0
  75. package/scripts/usage_report.py +211 -0
  76. package/templates/.git-hooks/pre-commit +152 -6
  77. package/templates/local_fh_context.md +1 -1
  78. package/templates/regression_guard.sh +1 -1
  79. package/plugins/fh-commons/skills/preprep/lane_geometry.py +0 -181
  80. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +0 -184
  81. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/README.md +0 -0
  82. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/canon_terms.example.yaml +0 -0
  83. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/diagram_from_json.py +0 -0
  84. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_negative.md +0 -0
  85. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_positive.md +0 -0
  86. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  87. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  88. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/font_revert_probe.py +0 -0
  89. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_negative.md +0 -0
  90. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_positive.md +0 -0
  91. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_negative.md +0 -0
  92. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_positive.md +0 -0
  93. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/interslide_deps.py +0 -0
  94. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/jargon_terms.example.yaml +0 -0
  95. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_adjacent_dup.py +0 -0
  96. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_diagram.py +0 -0
  97. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_font.py +0 -0
  98. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_progression.py +0 -0
  99. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_promise.py +0 -0
  100. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_refs.py +0 -0
  101. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_relations.py +0 -0
  102. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/c1_baseline.txt +0 -0
  103. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/gate.py +0 -0
  104. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep_wire.py +0 -0
  105. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/presentation_checklist.md +0 -0
  106. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/test_lane_font.py +0 -0
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """사용 원장 — 하네스가 «누구에게 얼마나 쓰이는가» 를 남긴다. 표준 라이브러리만 쓴다.
4
+
5
+ ## 🟥 옵트인이다 — `FH_USAGE_LEDGER` 가 없으면 **완전 무동작**이다.
6
+
7
+ 이 파일은 npm 으로 배포되는 하네스 안에 들어간다. 기본값으로 켜 두면 소비자가 동의한 적 없는
8
+ 계측이 남의 머신에서 돌게 되고, 그건 이 저장소가 §Operational Adaptation Loop 에서 금지하는
9
+ 형태다(동의는 «부재 ≠ 승인»). 그래서 스위치는 **경로를 직접 주는 것**이고, 경로를 주는 행위
10
+ 자체가 동의의 표시다.
11
+
12
+ ```
13
+ export FH_USAGE_LEDGER="$HOME/.local/state/fh-usage" # 이 줄이 없으면 아무 일도 안 난다
14
+ export FH_USAGE_AUTHOR=1 # 저자 본인 머신에서만
15
+ ```
16
+
17
+ ## 무엇을 적고 무엇을 안 적나 — residency 가 여기서 정해진다
18
+
19
+ 적는다 : 시각(분 단위) · 하네스 이름 · 진입점 이름(화이트리스트 형태) · **actor 해시** ·
20
+ 벽시계 ms · CPU ms · 반환 코드 · 결과 부류
21
+ 🟥 안 적는다 : **인자 · 경로 · 파일명 · 티켓 id · URL · 사용자 이름 · 호스트 이름.**
22
+ 접근이 제한된 환경에서도 돌 수 있는 하네스라 이건 취향이 아니라 경계다. 인자 한 칸을
23
+ 열어두면 그 칸으로 조직 내부 식별자가 흘러나간다 — 그래서 인자를 받는 필드가 **아예 없다**.
24
+
25
+ `actor` = sha256(salt + 사용자 + 호스트)[:12]. salt 는 원장 디렉터리 안 `.salt`(0600)에만 있고
26
+ 어디로도 안 나간다. ⚠️ **한계를 이름으로 남긴다**: 같은 사람이 머신 둘을 쓰면 **2 명**으로 세고,
27
+ 공용 머신을 여럿이 쓰면 **1 명**으로 센다. 그래서 이 값은 «사람 수»가 아니라 «머신-사용자 수»다.
28
+
29
+ ## degrade 방향 — 호스트를 절대 안 죽인다, 그러나 조용히 죽지도 않는다
30
+
31
+ 원장 쓰기가 실패해도 예외를 호스트로 올리지 않는다(관측이 대상을 죽이면 안 된다). 대신
32
+ 같은 디렉터리의 `.errors` 를 1 증가시키고, 리포터가 그 수를 **커버리지 옆에 같이 찍는다** —
33
+ 「미측정을 0 으로 렌더하는」 부류를 안 만들기 위해서다.
34
+ """
35
+ import hashlib
36
+ import json
37
+ import os
38
+ import time
39
+
40
+ _SCHEMA = 1
41
+ _ENV_DIR = "FH_USAGE_LEDGER"
42
+ _ENV_AUTHOR = "FH_USAGE_AUTHOR"
43
+ _ENV_SALT = "FH_USAGE_SALT"
44
+
45
+
46
+ def _dir():
47
+ d = os.environ.get(_ENV_DIR, "").strip()
48
+ return d or None
49
+
50
+
51
+ def _salt(d):
52
+ """salt 는 env 우선, 없으면 원장 디렉터리 안 파일. 없으면 만든다(0600)."""
53
+ s = os.environ.get(_ENV_SALT, "").strip()
54
+ if s:
55
+ return s
56
+ p = os.path.join(d, ".salt")
57
+ try:
58
+ # 🟥 cross-family codex 지목(2026-09-13, B-3): `.salt` 가 FIFO 면 열기가 **블록**한다.
59
+ # 이 함수는 `observe()` 의 finally 에서 불리므로, 그 순간 관측이 호스트를 멈춰 세운다.
60
+ # 관측은 대상의 타이밍 계약도 바꾸면 안 된다 → 정규 파일이 아니면 읽지 않는다.
61
+ # (O_NONBLOCK 은 FIFO 열기가 막히는 것까지 같이 막는 두 번째 자물쇠다.)
62
+ import stat as _stat
63
+ st = os.lstat(p)
64
+ if not _stat.S_ISREG(st.st_mode):
65
+ raise OSError("salt path is not a regular file")
66
+ fd = os.open(p, os.O_RDONLY | getattr(os, "O_NONBLOCK", 0))
67
+ try:
68
+ v = os.read(fd, 4096).decode("utf-8", "replace").strip()
69
+ finally:
70
+ os.close(fd)
71
+ if v:
72
+ return v
73
+ except FileNotFoundError:
74
+ pass
75
+ except Exception:
76
+ # 정규 파일이 아니거나 읽을 수 없다 → 아래 O_EXCL 생성이 실패하고 record() 가
77
+ # 그 예외를 잡아 `.errors` 를 올린다. 행은 안 남고 호스트는 안 멈춘다(fail-closed).
78
+ pass
79
+ import secrets
80
+ v = secrets.token_hex(16)
81
+ fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
82
+ try:
83
+ os.write(fd, v.encode())
84
+ finally:
85
+ os.close(fd)
86
+ return v
87
+
88
+
89
+ def _actor(d):
90
+ raw = "%s|%s" % (os.environ.get("USER") or os.environ.get("LOGNAME") or "?",
91
+ os.uname().nodename if hasattr(os, "uname") else "?")
92
+ return hashlib.sha256((_salt(d) + "|" + raw).encode()).hexdigest()[:12]
93
+
94
+
95
+ def _entry_name(name):
96
+ """진입점 이름만 받는다. 형태를 벗어나면 «other» — 인자가 이름에 섞여 들어오는 것 차단."""
97
+ ok = name and len(name) <= 40 and all(c.isalnum() or c in "_.-" for c in name)
98
+ return name if ok else "other"
99
+
100
+
101
+ def _bump_errors(d):
102
+ try:
103
+ p = os.path.join(d, ".errors")
104
+ n = 0
105
+ try:
106
+ with open(p) as f:
107
+ n = int((f.read() or "0").strip() or 0)
108
+ except Exception:
109
+ n = 0
110
+ with open(p, "w") as f:
111
+ f.write(str(n + 1))
112
+ except Exception:
113
+ pass
114
+
115
+
116
+ def record(harness, entry, wall_ms, cpu_ms, rc, outcome, counts=None):
117
+ """원장에 한 줄. 실패해도 예외를 밖으로 안 낸다."""
118
+ d = _dir()
119
+ if not d:
120
+ return False
121
+ try:
122
+ os.makedirs(d, exist_ok=True)
123
+ row = {
124
+ "v": _SCHEMA,
125
+ # 🟥 분 단위로 자른다 — 초 단위 타임스탬프는 개인 행동 패턴을 더 세밀히 남긴다.
126
+ "ts": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
127
+ "h": _entry_name(harness),
128
+ "e": _entry_name(entry),
129
+ "actor": _actor(d),
130
+ "author": 1 if os.environ.get(_ENV_AUTHOR, "").strip() in ("1", "true", "yes") else 0,
131
+ "wall_ms": int(wall_ms),
132
+ "cpu_ms": int(cpu_ms),
133
+ "rc": rc if isinstance(rc, int) else None,
134
+ "out": outcome if outcome in ("DONE", "EXIT", "ERROR") else "ERROR",
135
+ # LLM 호출이 있는 경로만 채운다. 없으면 null — 🟥 0 이 아니다.
136
+ "llm": None,
137
+ }
138
+ if isinstance(counts, dict):
139
+ row["n"] = {k: v for k, v in counts.items()
140
+ if isinstance(k, str) and isinstance(v, int) and len(k) <= 16}
141
+ path = os.path.join(d, "%s-%s.jsonl" % (row["h"], time.strftime("%Y-%m", time.gmtime())))
142
+ # append 는 O_APPEND 라 한 줄이 4 KiB 미만이면 동시 실행에서도 안 섞인다(POSIX).
143
+ # 🟥 0600 으로 만든다 — 행에 actor 해시와 사용 시각이 남으므로 기본 umask(0644)로
144
+ # 두면 같은 머신의 다른 계정이 읽는다. `.salt` 만 잠그고 원장을 열어두면 반쪽이다.
145
+ # 같은 이유로 원장 파일도 정규 파일일 때만 쓴다(FIFO 로 바꿔치기하면 append 가 막힌다).
146
+ if os.path.exists(path):
147
+ import stat as _stat
148
+ if not _stat.S_ISREG(os.lstat(path).st_mode):
149
+ raise OSError("ledger path is not a regular file")
150
+ fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT
151
+ | getattr(os, "O_NONBLOCK", 0), 0o600)
152
+ try:
153
+ os.write(fd, (json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8"))
154
+ finally:
155
+ os.close(fd)
156
+ return True
157
+ except Exception:
158
+ # 🟥 여기서 예외를 올리지 않는 것은 «관측이 대상을 죽이면 안 되기» 때문이고, 그래서
159
+ # default-toward-PASS 로 보인다. 정당화는 **조용하지 않다**는 것이다 — 실패는
160
+ # `.errors` 로 세어지고 리포터가 커버리지 옆에 같이 찍는다(안 막는 것과 안 보이는 것은 다르다).
161
+ # ⚠️ 닫히지 않은 잔여를 이름으로 남긴다: 디렉터리가 통째로 못 쓰이면 `_bump_errors` 도
162
+ # 같이 실패해서 **카운터조차 안 남는다**. 그 경우는 리포터가 쓰기 권한을 직접 보고 잡는다
163
+ # (usage_report.py `--dir` 쓰기 가능 검사). 원장 혼자서는 못 닫는다.
164
+ _bump_errors(d)
165
+ return False
166
+
167
+
168
+ def observe(harness, entry, fn, *args, **kwargs):
169
+ """`fn` 을 그대로 실행하고 반환값·예외를 **건드리지 않고** 통과시킨다.
170
+
171
+ 🟥 이 함수는 판정을 만들지도 바꾸지도 않는다. 종료코드는 `fn` 이 정하고 여기는 읽기만 한다.
172
+ 그래서 게이트 코드가 아니다 — 관측이 판정을 바꾸는 순간 그 숫자는 아무 의미가 없다.
173
+ """
174
+ t0, c0 = time.monotonic(), time.process_time()
175
+ rc, outcome = None, "ERROR"
176
+ try:
177
+ rc = fn(*args, **kwargs)
178
+ outcome = "DONE"
179
+ return rc
180
+ except SystemExit as e:
181
+ rc = e.code if isinstance(e.code, int) else None
182
+ outcome = "EXIT"
183
+ raise
184
+ except BaseException:
185
+ outcome = "ERROR"
186
+ raise
187
+ finally:
188
+ record(harness, entry,
189
+ (time.monotonic() - t0) * 1000.0,
190
+ (time.process_time() - c0) * 1000.0,
191
+ rc, outcome)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-qp",
3
- "version": "3.2.0",
3
+ "version": "3.5.0",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
package/scripts/fh-run.sh CHANGED
@@ -153,10 +153,23 @@ resolve_unit() {
153
153
  if [[ -n "$plugin" ]]; then
154
154
  candidates+=("$FH_ROOT/plugins/$plugin/skills/$bare/SKILL.md")
155
155
  fi
156
+ # 🟥 2026-09-13 — 종전엔 이 목록이 fh-meta·fh-commons **둘만** 하드코딩했다. 그래서
157
+ # `fh-qp` 의 스킬 4개는 **처음부터** 이름만으로 안 잡혔고(선재 갭 — `--skill qp` 가
158
+ # «unable to resolve»), preprep 을 자기 플러그인으로 승격하자 거기에 하나가 더 붙었다.
159
+ # cross-family(codex)가 그 이동에서 이걸 지목했다 — 레포 어느 검사도 이 해석 경로를
160
+ # 안 보고 있었다. ⇒ 목록을 **플러그인 전체로 일반화**한다.
161
+ # 우선순위는 보존한다: 명시 지정 → fh-meta → fh-commons → 나머지(사전순, 결정적).
162
+ # 이름이 겹칠 때 종전 해석이 안 바뀌게 하려는 것이고, 정렬은 파일시스템 순서에
163
+ # 기대지 않기 위해서다.
156
164
  candidates+=(
157
165
  "$FH_ROOT/plugins/fh-meta/skills/$bare/SKILL.md"
158
166
  "$FH_ROOT/plugins/fh-commons/skills/$bare/SKILL.md"
159
167
  )
168
+ local _p
169
+ while IFS= read -r _p; do
170
+ case "$_p" in */fh-meta/*|*/fh-commons/*) continue ;; esac
171
+ candidates+=("$_p")
172
+ done < <(ls -d "$FH_ROOT"/plugins/*/skills/"$bare"/SKILL.md 2>/dev/null | sort)
160
173
  for f in "${candidates[@]}"; do
161
174
  [[ -f "$f" ]] && printf "%s\n" "$f" && return 0
162
175
  done
@@ -149,7 +149,7 @@ skill_canon_emit() {
149
149
  SKEOF
150
150
  [ -z "$SENT_DIR" ] || : > "$SENT_DIR/skill-$_sk" 2>/dev/null || true
151
151
  }
152
- skill_canon_emit preprep "plugins/fh-commons/skills/preprep" \
152
+ skill_canon_emit preprep "plugins/fh-preprep/skills/preprep" \
153
153
  "발표" "장표" "슬라이드" "덱" "대본" "리허설" "presentation" "keynote"
154
154
 
155
155
  # 매핑된 프로젝트 = tracks/ 하위 디렉토리(언더스코어 접두는 메타라 제외)