@chrono-meta/fh-gate 2.6.0 → 2.8.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/rules/fh_4axis_gate.md +26 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +28 -3
- package/CLAUDE.md +146 -168
- package/README.ja.md +144 -24
- package/README.ko.md +135 -22
- package/README.md +204 -27
- package/README.zh.md +126 -21
- package/docs/ETHOS.md +10 -3
- package/knowledge/shared/dialogue/ai_dialogue_playbook.md +131 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +90 -0
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +75 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -3
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +92 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +196 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/knowledge/shared/rules/multi_session_close_protocol.md +7 -3
- package/package.json +15 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +157 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -0
- package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +37 -0
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
- package/scripts/adapters/peer_resolve.sh +58 -6
- package/scripts/branch_claim.sh +3 -1
- package/scripts/cluster_capability_scan.sh +42 -13
- package/scripts/digest_landing_check.sh +142 -2
- package/scripts/fh_hub_identity.sh +83 -0
- package/scripts/fh_session_load.sh +53 -5
- package/scripts/fh_track_resolve.sh +114 -0
- package/scripts/field_canon_preload.sh +50 -5
- package/scripts/listing_watch.sh +187 -0
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/prior_art_prompt.sh +168 -0
- package/scripts/psa_scan_lib.sh +201 -15
- package/scripts/residency_admission_check.sh +204 -0
- package/scripts/selfcheck.sh +103 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -0
- package/scripts/test_listing_watch_lanes.sh +162 -0
- package/scripts/test_marker_defense_lanes.sh +152 -0
- package/scripts/test_marker_soul_check_lanes.sh +211 -0
- package/scripts/test_prior_art_prompt_lanes.sh +128 -0
- package/scripts/test_psa_singlefile_lanes.sh +351 -1
- package/scripts/test_residency_admission_lanes.sh +60 -0
- package/scripts/test_track_resolve_lanes.sh +158 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +40 -0
- package/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- package/templates/settings.PriorArt.snippet.json +44 -0
|
@@ -21,6 +21,34 @@
|
|
|
21
21
|
#
|
|
22
22
|
# 종료: 항상 0 · 보고는 stdout. (비영 종료는 stdout 이 폐기되고 stderr 도 안 전달된다 —
|
|
23
23
|
# `[[feedback_hook_nonzero_exit_is_silent]]`.)
|
|
24
|
+
# ── track→repo 해석: 단일 소스 = scripts/fh_track_resolve.sh ──────────────────
|
|
25
|
+
# 🟥 강등 블록은 세 소비자(fh_session_load · field_canon_preload · cluster_capability_scan)에
|
|
26
|
+
# **문자 그대로 동일**해야 한다. 2026-08-21 적대검증 HIGH-2: 초판은 파일마다 별칭을
|
|
27
|
+
# 1종/2종/3종으로 다르게 봤고, 그건 «강등 경로가 F-1 결함(갈라진 정규화기)의 완전한
|
|
28
|
+
# 복제본» 이라는 뜻이었다. 이제 강등은 **별칭 0종 + 큰 소리**다 — 덜 유용하지만
|
|
29
|
+
# 세 파일이 같은 답을 내고, 무엇보다 **조용하지 않다**. skipped 를 passed 로 렌더하지
|
|
30
|
+
# 않는다는 이 저장소 규율 그대로다. [[feedback_not_found_is_not_zero_family]]
|
|
31
|
+
_FH_TRLIB="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/fh_track_resolve.sh"
|
|
32
|
+
# shellcheck source=scripts/fh_track_resolve.sh
|
|
33
|
+
[ -f "$_FH_TRLIB" ] && . "$_FH_TRLIB"
|
|
34
|
+
# 🟥 `type` 는 **존재**만 재고 **정합**은 못 잰다(잘린 파일·구버전·환경에서 export -f 된 동명
|
|
35
|
+
# 함수는 전부 통과한다 — 적대검증 LOW-3). 그래서 라이브러리가 API 버전을 선언하고
|
|
36
|
+
# 소비자가 그걸 확인한다. 채널을 타입으로 만드는 것이지 성실성에 기대는 게 아니다.
|
|
37
|
+
if ! type fh_resolve_track_root >/dev/null 2>&1 || [ "${FH_TRACK_RESOLVE_API:-}" != "1" ]; then
|
|
38
|
+
printf '⚠️ [track-resolve] DEGRADED — fh_track_resolve.sh 부재 또는 API 불일치. 별칭 해석 없음(밑줄→하이픈·-dev 접미 미적용). 이건 «해당 없음»이 아니라 **미해석**이다.\n'
|
|
39
|
+
fh_resolve_track_root() {
|
|
40
|
+
case "${1-}" in '' |*/*|*'|'*|*'..'* ) printf '|ARGS:bad-name'; return 3 ;; esac
|
|
41
|
+
[ -n "${2-}" ] || { printf '|ARGS:empty-root'; return 3; }
|
|
42
|
+
case "${3:-dir}" in
|
|
43
|
+
git) [ -d "$2/$1/.git" ] && { printf '%s|' "$2/$1"; return 0; } ;;
|
|
44
|
+
dir) [ -d "$2/$1" ] && { printf '%s|' "$2/$1"; return 0; } ;;
|
|
45
|
+
*) printf '|ARGS:bad-pred'; return 3 ;;
|
|
46
|
+
esac
|
|
47
|
+
printf '%s|UNRESOLVED' "$2/$1"
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
fi
|
|
51
|
+
|
|
24
52
|
set -uo pipefail
|
|
25
53
|
|
|
26
54
|
HUB="${CLAUDE_PROJECT_DIR:-${HOME:-}/projects/forge-harness}"
|
|
@@ -94,11 +122,28 @@ for name in $mapped; do
|
|
|
94
122
|
printf '%s' "$PROMPT" | grep -qiF -- "$name" || continue # -F: 이름의 regex 문자 오탐/미탐 방지
|
|
95
123
|
[ -n "$SENT_DIR" ] && [ -e "$SENT_DIR/$name" ] && continue # 이 세션에서 이미 띄웠다
|
|
96
124
|
|
|
97
|
-
# 레포
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
# 레포 해석은 scripts/fh_track_resolve.sh 가 단일 소스다 (2026-08-21 F-1).
|
|
126
|
+
# 여기 있던 «이름 그대로 → -dev 접미» 2종은 `tracks/the_bible`(→the-bible)을 **무음으로
|
|
127
|
+
# 버렸다** — 밑줄→하이픈 별칭이 없었기 때문이다. 같은 세션에서 cluster_capability_scan.sh 는
|
|
128
|
+
# 그걸 풀고 있었으므로, 갈라진 정규화기가 «한쪽만 통과하는 입력» 을 만든 형태였다.
|
|
129
|
+
# 술어는 `git` 을 유지한다 — 이 훅은 정본 파일을 읽히므로 실제 레포여야 한다.
|
|
130
|
+
rr=$(fh_resolve_track_root "$name" "$PROJ_ROOT" git)
|
|
131
|
+
repo="${rr%|*}"; rnote="${rr##*|}"
|
|
132
|
+
case "$rnote" in
|
|
133
|
+
UNRESOLVED) continue ;;
|
|
134
|
+
ARGS:*)
|
|
135
|
+
printf '⚠️ [field-canon] 트랙 이름 «%s» 거부(%s) — «레포 없음» 이 아니라 **전제 파손**이다.\n' \
|
|
136
|
+
"$name" "${rnote#ARGS:}"
|
|
137
|
+
continue ;;
|
|
138
|
+
# 🟥 여럿이 맞으면 고르지 않는다 — 어느 레포의 정본을 실었는지 모르게 되는 게 더 나쁘다.
|
|
139
|
+
# 그러나 **고르지 않는 것과 말하지 않는 것은 다르다** (적대검증 MED-3). 라이브러리가
|
|
140
|
+
# 이 값을 «사람이 매핑을 정리해야 한다» 로 정의했는데 그 판정을 사람에게 안 전달하면
|
|
141
|
+
# 판정이 없는 것과 같다 — 이 훅의 계약은 「항상 exit 0 · stdout 보고」라 말할 자리가 있다.
|
|
142
|
+
AMBIGUOUS:*)
|
|
143
|
+
printf '⚠️ [field-canon] 트랙 «%s» 에 레포가 여럿 맞는다(%s) — 고르지 않았다. 정본을 안 실었으니 «없음»이 아니라 **미해석**이다. 매핑을 정리해라.\n' \
|
|
144
|
+
"$name" "${rnote#AMBIGUOUS:}"
|
|
145
|
+
continue ;;
|
|
146
|
+
esac
|
|
102
147
|
[ -n "$repo" ] || continue
|
|
103
148
|
|
|
104
149
|
# **실재하는 것만** 싣는다. 없는 파일을 가리키면 다음 사람이 그 지시를 못 믿게 된다.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# listing_watch.sh — 외부 큐레이션 목록에 낸 제출의 «조용한 상태 전이»를 잡는다.
|
|
3
|
+
#
|
|
4
|
+
# 왜 있나 (실측 근거, 2026-08-19):
|
|
5
|
+
# `awesome-claude-code` #1961 이 2026-06-07 에 validation-PASSED 였다가, 우리가 아무것도
|
|
6
|
+
# 안 건드린 채 2026-08-15 에 **validation-FAILED 로 뒤집혔다** — 그쪽이 허용 카테고리
|
|
7
|
+
# 목록을 바꾸고 봇이 기존 이슈를 재검증하면서 떨어진 것이다. 아무도 안 알려줬고,
|
|
8
|
+
# `validation-failed` 는 «리뷰 대기열에서 빠진» 상태다. 4일 뒤 **우연히** 발견했다.
|
|
9
|
+
# ⇒ 통과는 영구적이지 않고, 알림은 오지 않는다. 그래서 이쪽에서 스냅샷을 떠서 대조한다.
|
|
10
|
+
#
|
|
11
|
+
# 설계 규율 (전부 이 저장소의 실측에서 나온 것):
|
|
12
|
+
# · **항상 exit 0 + stdout 보고** — 훅은 비영 종료 시 stdout 이 버려지고 stderr 도 안 온다
|
|
13
|
+
# (`feedback_hook_nonzero_exit_is_silent`). 이 스크립트는 판정을 «출력»하지 종료코드로
|
|
14
|
+
# 말하지 않는다.
|
|
15
|
+
# · **미측정을 0 으로 접지 않는다** — 상태는 넷이다: DISABLED / SKIPPED / FAILED / EXECUTED
|
|
16
|
+
# (`feedback_not_found_is_not_zero_family`). `gh` 가 없으면 «통과» 가 아니라 «SKIPPED» 다.
|
|
17
|
+
# · **파이프로 판정하지 않는다** — `out=$(cmd); rc=$?` 형태만 쓴다
|
|
18
|
+
# (`feedback_pipefail_fallback_disarms_guard`).
|
|
19
|
+
# · **부재 주장엔 컨트롤** — `--selftest` 가 known-positive/known-negative 를 돌린다
|
|
20
|
+
# (`feedback_absence_measurement_needs_control`).
|
|
21
|
+
# · bash 3.2 (macOS 기본) 호환 — 연관배열 안 쓴다.
|
|
22
|
+
#
|
|
23
|
+
# 채널 목록은 **이 파일에 안 박는다** — 운영자의 실제 제출은 사설 정보다.
|
|
24
|
+
# 매니페스트: tracks/_meta/listing_watch_channels.tsv (gitignored)
|
|
25
|
+
# 형식(TAB 구분, `#` 주석): owner/repo <TAB> number <TAB> pr|issue <TAB> 목록에서_찾을_문자열
|
|
26
|
+
#
|
|
27
|
+
# 사용: bash scripts/listing_watch.sh [--force] [--selftest]
|
|
28
|
+
|
|
29
|
+
HUB="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
30
|
+
MANIFEST="$HUB/tracks/_meta/listing_watch_channels.tsv"
|
|
31
|
+
STATE="$HUB/tracks/_meta/.listing_watch_state.tsv"
|
|
32
|
+
THROTTLE_H=24
|
|
33
|
+
FORCE=0; SELFTEST=0
|
|
34
|
+
for a in "$@"; do
|
|
35
|
+
case "$a" in --force) FORCE=1 ;; --selftest) SELFTEST=1 ;; esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
say() { printf '%s\n' "$*"; }
|
|
39
|
+
|
|
40
|
+
# 🟥 **`stat -f %m` 을 «먼저» 쓰면 리눅스에서 조용히 깨진다** — GNU stat 의 `-f` 는
|
|
41
|
+
# «파일시스템 정보»라 **실패가 아니라 성공하며 다른 값을 뱉고**, 그래서 `||` 폴백이 안 탄다.
|
|
42
|
+
# CI 가 실제로 그렇게 잡았다(`Type: ext2/ext3 … syntax error in expression`).
|
|
43
|
+
# ⇒ 순서를 뒤집는 것으로 끝내지 않고 **숫자인지 검증**한다. 순서만 바꾸면 반대 플랫폼에서
|
|
44
|
+
# 같은 얼굴이 다시 난다. `[[feedback_wiring_surfaces_hidden_failures]]`
|
|
45
|
+
_mtime() {
|
|
46
|
+
local m
|
|
47
|
+
m=$(stat -c %Y "$1" 2>/dev/null)
|
|
48
|
+
case "$m" in ''|*[!0-9]*) m=$(stat -f %m "$1" 2>/dev/null) ;; esac
|
|
49
|
+
case "$m" in ''|*[!0-9]*) m=0 ;; esac
|
|
50
|
+
printf '%s' "$m"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# 🟥 grep 패턴의 `\t` 는 **이식되지 않는다** — GNU grep 은 리터럴 `t` 로 읽어 0건이 되고,
|
|
54
|
+
# 그러면 「FAILED 를 매번 보고한다」는 규율이 리눅스에서만 조용히 죽는다(CI 적발).
|
|
55
|
+
# 실제 탭 문자를 만들어 `-F` 로 쓴다.
|
|
56
|
+
TAB=$(printf '\t')
|
|
57
|
+
|
|
58
|
+
# 🟥 테스트 seam: 레인이 «gh 없는 호스트» 를 흉내내려고 PATH 를 깎으면 호스트에 따라
|
|
59
|
+
# 결과가 갈린다(리눅스의 /usr/bin 에 gh 가 있을 수 있다 — cross-family 지목).
|
|
60
|
+
# 명령 이름을 주입 가능하게 두면 레인이 호스트와 무관해진다.
|
|
61
|
+
GH="${LISTING_WATCH_GH:-gh}"
|
|
62
|
+
|
|
63
|
+
# ── 도구 가용성: 없으면 «SKIPPED, not passed» ─────────────────────────────
|
|
64
|
+
if ! command -v "$GH" >/dev/null 2>&1; then
|
|
65
|
+
say " ⚠️ [listing-watch] SKIPPED (not passed) — \`$GH\` 없음. 등재 상태를 **확인하지 못했다**."
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ── 한 채널의 현재 상태를 한 줄로 (owner/repo#num<TAB>STATE<TAB>labels<TAB>listed) ──
|
|
70
|
+
probe() { # $1=repo $2=num $3=kind $4=needle
|
|
71
|
+
local repo="$1" num="$2" kind="$3" needle="$4" st lab listed out rc
|
|
72
|
+
# 🟥 라벨 «개수» 를 같이 싣는다 — join(",") 만으로는 ["a,b"](1개) 와 ["a","b"](2개) 가
|
|
73
|
+
# 구분이 안 돼 진짜 라벨 전이가 스냅샷에서 같게 비친다(cross-family 지목).
|
|
74
|
+
out=$($GH api "repos/$repo/issues/$num" --jq '"\(.state)|\(.pull_request.merged_at // "null")|\([.labels[].name]|length)|\([.labels[].name] | sort | join(","))"' 2>&1); rc=$?
|
|
75
|
+
if [ $rc -ne 0 ]; then printf '%s#%s\tFAILED\t-\t-\n' "$repo" "$num"; return 0; fi
|
|
76
|
+
st=$(printf '%s' "$out" | cut -d'|' -f1)
|
|
77
|
+
local merged n_lab; merged=$(printf '%s' "$out" | cut -d'|' -f2); n_lab=$(printf '%s' "$out" | cut -d'|' -f3)
|
|
78
|
+
lab=$(printf '%s' "$out" | cut -d'|' -f4-)
|
|
79
|
+
[ "$merged" != "null" ] && st="MERGED"
|
|
80
|
+
[ -z "$lab" ] && lab="-"
|
|
81
|
+
lab="${n_lab}:${lab}"
|
|
82
|
+
listed="unknown"
|
|
83
|
+
if [ -n "$needle" ] && [ "$needle" != "-" ]; then
|
|
84
|
+
local body brc dec
|
|
85
|
+
body=$($GH api "repos/$repo/contents/README.md" --jq '.content' 2>/dev/null); brc=$?
|
|
86
|
+
if [ $brc -eq 0 ] && [ -n "$body" ]; then
|
|
87
|
+
# base64 플래그가 갈린다: GNU/신 macOS 는 -d, 구 macOS 는 -D. 둘 다 시도한다.
|
|
88
|
+
dec=$(printf '%s' "$body" | base64 -d 2>/dev/null) || true
|
|
89
|
+
[ -z "$dec" ] && dec=$(printf '%s' "$body" | base64 -D 2>/dev/null) || true
|
|
90
|
+
if [ -z "$dec" ]; then
|
|
91
|
+
listed="unreadable" # 🟥 디코드 실패를 «absent» 로 접으면 계기 고장이 «미등재» 로 읽힌다
|
|
92
|
+
elif printf '%s' "$dec" | grep -qF -- "$needle"; then listed="LISTED"
|
|
93
|
+
else listed="absent"; fi
|
|
94
|
+
else
|
|
95
|
+
listed="unreadable" # ← «없음» 이 아니라 «못 읽었음». 접지 않는다
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
printf '%s#%s\t%s\t%s\t%s\n' "$repo" "$num" "$st" "$lab" "$listed"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ── 컨트롤: 계기가 살아 있는지 (known-positive / known-negative) ──────────
|
|
102
|
+
if [ "$SELFTEST" = "1" ]; then
|
|
103
|
+
say "[listing-watch selftest] 컨트롤 2종"
|
|
104
|
+
kp=$(probe "walkinglabs/awesome-harness-engineering" 33 pr "forge-harness")
|
|
105
|
+
say " known-positive walkinglabs#33 (MERGED 로 알려짐): $kp"
|
|
106
|
+
case "$kp" in *MERGED*) say " ✅ 양성 검출" ;; *) say " ❌ 계기 고장 — 알려진 MERGED 를 못 읽었다" ;; esac
|
|
107
|
+
kn=$(probe "walkinglabs/awesome-harness-engineering" 99999999 pr "-")
|
|
108
|
+
say " known-negative 없는 번호 #99999999: $kn"
|
|
109
|
+
case "$kn" in *FAILED*) say " ✅ 없는 것을 «없다»로 (지어내지 않음)" ;; *) say " ❌ 없는 것에 상태를 만들어냈다" ;; esac
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# ── 매니페스트 없으면 DISABLED (기능 자체가 안 켜진 것 — 실패가 아니다) ──
|
|
114
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
115
|
+
say " ℹ️ [listing-watch] DISABLED — 채널 매니페스트 없음 ($MANIFEST)."
|
|
116
|
+
say " 켜려면 TAB 구분으로: owner/repo <TAB> 번호 <TAB> pr|issue <TAB> 목록에서_찾을_문자열"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# ── 스로틀: 24h 안에 돌았으면 조용히 넘어간다 ────────────────────────────
|
|
121
|
+
if [ "$FORCE" != "1" ] && [ -f "$STATE" ]; then
|
|
122
|
+
now=$(date +%s)
|
|
123
|
+
mt=$(_mtime "$STATE")
|
|
124
|
+
if [ $(( (now - mt) / 3600 )) -lt "$THROTTLE_H" ]; then exit 0; fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
NEW=$(mktemp); CHANGED=0; ROWS=0
|
|
128
|
+
# 🟥 `IFS=$'\t' read` 를 쓰지 않는다 — 탭은 IFS 공백류라 **연속 탭이 합쳐져** 빈 칸이 있는
|
|
129
|
+
# 행에서 필드가 밀린다(cross-family 재현). 줄을 통째로 읽고 `cut` 으로 자른다.
|
|
130
|
+
# 🟥 `|| [ -n "$line" ]` 이 없으면 **개행 없는 마지막 행이 통째로 누락**된다(재현: 2행 중 1행).
|
|
131
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
132
|
+
case "$line" in ''|\#*) continue ;; esac
|
|
133
|
+
repo=$(printf '%s' "$line" | cut -f1)
|
|
134
|
+
num=$(printf '%s' "$line" | cut -f2)
|
|
135
|
+
kind=$(printf '%s' "$line" | cut -f3)
|
|
136
|
+
needle=$(printf '%s' "$line" | cut -f4)
|
|
137
|
+
case "$repo" in ''|\#*) continue ;; esac
|
|
138
|
+
[ -z "$num" ] && continue
|
|
139
|
+
ROWS=$((ROWS+1))
|
|
140
|
+
line=$(probe "$repo" "$num" "${kind:-pr}" "${needle:--}")
|
|
141
|
+
printf '%s\n' "$line" >> "$NEW"
|
|
142
|
+
key=$(printf '%s' "$line" | cut -f1)
|
|
143
|
+
if [ -f "$STATE" ]; then
|
|
144
|
+
old=$(grep -F "$key " "$STATE" 2>/dev/null | head -1)
|
|
145
|
+
if [ -n "$old" ] && [ "$old" != "$line" ]; then
|
|
146
|
+
[ "$CHANGED" = "0" ] && say "" && say "🔔 [listing-watch] **등재 채널 상태가 바뀌었다** — 아무도 안 알려주는 종류다"
|
|
147
|
+
CHANGED=$((CHANGED+1))
|
|
148
|
+
say " $key"
|
|
149
|
+
say " 이전: $(printf '%s' "$old" | cut -f2-)"
|
|
150
|
+
say " 지금: $(printf '%s' "$line" | cut -f2-)"
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
done < "$MANIFEST"
|
|
154
|
+
|
|
155
|
+
if [ "$ROWS" = "0" ]; then
|
|
156
|
+
say " ℹ️ [listing-watch] DISABLED — 매니페스트에 유효한 채널 행이 0개다."
|
|
157
|
+
rm -f "$NEW"; exit 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# 🟥 **FAILED 는 «변화» 가 아니라 «상태» 로 매번 보고한다.** 안 그러면 인증 만료가 첫 실행에
|
|
161
|
+
# 스냅샷으로 굳고, 이후 FAILED==FAILED 라 **영구 무출력**이 된다 — 이 스크립트 헤더가
|
|
162
|
+
# 스스로 금지한 `not_found ≠ 0` 을 스크립트 자신이 저지르고 있었다(cross-family 지목, 재현됨).
|
|
163
|
+
# 🟥 `grep -c ... || echo 0` 을 쓰지 않는다: grep -c 는 무매치에서 **"0" 을 출력하고 exit 1** 이라
|
|
164
|
+
# 폴백이 "0\n0" 을 만들고 `[` 가 «integer expression expected» 로 죽는다(내 수리가 낸 결함).
|
|
165
|
+
# `[[feedback_pipefail_fallback_disarms_guard]]` 일가 — 값을 먼저 잡고 따로 판정한다.
|
|
166
|
+
NFAIL=$(grep -cF "${TAB}FAILED${TAB}" "$NEW" 2>/dev/null)
|
|
167
|
+
case "$NFAIL" in ''|*[!0-9]*) NFAIL=0 ;; esac
|
|
168
|
+
if [ "$NFAIL" -gt 0 ]; then
|
|
169
|
+
say ""
|
|
170
|
+
say " ⚠️ [listing-watch] 채널 $NFAIL 개를 **읽지 못했다**(FAILED) — «변화 없음» 이 아니다."
|
|
171
|
+
grep -F "${TAB}FAILED${TAB}" "$NEW" 2>/dev/null | while IFS= read -r l; do say " $(printf '%s' "$l" | cut -f1)"; done
|
|
172
|
+
say " 흔한 원인: gh 인증 만료(\`gh auth status\`) · 레포/번호 오타 · 비공개 전환."
|
|
173
|
+
say " 🟥 이 줄이 안 보일 때까지는 그 채널의 상태를 **모르는 것**이다."
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if [ ! -f "$STATE" ]; then
|
|
177
|
+
say " ℹ️ [listing-watch] 최초 실행 — 채널 $ROWS 개 스냅샷을 떴다. 다음 실행부터 «변화»를 낸다."
|
|
178
|
+
elif [ "$CHANGED" -gt 0 ]; then
|
|
179
|
+
say " ⚠️ 통과는 영구적이지 않다 — 남의 스키마가 바뀌면 과거 제출이 소급 무효가 된다."
|
|
180
|
+
say " 새로 내기 전에 라벨 이력부터: gh api repos/<r>/issues/<n>/timeline"
|
|
181
|
+
fi
|
|
182
|
+
if ! mv "$NEW" "$STATE" 2>/dev/null; then
|
|
183
|
+
say " ⚠️ [listing-watch] 스냅샷 **저장 실패** ($STATE) — 다음 실행이 이번 관측을 못 본다."
|
|
184
|
+
say " 변화 검출이 이 시점부터 **멈춘 것**이지 «변화 없음» 이 아니다."
|
|
185
|
+
rm -f "$NEW"
|
|
186
|
+
fi
|
|
187
|
+
exit 0
|
|
@@ -39,6 +39,14 @@ cd "$REPO_ROOT" || exit 1
|
|
|
39
39
|
# scripts/sync_to_be_lanes.sh — forward-path lane suite for sync-to-be.sh, itself
|
|
40
40
|
# ACCEPTED_ABSENT above; added 2026-08-14, pmh-dev#69.
|
|
41
41
|
ACCEPTED_ABSENT=(
|
|
42
|
+
# 🟥 나가는 질의 위생 린트와 그 레인 — **일부러 출하하지 않는다.**
|
|
43
|
+
# 이 가드는 운영자 내부 패턴층(`.claude/rules/.public-surface-patterns`, gitignored)이 없으면
|
|
44
|
+
# fail-closed 로 막는다. 그 층은 소비자에게 안 나가므로, 출하하면 **신선한 설치는 100% 차단**된다
|
|
45
|
+
# — CLAUDE.md 가 «모든 새 install 을 막는 게이트는 엄격한 게이트가 아니라 우회 훈련기» 라 못박은
|
|
46
|
+
# 그 형태다. 소비자용으로 열려면 override 부재 시의 degrade 를 따로 설계해야 하고, 안 했다.
|
|
47
|
+
# `selfcheck.sh` 는 대상 부재 시 `_absent_subject_verdict` 로 빠지므로 패키지 모드에서 옳게 degrade 한다.
|
|
48
|
+
"scripts/outbound_query_guard.sh"
|
|
49
|
+
"scripts/test_outbound_query_lanes.sh"
|
|
42
50
|
".claude/registry/LOCAL_SKILL_REGISTRY.md"
|
|
43
51
|
# An INSTALL DESTINATION the user creates (`cp templates/local_fh_context.md
|
|
44
52
|
# .claude/rules/local_fh_context.md`), not a file FH ships. Shipping it would overwrite the
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prior_art_prompt.sh — T2 «새로 짓기 시작» 시점에 **내 컨텍스트로** 한 줄을 넣는다.
|
|
3
|
+
#
|
|
4
|
+
# ── 왜 이 자리인가 (2026-08-21, 챔버 런 consolidation-path-gate) ──
|
|
5
|
+
# 운영자 실경험이 준 두 시점: T1 착수(세션 갓 시작) · **T2 컨텍스트가 길어진 뒤 또 새로 빌드업할 때**.
|
|
6
|
+
# 이 훅이 T2 다. PreToolUse(Write) 는 «새 파일을 쓰기 직전» 이라 그 경계와 정확히 겹친다.
|
|
7
|
+
#
|
|
8
|
+
# ── 왜 산문이 아니라 훅인가 (실측) ──
|
|
9
|
+
# 같은 날 sim: **명시 지시 3/3 · advisory 0/3 · 프레이밍 0/3**. CLAUDE.md 에 규율을 더 적는 것은
|
|
10
|
+
# 프레이밍이라 0/3 자리다. `additionalContext` 는 훅이 **모델 컨텍스트에 지시로** 넣는 유일한
|
|
11
|
+
# 채널이고([[feedback_advisory_invisible_to_actor]]), 그게 3/3 자리다.
|
|
12
|
+
#
|
|
13
|
+
# ── 왜 서브에이전트가 아닌가 ──
|
|
14
|
+
# 같은 날 먼저 지은 `persona-innovator` Mode X 는 **불렀을 때** 돈다. 남에게 시킨 숙고는 그 순간의
|
|
15
|
+
# 세션을 안 바꾼다 — 오늘 자력 적발 0 · 외부(운영자5·codex·challenger·레인·CI) 적발 10+ 이 그 증거다.
|
|
16
|
+
#
|
|
17
|
+
# ── 이건 브레이크가 아니라 곱셈기다 (운영자, 2026-08-21) ──
|
|
18
|
+
# *"충분히 세상을 찾아봤으면 그 이후에는 가진 능력을 활용해서 개발을 해내야 한다. 이때 내부의
|
|
19
|
+
# 능력을 100% 활용해야지. 조합하면 배수는 지수적으로 올라갈 수 있고. 그게 가능한 이유는 바깥의
|
|
20
|
+
# 문제를 풀어낸 데이터들과 내부의 능력 오케스트레이션이 서로 엮이고 곱해지기 때문이다."*
|
|
21
|
+
# ⇒ 이 훅의 목적은 «짓지 마라» 가 아니라 «바깥을 입력으로 삼아 안을 100% 써라» 다.
|
|
22
|
+
# 금지로 읽히면 무시당하고, 곱셈기로 읽히면 쓰인다. 문안이 그렇게 끝나야 한다.
|
|
23
|
+
#
|
|
24
|
+
# ── 새 판단 0줄 ──
|
|
25
|
+
# 🟥 «명명 어휘» 가 이 훅의 실제 기전이다 (2026-08-21 실측). 같은 날 sim 에서 양팔 다 검색했는데
|
|
26
|
+
# 힌트 쥔 쪽만 정답에 닿았다(2/3 vs 0/3) — 차이는 검색 의지가 아니라 **무엇을 찾을지 이름 붙이는
|
|
27
|
+
# 능력**이었다. 운영자: *"늘 책을 읽는 사람이 AI 를 더 잘 다루어낸다."* 독서가 주는 것이 명명
|
|
28
|
+
# 어휘이고, 이 훅은 그걸 그 순간에 빌려주는 것이다. 그래서 문안이 «찾아라» 가 아니라
|
|
29
|
+
# «일반 어휘로 이름 붙여 찾아라» 로 끝난다.
|
|
30
|
+
#
|
|
31
|
+
# ── 사다리지 체크리스트가 아니다 (운영자, 2026-08-21) ──
|
|
32
|
+
# *"근처에 있는 책장 찾기 > 책장에도 없으면 도서관 가기"* — 순서가 하중이다. 셋이 그 순서를 지지한다:
|
|
33
|
+
# 코퍼스(ⓑ내부미조회 9 > ⓐ외부미조회 6) · 이 세션 sim(내부 부재판정 오류 6/6) · SAAS 2605.29796
|
|
34
|
+
# («internal knowledge suffices 인데 무턱대고 검색 = severe over-search»). 평평한 목록은 그 순서를
|
|
35
|
+
# 못 준다. 그리고 FH 자산 이름이 이미 `deep_research_capability_ladder` 다 — 사다리가 원래 형태다.
|
|
36
|
+
#
|
|
37
|
+
# 내용은 **이미 있는 스킬 넷을 가리키기만** 한다(운영자: *"우리 안에 그 스킬들이 있고 그 스킬들을
|
|
38
|
+
# 발휘하면 세계에 닿겠지"*). 내부=asset-placement-gate Step 0.6 · CATALOG · novelty_claim_check ·
|
|
39
|
+
# 외부=deep_research_capability_ladder · 숙고=fh-commons:deliberation.
|
|
40
|
+
#
|
|
41
|
+
# ── degrade ──
|
|
42
|
+
# 가역 표면(파일 쓰기)이라 **절대 안 막는다.** exit 0 고정, permissionDecision 없음.
|
|
43
|
+
# 판정도 안 한다 — 한 줄 제안이다. 조용한 실패(파싱 불가·python3 부재)는 무음 통과.
|
|
44
|
+
set -uo pipefail
|
|
45
|
+
RAW=$(cat 2>/dev/null || true)
|
|
46
|
+
# file_path 와 session_id 를 한 번에 뽑는다(파이프 두 번 안 탄다).
|
|
47
|
+
PARSED=$(printf '%s' "$RAW" | python3 -c '
|
|
48
|
+
import json,sys
|
|
49
|
+
try: d=json.load(sys.stdin)
|
|
50
|
+
except Exception: sys.exit(0)
|
|
51
|
+
ti=d.get("tool_input",{}) or {}
|
|
52
|
+
fp=(ti.get("file_path","") or "")
|
|
53
|
+
cmd=(ti.get("command","") or "").replace("\t"," ").replace("\n"," ")
|
|
54
|
+
sid=(d.get("session_id","") or "")
|
|
55
|
+
sys.stdout.buffer.write((fp+"\t"+sid+"\t"+cmd).encode("utf-8"))
|
|
56
|
+
' 2>/dev/null) || exit 0
|
|
57
|
+
FILE="${PARSED%%$'\t'*}"
|
|
58
|
+
_REST="${PARSED#*$'\t'}"
|
|
59
|
+
SID="${_REST%%$'\t'*}"
|
|
60
|
+
CMD="${_REST#*$'\t'}"
|
|
61
|
+
[ "$CMD" = "$_REST" ] && CMD=""
|
|
62
|
+
|
|
63
|
+
# ── 🟥 Bash 경로 (2026-08-21, 첫 실사용이 «비발화» 로 났다) ──
|
|
64
|
+
# 초판 matcher 는 `Write` 뿐이었다. 그런데 auto-mode 는 파일 생성을 **Bash 의 리다이렉트/heredoc**
|
|
65
|
+
# 으로 한다(이 저장소 CLAUDE.md 가 그렇게 지시한다: *"make file changes with sed, heredocs…"*).
|
|
66
|
+
# 즉 이 훅이 겨냥한 «새 메커니즘을 짓기 직전» 이 **구조적으로 Write 를 안 탄다.**
|
|
67
|
+
# ⇒ matcher 에 Bash 를 넣고, `file_path` 가 없으면 **명령의 리다이렉트 대상**에서 뽑는다.
|
|
68
|
+
#
|
|
69
|
+
# 🟥 heredoc 을 파싱하지 않는다 — 리다이렉트 «대상 경로» 만 본다. `cat > x.sh <<EOF` 든
|
|
70
|
+
# `printf … > x.sh` 든 대상은 `> ` 뒤에 있고, 본문은 볼 필요가 없다. 놓치는 형태
|
|
71
|
+
# (`tar x` · 에디터 · `cp`)는 **명시된 잔여**다 — 이 훅은 판정이 아니라 제안이라 미발화의
|
|
72
|
+
# 비용이 오발화보다 싸다.
|
|
73
|
+
if [ -z "$FILE" ] && [ -n "$CMD" ]; then
|
|
74
|
+
# 🟥 cross-family 4 HIGH 를 받고 다시 지었다 (2026-08-21). 넷 다 **오발화**였고,
|
|
75
|
+
# 이 훅에서 오발화는 미발화보다 훨씬 비싸다(소음 = 자기무력화 경로).
|
|
76
|
+
# ① `[[:space:]]tee` 에 **뒤 경계가 없어** `rg tee_probe.py` 가 tee 대상으로 읽혔다
|
|
77
|
+
# ② `>>?` 가 **`2>` 안의 `>`** 를 잡아 `pytest 2> /tmp/errors.sh` 가 발화했다
|
|
78
|
+
# ③ `git diff > /tmp/review_delta.sh` — 리포트인데 이름이 `.sh` 라 발화했다
|
|
79
|
+
# ④ `"$LOG_DIR/x.sh"` — 변수가 안 풀려 리터럴 `$LOG_DIR/...` 로 존재검사를 통과했다
|
|
80
|
+
# 수리 셋: ⓐ 리다이렉트는 **앞이 «줄머리 또는 공백»** 이어야 한다(→ `2>`·`1>`·`&>` 배제)
|
|
81
|
+
# ⓑ `tee` 는 **앞뒤 공백**을 요구한다 ⓒ 대상은 **이 프로젝트 안**이어야 한다
|
|
82
|
+
# ⓒ 가 ②③ 의 현실적 형태를 함께 죽인다 — 새 «메커니즘» 은 /tmp 에 안 산다.
|
|
83
|
+
for _c in $(printf '%s' "$CMD" \
|
|
84
|
+
| grep -oE '(^|[[:space:]])(>>?|tee[[:space:]])[[:space:]]*"?'"'"'?[^ "'"'"'|;&)<>]+' 2>/dev/null \
|
|
85
|
+
| sed -E 's/^[[:space:]]*(>>?|tee[[:space:]])[[:space:]]*//; s/^["'"'"']//'); do
|
|
86
|
+
case "$_c" in
|
|
87
|
+
''|/dev/*|\&*|-) continue ;;
|
|
88
|
+
*'$'*) continue ;; # ④ 미확장 변수 — 리터럴로 판정하면 거짓이다
|
|
89
|
+
*..*) continue ;; # 상위 탈출 경로는 «이 프로젝트 안» 이 아니다
|
|
90
|
+
esac
|
|
91
|
+
# ⓒ 프로젝트 밖 절대경로는 대상이 아니다. 상대경로는 프로젝트 기준이라 통과시킨다.
|
|
92
|
+
case "$_c" in
|
|
93
|
+
/*) case "$_c" in
|
|
94
|
+
"${CLAUDE_PROJECT_DIR:-/nonexistent-project-dir}"/*) ;;
|
|
95
|
+
*) continue ;;
|
|
96
|
+
esac ;;
|
|
97
|
+
esac
|
|
98
|
+
case "$_c" in
|
|
99
|
+
*.sh|*/SKILL.md|*/SKILL_detail.md|*/agents/*.md|*/rules/*.md|*.py|*.js|*.ts) FILE="$_c"; break ;;
|
|
100
|
+
esac
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
[ -n "$FILE" ] || exit 0
|
|
104
|
+
|
|
105
|
+
# ── 발동 조건 — 소음이 이 능력을 죽인다(냉독자 «자기무력화 경로») ──
|
|
106
|
+
# ① 이미 있는 파일 편집은 «새로 짓기» 가 아니다
|
|
107
|
+
[ -e "$FILE" ] && exit 0
|
|
108
|
+
# ② 기록·산출물은 대상이 아니다. «메커니즘» 만.
|
|
109
|
+
# 🟥 상대경로도 잡아야 한다 (2026-08-21 레인 D4). 초판 제외는 `*/tracks/*` 뿐이라
|
|
110
|
+
# **절대경로만** 걸렀다 — Write 는 절대경로를 주지만 **Bash 는 상대경로를 준다**
|
|
111
|
+
# (`cat > tracks/_meta/note.sh`). Bash 팔을 붙이면서 이 비대칭이 드러났고,
|
|
112
|
+
# 제외가 안 먹으면 이 훅은 산출물마다 뜨는 소음이 된다(=자기무력화 경로).
|
|
113
|
+
case "$FILE" in
|
|
114
|
+
tracks/*|logs/*|.git/*|node_modules/*) exit 0 ;;
|
|
115
|
+
*/tracks/*|*/logs/*|*.log|*.json|*.lock|*/node_modules/*|*/.git/*) exit 0 ;;
|
|
116
|
+
esac
|
|
117
|
+
case "$FILE" in
|
|
118
|
+
*.sh|*/SKILL.md|*/SKILL_detail.md|*/agents/*.md|*/rules/*.md|*.py|*.js|*.ts) ;;
|
|
119
|
+
*) exit 0 ;;
|
|
120
|
+
esac
|
|
121
|
+
# ③ **세션당** 한 번. 매 Write 마다 뜨면 3회 거절로 조용히 죽는다.
|
|
122
|
+
# 🟥 초판은 스탬프를 **날짜**로 잡았다 — 주석은 «세션당» 인데 구현은 «하루당» 이었고, 하루에 세션을
|
|
123
|
+
# 여럿 도는 환경(이 운영자가 그렇다)에서는 **첫 세션만 뜨고 나머지는 전부 침묵**한다. 내 레인도
|
|
124
|
+
# 못 잡았다: 디렉터리만 갈랐지 날짜 의미론을 안 쟀다. 훅 입력의 `session_id` 로 고친다.
|
|
125
|
+
# session_id 가 없으면(구버전·수동 호출) 날짜로 degrade — 침묵이 아니라 «덜 자주» 쪽이다.
|
|
126
|
+
# 🟥 `session_id` 는 **신뢰할 수 없는 입력**이다 (cross-family HIGH#5). 실측: `x/../../../name`
|
|
127
|
+
# 이 접두 검사를 통과해 프로젝트 밖에 파일을 만들었고, 끝에 `/` 를 붙이면 스탬프가 아예
|
|
128
|
+
# 안 써져 **매번 발화**했다(세션당 1회가 무력화). 화이트리스트로 좁힌다 — 빈 값이 되면
|
|
129
|
+
# 날짜로 degrade(침묵이 아니라 «덜 자주» 쪽).
|
|
130
|
+
_SID_SAFE=$(printf '%s' "${SID:-}" | tr -cd 'A-Za-z0-9_-')
|
|
131
|
+
STAMP="${CLAUDE_PROJECT_DIR:-.}/.claude/.prior_art_prompted_${_SID_SAFE:-$(date +%Y%m%d)}"
|
|
132
|
+
[ -f "$STAMP" ] && exit 0
|
|
133
|
+
mkdir -p "$(dirname "$STAMP")" 2>/dev/null || true
|
|
134
|
+
: > "$STAMP" 2>/dev/null || true
|
|
135
|
+
|
|
136
|
+
MSG="🔎 새 메커니즘을 쓰려는 참이다 ($(basename "$FILE")). 짓기 전에 **책장부터, 없으면 도서관** —
|
|
137
|
+
안 했으면 지금 하고, 했으면 이 줄은 무시해라.
|
|
138
|
+
|
|
139
|
+
① 책장 바로 옆에 있나. **여기가 대부분이다.**
|
|
140
|
+
우리 안 : CATALOG.md · .claude/registry/ · plugins/**/SKILL.md · scripts/ 를 실제로 grep
|
|
141
|
+
옆 칸 : LOCAL_SKILL_REGISTRY · scripts/cluster_capability_scan.sh · §Cross-Project Skill Bus
|
|
142
|
+
🟥 여기서 나오면 «찾아온 것» 이 아니라 **바로 호출 가능한 능력**이다
|
|
143
|
+
🟥 «grep 0건» 을 «없다» 로 확정하지 마라 — 오늘 실측에서 세션 6/6 이 이 자리에서 틀렸다.
|
|
144
|
+
내 어휘로만 두드렸을 가능성을 먼저 의심해라.
|
|
145
|
+
|
|
146
|
+
② 도서관 **책장에 없을 때만** 간다. 있는데 가면 그건 over-search 다(arXiv 2605.29796 이 명명한 실패모드).
|
|
147
|
+
갈 때는 문제를 **일반 어휘로 이름 붙여서** — 내부 어휘로는 세계에 안 닿는다.
|
|
148
|
+
경로: knowledge/shared/harness-core/deep_research_capability_ladder.md
|
|
149
|
+
|
|
150
|
+
③ 숙고 ①②를 놓고 갈랐나 — fh-commons:deliberation (제안→반론→종합)
|
|
151
|
+
|
|
152
|
+
④ 짓기 🟥 **탐색은 끝이 아니라 입력이다.** 찾았으면 거기서 멈추지 말고 **가진 능력을 100% 써서
|
|
153
|
+
지어라** — 바깥이 푼 데이터 × 내부 오케스트레이션은 더해지는 게 아니라 **곱해진다**
|
|
154
|
+
(arXiv 2607.06906 실측: 품질/달러 +82%, 백만토큰당 완수 54.9→92.0).
|
|
155
|
+
합주 형식은 이미 있다: CLAUDE.md §Intent Marshaling.
|
|
156
|
+
|
|
157
|
+
안 밟은 칸이 있으면 한 줄로 물어라: «이 문제 이름이 X 같은데 확인해볼까?»
|
|
158
|
+
판정이 아니라 제안이다. 이미 밟았으면 진행해라 — 단, 진행할 때 **내부 능력을 덜 쓰지 마라.**"
|
|
159
|
+
|
|
160
|
+
if json_out=$(printf '%s' "$MSG" | PYTHONIOENCODING=utf-8 python3 -c '
|
|
161
|
+
import json,sys
|
|
162
|
+
h=sys.stdin.read()
|
|
163
|
+
print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":h}}))
|
|
164
|
+
' 2>/dev/null) && [ -n "$json_out" ]; then
|
|
165
|
+
printf '%s\n' "$json_out"; exit 0
|
|
166
|
+
fi
|
|
167
|
+
printf '%s\n' "$MSG" >&2
|
|
168
|
+
exit 0
|