@chrono-meta/fh-gate 1.4.89 → 1.4.91
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/judgment_circuits.txt +14 -0
- package/.claude/rules/fh_4axis_gate.md +7 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +25 -0
- package/CLAUDE.md +202 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
- package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
- package/package.json +23 -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/skills/auto-decorrelation/SKILL.md +56 -8
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
- package/scripts/branch_claim.sh +266 -0
- package/scripts/chamber_run.sh +64 -2
- package/scripts/chamber_witness.sh +439 -0
- package/scripts/compaction_probe.sh +456 -0
- package/scripts/digest_landing_check.sh +385 -0
- package/scripts/directional_diff_gate.sh +459 -0
- package/scripts/fh_env_delta_scan.sh +15 -0
- package/scripts/fh_session_load.sh +34 -1
- package/scripts/field_canon_preload.sh +129 -0
- package/scripts/hook_source_lib.sh +41 -0
- package/scripts/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +106 -0
- package/scripts/session_close_check.sh +134 -1
- package/scripts/test_branch_claim_lanes.sh +231 -0
- package/scripts/test_dispatch_log_lanes.sh +35 -1
- package/scripts/test_field_canon_lanes.sh +142 -0
- package/scripts/test_hook_source_gate_lanes.sh +81 -0
- package/scripts/test_marker_crossfamily_lanes.sh +132 -0
- package/scripts/test_marker_floor_lanes.sh +9 -8
- package/scripts/test_relay_channel_lanes.sh +583 -0
- package/scripts/test_reviewer_capability_conformance.sh +173 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/utterance_landing_check.sh +209 -0
- package/templates/.git-hooks/pre-commit +297 -13
- package/templates/settings.Compaction.snippet.json +56 -0
- package/templates/settings.FieldCanon.snippet.json +51 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# reviewer_capability_corpus.tsv — SHARED known-pair corpus for "can this model be an
|
|
2
|
+
|
|
3
|
+
# adversarial reviewer?". Language-neutral DATA, deliberately not code.
|
|
4
|
+
#
|
|
5
|
+
# WHY DATA AND NOT A SHARED LIBRARY. The same judgment lives in at least three places, in
|
|
6
|
+
# three languages, across a public/private repo boundary: a public gate hook (bash), a
|
|
7
|
+
# field probe (shell), and a review backend (python). They cannot import each other —
|
|
8
|
+
# different repos, and the private side's vocabulary contains identifiers that must not
|
|
9
|
+
# reach a public repo. So the shareable artifact is the CORPUS, not the classifier: each
|
|
10
|
+
# side implements in its own language and proves agreement by running these rows.
|
|
11
|
+
#
|
|
12
|
+
# Measured drift 2026-08-08, before this file existed: 19 probe ids, **10 disagreements**
|
|
13
|
+
# between two of the implementations — and asymmetrically, 9 of them were the private side
|
|
14
|
+
# reading INCAPABLE models as CAPABLE. A permissive drift on the side that faces a real
|
|
15
|
+
# model catalog is the dangerous direction: it inflates panel size and family-diversity
|
|
16
|
+
# counts simultaneously, so the defense reports green while the panel cannot review.
|
|
17
|
+
#
|
|
18
|
+
# RESIDENCY: this file is PUBLIC. Generic capability classes and publicly-documented model
|
|
19
|
+
# families only. A corp/internal model id NEVER goes here — carry those in a local overlay
|
|
20
|
+
# corpus alongside (gitignored), same two-layer shape as the public-surface pattern file.
|
|
21
|
+
#
|
|
22
|
+
# NAMESPACES — declare what you own, skip what you do not (OPA-bundle `roots` shape).
|
|
23
|
+
# Two different things get classified under the same question and they are NOT interchangeable:
|
|
24
|
+
# ns:cli sidecar / CLI names a panel is composed of — `codex`, `gemini`, `agy`
|
|
25
|
+
# ns:model-id concrete served model identifiers — `voyage-3`, `glm-ocr`
|
|
26
|
+
# A consumer that classifies model ids has no opinion on `codex`; a consumer that composes
|
|
27
|
+
# panels has no opinion on `bge-m3`. Mixing them silently is a defect: measured 2026-08-08,
|
|
28
|
+
# the first draft of this corpus put both in one list and a model-id classifier failed the
|
|
29
|
+
# `codex` row — reported as implementation drift when it was corpus scope error. An adapter
|
|
30
|
+
# MUST declare its namespaces and SKIP rows outside them EXPLICITLY (a skipped row is
|
|
31
|
+
# reported, never silently counted as passing — unmeasured is not clean).
|
|
32
|
+
#
|
|
33
|
+
# FORMAT (tab-separated): id <TAB> expected <TAB> ns:<namespace>/<class> <TAB> why
|
|
34
|
+
# expected ∈ CAPABLE | INCAPABLE | UNDECIDABLE
|
|
35
|
+
# CAPABLE a generative model of a family known to return prose findings
|
|
36
|
+
# INCAPABLE emits vectors / scores / labels / transcripts — cannot return a finding
|
|
37
|
+
# UNDECIDABLE unrecognised. NOT "capable by default" — default-deny, because a
|
|
38
|
+
# denylist cannot know that `voyage-3` is an embedding model. An
|
|
39
|
+
# implementation that answers CAPABLE here is failing open.
|
|
40
|
+
#
|
|
41
|
+
# THE CORPUS STATES GROUND TRUTH, NOT WHAT ANY IMPLEMENTATION CAN SEE. `voyage-3` IS an
|
|
42
|
+
# embedding model, so the row says INCAPABLE — even though a pattern classifier cannot
|
|
43
|
+
# tell from the string. Grading is therefore on OUTCOME SAFETY, not label identity:
|
|
44
|
+
#
|
|
45
|
+
# hard FAIL want ∈ {INCAPABLE, UNDECIDABLE} and got = CAPABLE (permissive — ships a
|
|
46
|
+
# broken panel; the only direction that can actually hurt)
|
|
47
|
+
# hard FAIL want = CAPABLE and got ≠ CAPABLE (over-block — an
|
|
48
|
+
# always-red gate teaches --no-verify)
|
|
49
|
+
# DEVIATION INCAPABLE ↔ UNDECIDABLE in either direction (reported, not failed:
|
|
50
|
+
# both BLOCK under default-deny, so the outcome is identical)
|
|
51
|
+
#
|
|
52
|
+
# This rule exists because the first draft graded on label identity and was silently fitted
|
|
53
|
+
# to one implementation's regex — `armorm` was written UNDECIDABLE and `voyage-3` INCAPABLE
|
|
54
|
+
# purely because that regex caught one and not the other. Two ids with the same property,
|
|
55
|
+
# two different standards, and the corpus was grading itself. Worse, it made "delete the
|
|
56
|
+
# row" the rational move for whoever maintained the weaker classifier.
|
|
57
|
+
#
|
|
58
|
+
# ORDERING INVARIANT the rows enforce: ineligibility must be tested BEFORE eligibility.
|
|
59
|
+
# Rows marked `overlap` match a valid family AND an incapable class simultaneously; an
|
|
60
|
+
# eligibility-first implementation admits them silently. They are the anchor for the ORDER,
|
|
61
|
+
# not for the list.
|
|
62
|
+
|
|
63
|
+
# ── CONTRACT PIN ───────────────────────────────────────────────────────────────
|
|
64
|
+
#CORPUS-VERSION 2
|
|
65
|
+
#CORPUS-SHA256 e0d8d83b9ae042e4
|
|
66
|
+
# Hash covers the DATA rows only (comments/format may be reworded without a version bump).
|
|
67
|
+
# Recompute EXACTLY this way — two ways of computing it is itself a drift source, and the
|
|
68
|
+
# first draft of this pin failed on a trailing-newline difference between two implementations:
|
|
69
|
+
# grep -E '^[^#[:space:]]' reviewer_capability_corpus.tsv | shasum -a 256 | cut -c1-16
|
|
70
|
+
# Every consumer recomputes it and compares. A mismatch is HARNESS_ERROR — not PASS, not FAIL:
|
|
71
|
+
# it means the consumer is reading a DIFFERENT corpus than the one this contract names, and a
|
|
72
|
+
# drifted local copy is the failure mode that makes every downstream instrument report healthy
|
|
73
|
+
# while nothing is actually shared. (Independent precedent: OPA bundle `roots`, test262/CNCF
|
|
74
|
+
# conformance-suite-as-contract; failure taxonomy: local-first resolution · silent skipping ·
|
|
75
|
+
# drifted duplicates.)
|
|
76
|
+
|
|
77
|
+
# ── capable: generative families ────────────────────────────────────────────────
|
|
78
|
+
codex CAPABLE ns:cli/generative GPT-family agent CLI
|
|
79
|
+
gpt-5.5 CAPABLE ns:model-id/generative GPT family
|
|
80
|
+
gemini CAPABLE ns:cli/generative Gemini family
|
|
81
|
+
qwen3-32b CAPABLE ns:model-id/generative Qwen family, generative variant
|
|
82
|
+
glm-4.6 CAPABLE ns:model-id/generative GLM family, generative variant
|
|
83
|
+
deepseek-v3 CAPABLE ns:model-id/generative DeepSeek family
|
|
84
|
+
mistral-large CAPABLE ns:model-id/generative Mistral family
|
|
85
|
+
llama-4-70b CAPABLE ns:model-id/generative Llama family
|
|
86
|
+
|
|
87
|
+
# ── incapable: named by class token ─────────────────────────────────────────────
|
|
88
|
+
text-embedding-3-large INCAPABLE ns:model-id/embedding emits vectors
|
|
89
|
+
some-reranker-v1 INCAPABLE ns:model-id/reranker emits scores
|
|
90
|
+
generic-ocr-2b INCAPABLE ns:model-id/ocr emits transcribed text, not judgment
|
|
91
|
+
acme-safeguard-8b INCAPABLE ns:model-id/safeguard classifier — emits labels
|
|
92
|
+
text-moderation-latest INCAPABLE ns:model-id/moderation classifier — emits labels
|
|
93
|
+
whisper-large-v3 INCAPABLE ns:model-id/speech speech-to-text
|
|
94
|
+
|
|
95
|
+
# ── incapable WITHOUT a class token in the name (the denylist's blind spot) ─────
|
|
96
|
+
# These are why a denylist alone is insufficient and default-deny is required: nothing in
|
|
97
|
+
# the string says "embedding". Every one is a real, publicly-documented model.
|
|
98
|
+
voyage-3 INCAPABLE ns:model-id/embedding embedding model; name carries no class token
|
|
99
|
+
bge-m3 INCAPABLE ns:model-id/embedding embedding model; name carries no class token
|
|
100
|
+
gte-large INCAPABLE ns:model-id/embedding embedding model; name carries no class token
|
|
101
|
+
all-minilm-l6-v2 INCAPABLE ns:model-id/embedding embedding model; name carries no class token
|
|
102
|
+
armorm INCAPABLE ns:model-id/reward reward model — emits a scalar; opaque name, no class token
|
|
103
|
+
starling-rm INCAPABLE ns:model-id/reward reward model — emits a scalar
|
|
104
|
+
cohere-rank-v3 INCAPABLE ns:model-id/reranker reranker named `rank`, not `rerank`
|
|
105
|
+
|
|
106
|
+
# ── overlap: matches a VALID family AND an incapable class (ordering anchor) ────
|
|
107
|
+
glm-ocr INCAPABLE ns:model-id/overlap matches `glm` (capable) and `ocr` (incapable) — order decides
|
|
108
|
+
qwen3-embedding-8b INCAPABLE ns:model-id/overlap matches `qwen` (capable) and `embed` (incapable)
|
|
109
|
+
snowflake-arctic-embed-l-v2.0 INCAPABLE ns:model-id/overlap family prefix unrecognised; `embed` decides
|
|
110
|
+
|
|
111
|
+
# ── undecidable: unrecognised → default-deny, never assumed capable ─────────────
|
|
112
|
+
some-new-model-x UNDECIDABLE ns:model-id/unknown no known family; answering CAPABLE here is fail-open
|
|
113
|
+
internal-model-7 UNDECIDABLE ns:model-id/unknown no known family
|
|
114
|
+
codeguardian UNDECIDABLE ns:cli/unknown NOT auto-incapable — a bare `guard` substring rule blocks this legitimately-named review tool; unrecognised, so default-deny applies, but as UNDECIDABLE not INCAPABLE
|
|
115
|
+
|
|
116
|
+
# ── same-family exclusion (decorrelation, not capability) ──────────────────────
|
|
117
|
+
# Capable models that must still be REJECTED as panel members when the governor is of that
|
|
118
|
+
# family. Capability and decorrelation are different axes; an implementation that only
|
|
119
|
+
# checks capability passes a same-family panel. Marked separately so an adapter that does
|
|
120
|
+
# not own the decorrelation axis can skip this block explicitly rather than silently.
|
|
121
|
+
#SAMEFAMILY claude
|
|
122
|
+
#SAMEFAMILY opus
|
|
123
|
+
#SAMEFAMILY sonnet
|
|
124
|
+
#SAMEFAMILY haiku
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -211,6 +211,45 @@ else
|
|
|
211
211
|
fail=1
|
|
212
212
|
fi
|
|
213
213
|
|
|
214
|
+
# embedded --self-test suites (compaction_probe · judgment_circuit_lint · novelty_claim_check).
|
|
215
|
+
# These three carry their lanes INSIDE the script (`--self-test`) rather than in a sibling
|
|
216
|
+
# test_*_lanes.sh, so the name-list wiring above skipped them silently: 48 lanes existed and ran
|
|
217
|
+
# only when a human typed the command. That is built-but-not-wired applied to the anchors themselves
|
|
218
|
+
# — a later edit that breaks a lane stays green everywhere the project actually checks
|
|
219
|
+
# (high re-review 2026-08-08). Same shape as the block above: subject absent → SKIP, subject present
|
|
220
|
+
# but self-test missing → FAIL, never a silent pass.
|
|
221
|
+
for _subj in compaction_probe judgment_circuit_lint novelty_claim_check; do
|
|
222
|
+
if [ ! -f "scripts/$_subj.sh" ]; then
|
|
223
|
+
echo "SKIP $_subj --self-test (subject scripts/$_subj.sh absent)"
|
|
224
|
+
else
|
|
225
|
+
# ⚠️ **문자열 존재로 판정하지 마라.** 초판은 `grep -q -- '--self-test'` 였는데, 그 문자열은
|
|
226
|
+
# 헤더 주석과 usage echo 에도 있어서 **디스패처 한 줄만 지워도 여전히 매치**한다. 그리고
|
|
227
|
+
# 인식 못 한 모드에서 스크립트가 usage 를 찍고 exit 0 을 내므로, selfcheck 는 rc=0 을 보고
|
|
228
|
+
# 조용히 통과했다 — 25개 레인이 통째로 사라져도 `npm test` 는 PASS (high 3차 리뷰 실측).
|
|
229
|
+
# 존재검사가 진위를 못 본다는 그 클래스의 재발이다. **실행이 일어났다는 증거**를 요구한다.
|
|
230
|
+
# `< /dev/null` 필수: 인식 못 한 모드로 떨어지면 스크립트가 stdin 을 기다려 **무한 대기**한다
|
|
231
|
+
# (실측 — 디스패처 제거 known-negative 가 2분 타임아웃). CI 를 멈추는 건 조용한 통과보다 나쁘다.
|
|
232
|
+
# `timeout` 은 GNU coreutils 이고 **stock macOS 에 없다**. 가용성 확인 없이 부르면 rc=127 +
|
|
233
|
+
# 빈 출력 → 아래 `*)` 가 발동해 "dispatcher missing?" 이라는 **틀린 원인**으로 거짓 FAIL 이
|
|
234
|
+
# 난다(실측: homebrew 없는 PATH 에서 3개 subject 전부). 소비자 머신에서 `npm test` 와
|
|
235
|
+
# `prepublishOnly` 를 깨뜨리는 경로다. 정답 폼은 이미 레포에 있다(sync-from-be.sh:134).
|
|
236
|
+
# 없으면 무한대기 방지를 잃는 대신 도는 쪽을 택한다 — `< /dev/null` 이 그 방어의 본체다.
|
|
237
|
+
local _to=""; command -v timeout >/dev/null 2>&1 && _to="timeout 120"
|
|
238
|
+
_st_out="$($_to bash "scripts/$_subj.sh" --self-test < /dev/null 2>&1)"; _st_rc=$?
|
|
239
|
+
case "$_st_out" in
|
|
240
|
+
*캘리브레이션*) : ;;
|
|
241
|
+
*) echo "FAIL $_subj: --self-test produced no calibration verdict (dispatcher missing?)"
|
|
242
|
+
printf '%s\n' "$_st_out" | head -3 | sed 's/^/ /'
|
|
243
|
+
fail=1; _st_rc=0 ;; # 이미 FAIL 로 셌으니 아래서 중복 계상 안 한다
|
|
244
|
+
esac
|
|
245
|
+
if [ "$_st_rc" -ne 0 ]; then
|
|
246
|
+
echo "FAIL $_subj --self-test (exit $_st_rc)"
|
|
247
|
+
printf '%s\n' "$_st_out" | tail -6 | sed 's/^/ /'
|
|
248
|
+
fail=1
|
|
249
|
+
fi
|
|
250
|
+
fi
|
|
251
|
+
done
|
|
252
|
+
|
|
214
253
|
# memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
|
|
215
254
|
# edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
|
|
216
255
|
# that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
|
|
@@ -281,6 +320,34 @@ else
|
|
|
281
320
|
fail=1
|
|
282
321
|
fi
|
|
283
322
|
|
|
323
|
+
# branch-claim lanes — shipped with 27 hand-run lanes and ZERO callers, so CI never ran one of them.
|
|
324
|
+
# That is the exact defect its own subject guards against (a gate nobody claims against passes
|
|
325
|
+
# forever), reproduced one layer up in its anchor. Same SKIP/FAIL shape: a missing subject is a
|
|
326
|
+
# legitimate skip, a present subject with a missing anchor is a real failure. The lanes are hermetic
|
|
327
|
+
# (per-PID temp dirs under the worktree, no network, no API spend), so running them here costs
|
|
328
|
+
# nothing.
|
|
329
|
+
#
|
|
330
|
+
# ⚠️ Subject-absent is a FAIL here, NOT the SKIP the sibling blocks use. The SKIP arm exists for
|
|
331
|
+
# subjects that may legitimately not ship; branch_claim.sh IS in package.json files[], so it is
|
|
332
|
+
# present in the source checkout AND in the published package — absence can only mean deletion.
|
|
333
|
+
# Nothing else catches that: measured 2026-08-09, package_coverage_check.sh returns PASS on a
|
|
334
|
+
# files[] entry whose file does not exist, so a deleted subject would be green on every surface
|
|
335
|
+
# (SKIP here + PASS there + npm silently omitting it). A cross-family reviewer caught this; the
|
|
336
|
+
# first draft of this block used SKIP and would have silenced exactly that case.
|
|
337
|
+
# Named residual, deliberately not fixed here: the sibling blocks above carry the same hole for
|
|
338
|
+
# their own shipped subjects. Fixing them is a separate change with its own verification.
|
|
339
|
+
if [ ! -f scripts/branch_claim.sh ]; then
|
|
340
|
+
echo "FAIL scripts/branch_claim.sh is in package.json files[] but absent — a deleted subject, not a skip"
|
|
341
|
+
fail=1
|
|
342
|
+
elif [ -f scripts/test_branch_claim_lanes.sh ]; then
|
|
343
|
+
if ! bash scripts/test_branch_claim_lanes.sh; then
|
|
344
|
+
fail=1
|
|
345
|
+
fi
|
|
346
|
+
else
|
|
347
|
+
echo "FAIL test_branch_claim_lanes.sh: branch_claim.sh present but its anchor is missing"
|
|
348
|
+
fail=1
|
|
349
|
+
fi
|
|
350
|
+
|
|
284
351
|
# And the ablation calibrator, for the third instance of the same reason. Its whole job is telling
|
|
285
352
|
# apart states that look identical from outside — "the arm could not answer" vs "the runner is dead"
|
|
286
353
|
# vs "the runner read the answer off disk" — and round 2 of its own adversarial review found that
|
|
@@ -439,6 +506,18 @@ else
|
|
|
439
506
|
fail=1
|
|
440
507
|
fi
|
|
441
508
|
|
|
509
|
+
# hook_source_gate 는 subject 가 session_close_check.sh 가 아니라 별도로 돌린다 — 위 루프의
|
|
510
|
+
# SKIP 조건(subject 부재)에 얹으면 엉뚱한 이유로 건너뛴다.
|
|
511
|
+
if [ -f scripts/test_hook_source_gate_lanes.sh ]; then
|
|
512
|
+
if bash scripts/test_hook_source_gate_lanes.sh >/dev/null 2>&1; then
|
|
513
|
+
echo "PASS test_hook_source_gate_lanes.sh"
|
|
514
|
+
else
|
|
515
|
+
echo "FAIL test_hook_source_gate_lanes.sh (SessionStart source 게이트 쌍 붕괴)"
|
|
516
|
+
bash scripts/test_hook_source_gate_lanes.sh 2>&1 | grep "❌" | head -5 | sed "s/^/ /"
|
|
517
|
+
fail=1
|
|
518
|
+
fi
|
|
519
|
+
fi
|
|
520
|
+
|
|
442
521
|
for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh scripts/test_session_close_chain_lanes.sh; do
|
|
443
522
|
if [ ! -f scripts/session_close_check.sh ]; then
|
|
444
523
|
echo "SKIP ${_anchor##*/} (subject scripts/session_close_check.sh absent)"
|
|
@@ -520,6 +599,33 @@ else
|
|
|
520
599
|
fail=1
|
|
521
600
|
fi
|
|
522
601
|
|
|
602
|
+
# utterance landing check — the close chain's CONTENT anchor, and it has to be RUN, not parsed.
|
|
603
|
+
# Measured 2026-08-08 on the branch that introduced it: the only thing in this file that touched
|
|
604
|
+
# `scripts/utterance_landing_check.sh` was the `bash -n` syntax sweep at the top. Syntax passing is
|
|
605
|
+
# not the instrument working, so the script shipped via files[] with zero behavioural callers —
|
|
606
|
+
# [[feedback_built_but_not_wired]] in its purest form, where the sole caller is prose in CLAUDE.md.
|
|
607
|
+
#
|
|
608
|
+
# What the self-test defends is specifically the DEGRADE DIRECTION. This checker's whole reason for
|
|
609
|
+
# existing is that a dead grep prints zero hits and zero hits read as "nothing landed" — a fail-open
|
|
610
|
+
# that manufactures a clean verdict out of a broken instrument. Its known-pair set pins that apart:
|
|
611
|
+
# a genuine miss must exit 1 while a dead control must exit 10, and 10 must never collapse into 1.
|
|
612
|
+
# Left unrun, the file rots exactly where it is load-bearing and nothing here would notice.
|
|
613
|
+
# Absence is a FAIL, not a SKIP — and the distinction is mechanical, not stylistic. The SKIP arm
|
|
614
|
+
# above for `probe_scope_check.sh` is correct because that file genuinely never ships; this one is
|
|
615
|
+
# listed in package.json files[], so in BOTH a source tree and an installed package it must be here.
|
|
616
|
+
# A SKIP would convert `rm scripts/utterance_landing_check.sh` into a green run — deletion as a
|
|
617
|
+
# clean bill of health, which is the same fail-open shape the checker itself exists to refuse.
|
|
618
|
+
if [ ! -f scripts/utterance_landing_check.sh ]; then
|
|
619
|
+
echo "FAIL utterance_landing_check.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
|
|
620
|
+
fail=1
|
|
621
|
+
elif _out=$(bash scripts/utterance_landing_check.sh --self-test 2>&1); then
|
|
622
|
+
echo "PASS utterance_landing_check.sh (known-pair self-test: control-death → 10, target-miss → 1)"
|
|
623
|
+
else
|
|
624
|
+
echo "FAIL utterance_landing_check.sh: self-test failed — the close-chain content anchor cannot be trusted"
|
|
625
|
+
_show_failure "$_out"
|
|
626
|
+
fail=1
|
|
627
|
+
fi
|
|
628
|
+
|
|
523
629
|
# tag/version consistency guard. Wired with the guard it anchors: the guard exists because a wrong
|
|
524
630
|
# tag reached the remote and a publish from the wrong tree was stopped only by npm's own collision
|
|
525
631
|
# check, so an unrun anchor here would be the same luck-as-floor arrangement one layer up.
|
|
@@ -94,6 +94,128 @@ if command -v gh >/dev/null 2>&1; then
|
|
|
94
94
|
[ "${PRS:-0}" -gt 0 ] && echo "⚠️ ①-b $PRS open PR(s) by you — classify: self-mergeable vs awaiting-external"
|
|
95
95
|
fi
|
|
96
96
|
|
|
97
|
+
# ── ①-d CLOSE RE-OPENED? ──────────────────────────────────────────────────────
|
|
98
|
+
# WHY: a session that closed cleanly and then kept working looks IDENTICAL to one that never
|
|
99
|
+
# closed — both read Completed from outside, and ①-c cannot separate them. Measured 2026-08-09:
|
|
100
|
+
# this session ran step 5 eleven times and a peer independently named the same gap. The two need
|
|
101
|
+
# opposite actions — an unfinished close must be FINISHED, a re-opened one must be RE-FOLDED.
|
|
102
|
+
# HOW: stamp each close-gated pass and count. Self-attested (whoever runs the check writes it):
|
|
103
|
+
# it detects the pattern, it does not prove it.
|
|
104
|
+
CLOSE_STAMP="$FH/tracks/_meta/.close_stamps_$TODAY"
|
|
105
|
+
if [ "${FH_SESSION_CLOSE:-0}" = "1" ]; then
|
|
106
|
+
mkdir -p "$(dirname "$CLOSE_STAMP")" 2>/dev/null
|
|
107
|
+
date -u +%Y-%m-%dT%H:%M:%SZ >> "$CLOSE_STAMP" 2>/dev/null || true
|
|
108
|
+
fi
|
|
109
|
+
if [ -f "$CLOSE_STAMP" ]; then
|
|
110
|
+
CLOSE_N=$(grep -c . "$CLOSE_STAMP" 2>/dev/null | tr -d ' ')
|
|
111
|
+
if [ "${CLOSE_N:-0}" -gt 1 ]; then
|
|
112
|
+
echo "ℹ️ ①-d close ran $CLOSE_N times today — CLOSED-THEN-WORKED, not a stalled close."
|
|
113
|
+
echo " Both look Completed from outside; only this counter separates them."
|
|
114
|
+
echo " Every re-open owes a FULL step 5: append to fh_completed FIRST, then rewrite the card."
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# ── ①-c LIVE PEER SESSIONS in this same harness ─────────────────────────────────
|
|
119
|
+
# WHY: FH's close chain has an agreed two-axis order (peer appends → ping → **ask "더 있나" before
|
|
120
|
+
# folding** → fold → re-check merged PRs), and it lived only in the session card as prose. Measured
|
|
121
|
+
# 2026-08-09, the first two-axis close: the ping surfaced four deltas the closing session did not
|
|
122
|
+
# know about, and **two of them were structurally invisible to `gh pr list`** (an inheritance
|
|
123
|
+
# channel and a landing check — no PR exists for either). So a PR sweep is not a substitute; the
|
|
124
|
+
# question is. This block does the DETECTION half mechanically. It never merges anything and never
|
|
125
|
+
# blocks — the merge is where the loss risk is (two sessions writing one card is the shared-checkout
|
|
126
|
+
# hazard this repo already named), and one occurrence is below this repo's own mechanization bar.
|
|
127
|
+
#
|
|
128
|
+
# HOW: a live session owns /tmp/cc-socks/<pid>.sock. pid → cwd resolves the ONE thing that actually
|
|
129
|
+
# matters, "is this peer in MY harness" — which the agent-facing session list cannot answer, because
|
|
130
|
+
# it reports names, not directories. Stale sockets are detectable (pid gone), so absence here is a
|
|
131
|
+
# measurement rather than silence.
|
|
132
|
+
# SCOPE (operator, 2026-08-09): this is a **claude-agents / Agent-View** feature — the surface where
|
|
133
|
+
# sessions are deliberately run in parallel. A plain solo terminal must not be nagged because some
|
|
134
|
+
# other window happens to be open. Discriminator is env, set by the parent that spawns the session.
|
|
135
|
+
# ⚠️ NEGATIVE ARM UNVERIFIED: a child session cannot observe a top-level session's environment, so
|
|
136
|
+
# "a real top-level session lacks these vars" is asserted, not measured. The lane simulates absence
|
|
137
|
+
# by unsetting them, which tests THIS SCRIPT'S logic — not that claim. Verify from a plain terminal.
|
|
138
|
+
if [ -z "${CLAUDE_CODE_CHILD_SESSION:-}${CLAUDE_CODE_AGENT:-}" ] && [ "${FH_PEER_SCAN_FORCE:-0}" != "1" ]; then
|
|
139
|
+
: # solo/top-level surface — ①-c is out of scope here, and silence is the correct output
|
|
140
|
+
else
|
|
141
|
+
PEER_LIVE=0; PEER_UNKNOWN=0; PEER_STALE=0; PEER_SPARE=0; PEER_IDS=""
|
|
142
|
+
SOCK_DIR="${FH_PEER_SOCK_DIR:-/tmp/cc-socks}"
|
|
143
|
+
# Self-exclusion must walk the ANCESTOR chain, not compare `$$`. This script is a grandchild of the
|
|
144
|
+
# session process that owns the socket, so `$$` never matches it — measured: the first run counted
|
|
145
|
+
# the calling session as its own peer, i.e. it would tell you to go ask yourself.
|
|
146
|
+
SELF_CHAIN=" $$ "
|
|
147
|
+
_p=$$
|
|
148
|
+
while :; do
|
|
149
|
+
_p=$(ps -o ppid= -p "$_p" 2>/dev/null | tr -d ' ')
|
|
150
|
+
case "$_p" in ''|0|1) break ;; esac
|
|
151
|
+
SELF_CHAIN="$SELF_CHAIN$_p "
|
|
152
|
+
done
|
|
153
|
+
_peer_cwd() { # $1=pid → cwd on stdout, empty if unresolvable
|
|
154
|
+
if [ -r "/proc/$1/cwd" ]; then readlink "/proc/$1/cwd" 2>/dev/null; return; fi
|
|
155
|
+
command -v lsof >/dev/null 2>&1 || return 0
|
|
156
|
+
lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1
|
|
157
|
+
}
|
|
158
|
+
if [ -d "$SOCK_DIR" ]; then
|
|
159
|
+
for _s in "$SOCK_DIR"/*.sock; do
|
|
160
|
+
[ -e "$_s" ] || continue
|
|
161
|
+
_pid="${_s##*/}"; _pid="${_pid%.sock}"
|
|
162
|
+
case "$_pid" in ''|*[!0-9]*) continue ;; esac
|
|
163
|
+
case "$SELF_CHAIN" in *" $_pid "*) continue ;; esac # 나 자신(조상 체인)
|
|
164
|
+
if ! ps -p "$_pid" >/dev/null 2>&1; then PEER_STALE=$((PEER_STALE+1)); continue; fi
|
|
165
|
+
# bg-spare = pre-warmed worker, NOT a session. It owns a socket and a cwd like everything else,
|
|
166
|
+
# so cwd matching alone counts it as a peer — measured 2026-08-09, and it is a pure false
|
|
167
|
+
# positive with a clean signature. Exclude mechanically rather than explaining it away.
|
|
168
|
+
case "$(ps -p "$_pid" -o args= 2>/dev/null)" in *--bg-spare*) PEER_SPARE=$((PEER_SPARE+1)); continue ;; esac
|
|
169
|
+
_pcwd="$(_peer_cwd "$_pid")"
|
|
170
|
+
if [ -z "$_pcwd" ]; then
|
|
171
|
+
# Alive but undirected. NOT zero — a peer we cannot place is exactly the case that must not
|
|
172
|
+
# render as "no peers" (not-found != zero).
|
|
173
|
+
PEER_UNKNOWN=$((PEER_UNKNOWN+1)); continue
|
|
174
|
+
fi
|
|
175
|
+
case "$_pcwd" in
|
|
176
|
+
"$FH"|"$FH"/*) PEER_LIVE=$((PEER_LIVE+1)); PEER_IDS="$PEER_IDS $_pid" ;;
|
|
177
|
+
esac
|
|
178
|
+
done
|
|
179
|
+
else
|
|
180
|
+
PEER_UNKNOWN=-1
|
|
181
|
+
fi
|
|
182
|
+
if [ "$PEER_UNKNOWN" = "-1" ]; then
|
|
183
|
+
echo "ℹ️ ①-c peer scan UNMEASURED — no session-socket dir at $SOCK_DIR (not 'no peers')"
|
|
184
|
+
elif [ "$PEER_LIVE" -gt 0 ]; then
|
|
185
|
+
echo "⚠️ ①-c $PEER_LIVE peer CANDIDATE(s) in THIS harness —$PEER_IDS"
|
|
186
|
+
echo " The surplus over the fleet view Working list is MIXED — do not read it as one thing."
|
|
187
|
+
echo " Measured 2026-08-09, three kinds land here and only the first needs a message:"
|
|
188
|
+
echo " working session → ask before folding"
|
|
189
|
+
echo " idle terminal → a real session sitting at a prompt. Not working. Usually skip."
|
|
190
|
+
echo " lingering process → a close that never finished (seen once, had to be killed by hand)"
|
|
191
|
+
echo " Only the fleet view separates them. An earlier version of this text called the whole"
|
|
192
|
+
echo " surplus unfinished closes; that was one case generalised, and it was wrong."
|
|
193
|
+
echo " Do NOT switch to the job state file to narrow it: it marked THIS session done"
|
|
194
|
+
echo " (firstTerminalAt set) while it was still running — it under-reports as hard as"
|
|
195
|
+
echo " sockets over-report. Cross-read with the fleet view; neither source alone is right."
|
|
196
|
+
echo " THIS IS A DETECTION LIST, NOT A RECIPIENT LIST — do not message all of them."
|
|
197
|
+
echo " What survives here is: a live PROCESS whose cwd is this harness, minus pre-warmed"
|
|
198
|
+
echo " spares. That still includes IDLE TERMINALS sitting at a prompt. It is NOT a list of"
|
|
199
|
+
echo " sessions that are working — nothing here can tell you that; the fleet view can."
|
|
200
|
+
echo " Notify only the ones actually WORKING/BUSY right now. Messaging a finished session"
|
|
201
|
+
echo " is noise, burns a recipient approval, and does not land in their record."
|
|
202
|
+
echo " Of those still working, ASK '지금부터 마감이다, 더 있나' — the question is wider than a"
|
|
203
|
+
echo " PR sweep: a peer's inheritance-channel or landing-check work has no PR to find."
|
|
204
|
+
echo " Then fold, then re-check \`gh pr list --merged\`. Merging their delta stays MANUAL."
|
|
205
|
+
echo " SENDING IS NOT DELIVERING — a cross-session message can be held for the recipient"
|
|
206
|
+
echo " user approval and never arrive; the send call still reports success. Measured"
|
|
207
|
+
echo " 2026-08-09: 2 of 5 notices were held while the card already claimed all 5 informed."
|
|
208
|
+
echo " Record ANSWERS, not sends, and put the unanswered count in the card."
|
|
209
|
+
[ "$PEER_UNKNOWN" -gt 0 ] && echo " + $PEER_UNKNOWN live session(s) whose directory is UNKNOWN (counted, not dismissed)"
|
|
210
|
+
elif [ "$PEER_UNKNOWN" -gt 0 ]; then
|
|
211
|
+
echo "⚠️ ①-c 0 peers placed in this harness, but $PEER_UNKNOWN live session(s) are UNPLACEABLE"
|
|
212
|
+
echo " — treat as possible peers and ask; do not read this as 'closing alone'."
|
|
213
|
+
else
|
|
214
|
+
echo "✅ ①-c no live peer in this harness (${PEER_STALE} stale · ${PEER_SPARE} pre-warmed spare(s) ignored)"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
fi
|
|
218
|
+
|
|
97
219
|
# ② FH assets changed today → harvest-loop owed
|
|
98
220
|
# NOTE: no `grep -q` here — under `set -o pipefail`, -q's early exit SIGPIPEs git log (141),
|
|
99
221
|
# masking a real match as pipeline failure so the warning NEVER fired on true positives
|
|
@@ -219,7 +341,18 @@ DISPATCHED=$(_int "$(grep -c "^$TODAY$" "$TALLY" 2>/dev/null | tr -d ' ' || true
|
|
|
219
341
|
# while anything appended via yaml.dump renders `- date: '"'"'2026-08-02'"'"'`. Matching one form counted
|
|
220
342
|
# half the entries as absent — a divergent-normalizer miss inside the check that exists to catch
|
|
221
343
|
# missing records. Known-pair calibrated below in test_dispatch_log_lanes.sh.
|
|
222
|
-
|
|
344
|
+
# ── 전환 설계: 레거시 단일 파일 ∪ 세션별 디렉터리를 **둘 다** 읽는다 ────────────
|
|
345
|
+
# WHY: 같은 파일 끝에 여러 세션이 append 하면 브랜치 병합이 확정 충돌이고, 그 해소가
|
|
346
|
+
# 조용히 손상된다 — git 이 엔트리 헤더 한 줄을 공통 접두로 밀어내서 «양쪽 다 보존»이라는
|
|
347
|
+
# 정답 해소가 엔트리를 흡수시킨다(2026-08-09 재현: 항목 133/정답 134, 파싱은 통과).
|
|
348
|
+
# 처방은 세션당 한 파일. 다만 **마이그레이션과 배선을 같은 커밋에 묶으면** 아직 머지 안 된
|
|
349
|
+
# 원장 브랜치와 대형 충돌이 나므로, 소비자를 먼저 «둘 다 읽게» 만든다. 그러면 데이터가
|
|
350
|
+
# 언제 옮겨가든 이 검사는 안 깨진다. 두 상태 모두 known-pair 로 고정돼 있다.
|
|
351
|
+
LOGDIR="$FH/knowledge/shared/learnings/subagent_invocations"
|
|
352
|
+
_cat_logs() { # 다중 파일에 grep -c 를 직접 걸면 파일별 `경로:개수` 를 찍어 합산이 깨진다(실측).
|
|
353
|
+
{ [ -f "$LOG" ] && cat "$LOG"; [ -d "$LOGDIR" ] && cat "$LOGDIR"/*.yaml; } 2>/dev/null
|
|
354
|
+
}
|
|
355
|
+
LOGGED=$(_int "$(_cat_logs | grep -cE "^- date: *'?$TODAY'?" | tr -d ' ' || true)")
|
|
223
356
|
if [ "${DISPATCHED:-0}" -gt 0 ] && [ "${LOGGED:-0}" -eq 0 ]; then
|
|
224
357
|
echo "❌ ④-e $DISPATCHED sub-agent dispatch(es) today and ZERO invocation-log entries — the 60/40"
|
|
225
358
|
echo " promotion gate and the UAP loop both read that file; an unlogged session is invisible to"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_branch_claim_lanes.sh — branch_claim.sh known-pair 앵커.
|
|
3
|
+
#
|
|
4
|
+
# 왜 실행 테스트인가: `bash -n` 은 계기가 아니다 — 문법만 보고 exit 0 을 내므로
|
|
5
|
+
# «전부 통과»와 구분이 안 된다([[feedback_gate_verification_must_execute]]).
|
|
6
|
+
# «막혔다» ≠ «옳게 막혔다» 이므로 BLOCK 레인은 **완성 문장**으로 매칭한다 —
|
|
7
|
+
# 조각 매칭은 claim/HEAD 역할을 뒤집는 뮤테이션을 통과시켰다(적대검증 A-5 실측).
|
|
8
|
+
# 그 역할 뒤바뀜은 사용자를 반대 브랜치로 보내므로 게이트가 사고를 만드는 쪽이 된다.
|
|
9
|
+
#
|
|
10
|
+
# 격리: 레인마다 임시 저장소를 새로 판다. 실제 레포는 안 건드린다.
|
|
11
|
+
# 세션 시뮬레이션: FH_CLAIM_TEST=1 + FH_CLAIM_SESSION 으로 두 세션을 흉내낸다.
|
|
12
|
+
#
|
|
13
|
+
# usage: bash scripts/test_branch_claim_lanes.sh
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
SCRIPT="$(cd "$(dirname "$0")" && pwd)/branch_claim.sh"
|
|
17
|
+
PASS=0; FAIL=0; LIVE_PID=""
|
|
18
|
+
|
|
19
|
+
cleanup() { [ -n "$LIVE_PID" ] && kill "$LIVE_PID" 2>/dev/null; return 0; }
|
|
20
|
+
trap cleanup EXIT
|
|
21
|
+
|
|
22
|
+
_mkrepo() {
|
|
23
|
+
local d; d=$(mktemp -d "${TMPDIR:-/tmp}/bclaim.XXXXXX")
|
|
24
|
+
git -C "$d" init -q -b main
|
|
25
|
+
git -C "$d" config user.email t@t; git -C "$d" config user.name t
|
|
26
|
+
echo x > "$d/f"; git -C "$d" add f; git -C "$d" commit -qm init
|
|
27
|
+
echo "$d"
|
|
28
|
+
}
|
|
29
|
+
# 살아있는 peer 세션 하나를 실제 프로세스로 만든다 (kill -0 이 참이 되도록)
|
|
30
|
+
# ⚠️ 반드시 fd 를 닫아서 띄운다: 백그라운드 프로세스가 stdout 을 물고 있으면
|
|
31
|
+
# 이 스크립트를 파이프에 물릴 때 파이프가 안 닫혀 무한 대기한다(실측: 120s 타임아웃).
|
|
32
|
+
_spawn_live() { sleep 900 >/dev/null 2>&1 & LIVE_PID=$!; echo "$LIVE_PID"; }
|
|
33
|
+
|
|
34
|
+
_lane() { # name expected_rc actual_rc [must_contain...]
|
|
35
|
+
local name="$1" exp="$2" got="$3"; shift 3
|
|
36
|
+
local out="$LANE_OUT" ok=1 pat
|
|
37
|
+
[ "$got" = "$exp" ] || { ok=0; echo " ❌ $name — rc=$got (기대 $exp)"; }
|
|
38
|
+
for pat in "$@"; do
|
|
39
|
+
case "$out" in *"$pat"*) ;; *) ok=0; echo " ❌ $name — 출력에 «${pat}» 없음";; esac
|
|
40
|
+
done
|
|
41
|
+
if [ "$ok" = 1 ]; then echo " ✅ $name"; PASS=$((PASS+1)); else FAIL=$((FAIL+1));
|
|
42
|
+
echo " ── 실제 출력 ──"; echo "$out" | sed 's/^/ /'; fi
|
|
43
|
+
}
|
|
44
|
+
_ok() { if [ "$2" = 1 ]; then echo " ✅ $1"; PASS=$((PASS+1)); else echo " ❌ $1"; FAIL=$((FAIL+1)); fi; }
|
|
45
|
+
|
|
46
|
+
# 세션 B(=peer)의 claim 을 살아있는 pid 로 직접 심는다
|
|
47
|
+
_plant_peer() { # repo branch pid
|
|
48
|
+
mkdir -p "$1/.git/fh-claims"
|
|
49
|
+
printf 'branch=%s\nlabel=peer\npid=%s\nat_epoch=%s\n' "$2" "$3" "$(date +%s)" \
|
|
50
|
+
> "$1/.git/fh-claims/session-PEER"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# ⚠️ 주변 환경을 흡입하지 않는다 — 이 한 줄이 없어서 이 스위트는 «거짓 초록» 이었다.
|
|
54
|
+
# branch_claim.sh 의 `_session_pid() { echo "${CLAUDE_PID:-${PPID:-$$}}"; }` 때문에, 저자 셸에
|
|
55
|
+
# CLAUDE_PID 가 있으면 claim 이 **살아있는 pid**(그 Claude 프로세스)를 기록한다. 레인 ③ 은
|
|
56
|
+
# A 가 재-claim 해서 peer 기록을 덮는 시나리오라, 그 pid 가 살아있느냐가 판정을 통째로 뒤집는다.
|
|
57
|
+
# 실측 2026-08-09 (known-pair, 같은 트리·같은 커밋):
|
|
58
|
+
# CLAUDE_PID 있음 → 24 PASS / 0 FAIL (저자 로컬 — 초록)
|
|
59
|
+
# CLAUDE_PID 없음 → 23 PASS / 1 FAIL ③ (CI — 적색, rc=0 기대 1)
|
|
60
|
+
# 즉 코드가 아니라 **저자 환경**을 재고 있었고, 그 초록이 S-1 재설계의 유일한 증명이었다.
|
|
61
|
+
# 앵커가 배선되기 전까지(=호출부 0개) 이 사실은 드러날 수 없었다.
|
|
62
|
+
unset CLAUDE_PID
|
|
63
|
+
|
|
64
|
+
echo "[branch-claim] known-pair 앵커"
|
|
65
|
+
PEERPID=$(_spawn_live)
|
|
66
|
+
|
|
67
|
+
# ── ① 기록 없음 → 통과 (부재 ≠ 위반) ───────────────────────────────────────
|
|
68
|
+
R=$(_mkrepo); LANE_OUT=$(cd "$R" && bash "$SCRIPT" check 2>&1); rc=$?
|
|
69
|
+
_lane "① 기록 없음 → 통과" 0 "$rc"; rm -rf "$R"
|
|
70
|
+
|
|
71
|
+
# ── ② 내 기록 == HEAD → 통과, 그리고 at_epoch 이 갱신된다 (A-6) ────────────
|
|
72
|
+
R=$(_mkrepo)
|
|
73
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=1000 bash "$SCRIPT" claim a >/dev/null 2>&1)
|
|
74
|
+
before=$(sed -n 's/^at_epoch=//p' "$R/.git/fh-claims/me")
|
|
75
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=99000 bash "$SCRIPT" check 2>&1); rc=$?
|
|
76
|
+
after=$(sed -n 's/^at_epoch=//p' "$R/.git/fh-claims/me")
|
|
77
|
+
_lane "② 내 기록==HEAD → 통과" 0 "$rc"
|
|
78
|
+
_ok "②-b at_epoch 갱신됨 (${before} -> ${after}) — 나이가 «유휴»를 재게 된다" "$([ "$after" = 99000 ] && echo 1 || echo 0)"
|
|
79
|
+
rm -rf "$R"
|
|
80
|
+
|
|
81
|
+
# ── ③ ★ S-1 회귀: 양쪽 세션이 다 프로토콜을 지켜도 잡는다 ──────────────────
|
|
82
|
+
# 초판 설계는 여기서 통과했다(= 오늘 사고를 못 막았다). 이 레인이 그 반증이다.
|
|
83
|
+
# 타임라인: B가 main claim → A가 switch+claim(프로토콜 준수) → B가 커밋 시도
|
|
84
|
+
R=$(_mkrepo)
|
|
85
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=B bash "$SCRIPT" claim bee >/dev/null 2>&1)
|
|
86
|
+
_plant_peer "$R" main "$PEERPID" # A 를 살아있는 peer 로
|
|
87
|
+
git -C "$R" switch -q -c ifkakao # A 가 트리를 옮기고
|
|
88
|
+
# A 는 «살아있는» 세션이다 — 그 사실을 환경에서 빌리지 말고 레인이 직접 고정한다.
|
|
89
|
+
# CLAUDE_PID 를 안 주면 claim 이 곧 죽을 서브셸의 PPID 를 기록하고, B 의 check 는
|
|
90
|
+
# «살아있는 peer 0» 으로 통과한다 — 시나리오가 성립하지도 않은 채 초록이 된다.
|
|
91
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=session-PEER CLAUDE_PID="$PEERPID" bash "$SCRIPT" claim ay >/dev/null 2>&1) # 프로토콜대로 claim
|
|
92
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=B bash "$SCRIPT" check 2>&1); rc=$?
|
|
93
|
+
_lane "③ ★S-1 회귀: 양쪽이 프로토콜을 지켜도 B 는 차단된다" 1 "$rc" \
|
|
94
|
+
"이 세션은 'main' 로 기록돼 있는데 HEAD 는 'ifkakao' 다" "git switch -- 'main'"
|
|
95
|
+
rm -rf "$R"
|
|
96
|
+
|
|
97
|
+
# ── ④ 불일치인데 살아있는 peer 없음 → 경고만, 통과 (A-2 과차단 방지) ───────
|
|
98
|
+
R=$(_mkrepo)
|
|
99
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
100
|
+
git -C "$R" switch -q -c next
|
|
101
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" check 2>&1); rc=$?
|
|
102
|
+
_lane "④ 1인 세션(살아있는 peer 0) → 차단 안 함" 0 "$rc" "살아있는 peer 세션이 없어"
|
|
103
|
+
rm -rf "$R"
|
|
104
|
+
|
|
105
|
+
# ── ④-b 컨트롤: 같은 상황에 살아있는 peer 가 있으면 차단 ───────────────────
|
|
106
|
+
R=$(_mkrepo)
|
|
107
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
108
|
+
_plant_peer "$R" whatever "$PEERPID"
|
|
109
|
+
git -C "$R" switch -q -c next
|
|
110
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" check 2>&1); rc=$?
|
|
111
|
+
_lane "④-b 컨트롤: peer 살아있으면 차단 (④가 무조건 통과가 아님)" 1 "$rc"
|
|
112
|
+
rm -rf "$R"
|
|
113
|
+
|
|
114
|
+
# ── ④-c 죽은 peer 는 동시성 증거가 아니다 ──────────────────────────────────
|
|
115
|
+
R=$(_mkrepo)
|
|
116
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
117
|
+
_plant_peer "$R" whatever 999999 # 존재하지 않는 pid
|
|
118
|
+
git -C "$R" switch -q -c next
|
|
119
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=solo bash "$SCRIPT" check 2>&1); rc=$?
|
|
120
|
+
_lane "④-c 죽은 peer → 차단 안 함 (시간 상수 의존을 줄인다)" 0 "$rc"
|
|
121
|
+
rm -rf "$R"
|
|
122
|
+
|
|
123
|
+
# ── ⑤ STALE → 통과 / 임계 미만 → 차단 ──────────────────────────────────────
|
|
124
|
+
R=$(_mkrepo)
|
|
125
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=0 bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
126
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other
|
|
127
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=$((13*3600)) bash "$SCRIPT" check 2>&1); rc=$?
|
|
128
|
+
_lane "⑤ STALE(13h) → 통과" 0 "$rc" "STALE"
|
|
129
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_CLAIM_NOW=$((11*3600)) bash "$SCRIPT" check 2>&1); rc=$?
|
|
130
|
+
_lane "⑤-b 컨트롤: 11h → 여전히 차단" 1 "$rc"
|
|
131
|
+
rm -rf "$R"
|
|
132
|
+
|
|
133
|
+
# ── ⑥ override → 통과 + **파일에 실제로 기록** (A-4 팬텀 주장 수리) ────────
|
|
134
|
+
R=$(_mkrepo)
|
|
135
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
136
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other
|
|
137
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me FH_BRANCH_CLAIM_OK=1 bash "$SCRIPT" check 2>&1); rc=$?
|
|
138
|
+
_lane "⑥ override → 통과" 0 "$rc" "FH_BRANCH_CLAIM_OK"
|
|
139
|
+
_ok "⑥-b override 로그 파일이 실제로 생성·append 됨" \
|
|
140
|
+
"$([ -s "$R/.git/fh-branch-claim-overrides.log" ] && echo 1 || echo 0)"
|
|
141
|
+
rm -rf "$R"
|
|
142
|
+
|
|
143
|
+
# ── ⑦ detached / rebase 중 → 통과 (A-1 과차단 수리) ────────────────────────
|
|
144
|
+
R=$(_mkrepo)
|
|
145
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
146
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" checkout -q --detach
|
|
147
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
148
|
+
_lane "⑦ detached HEAD → 통과 (rebase/bisect 중 커밋을 막지 않는다)" 0 "$rc"
|
|
149
|
+
rm -rf "$R"
|
|
150
|
+
R=$(_mkrepo)
|
|
151
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
152
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other; mkdir -p "$R/.git/rebase-merge"
|
|
153
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
154
|
+
_lane "⑦-b rebase 진행 중 → 통과" 0 "$rc"
|
|
155
|
+
rm -rf "$R"
|
|
156
|
+
|
|
157
|
+
# ── ⑧ 테스트 노브는 FH_CLAIM_TEST=1 없이 무시된다 (A-3 무음 우회 차단) ─────
|
|
158
|
+
R=$(_mkrepo)
|
|
159
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
160
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other
|
|
161
|
+
# 테스트 모드 없이 노브만 주면 → 세션ID 폴백이 달라 기록을 못 찾아 통과할 수 있으므로,
|
|
162
|
+
# 실제 세션ID를 고정해 두고 STALE/NOW 노브가 먹지 않는지만 본다
|
|
163
|
+
export CLAUDE_CODE_SESSION_ID=me
|
|
164
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_STALE_HOURS=0 FH_CLAIM_NOW=99999999999 bash "$SCRIPT" check 2>&1); rc=$?
|
|
165
|
+
_lane "⑧ FH_CLAIM_TEST 없이 STALE/NOW 노브 → 무시되고 차단 유지" 1 "$rc"
|
|
166
|
+
unset CLAUDE_CODE_SESSION_ID
|
|
167
|
+
rm -rf "$R"
|
|
168
|
+
|
|
169
|
+
# ── ⑨ 워크트리는 기록이 분리된다 ───────────────────────────────────────────
|
|
170
|
+
R=$(_mkrepo)
|
|
171
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
172
|
+
WT="$R-wt"; git -C "$R" worktree add -q -b wtb "$WT" >/dev/null 2>&1
|
|
173
|
+
if [ -d "$WT" ]; then
|
|
174
|
+
LANE_OUT=$(cd "$WT" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
175
|
+
_lane "⑨ 워크트리는 기록 분리 → 통과" 0 "$rc"
|
|
176
|
+
git -C "$R" worktree remove --force "$WT" >/dev/null 2>&1
|
|
177
|
+
else echo " ⚠️ ⑨ 워크트리 생성 실패 — SKIPPED(통과로 세지 않음)"; fi
|
|
178
|
+
rm -rf "$R" "$WT"
|
|
179
|
+
|
|
180
|
+
# ── ⑩ 서브디렉터리에서도 같은 기록을 본다 (상대 .git 절대화) ───────────────
|
|
181
|
+
R=$(_mkrepo)
|
|
182
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
183
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q -c other; mkdir -p "$R/deep/nested"
|
|
184
|
+
LANE_OUT=$(cd "$R/deep/nested" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
185
|
+
_lane "⑩ 서브디렉터리에서도 차단" 1 "$rc"
|
|
186
|
+
_ok "⑩-b 기록이 저장소 .git 에만 생성됨" \
|
|
187
|
+
"$([ -d "$R/.git/fh-claims" ] && [ ! -d "$R/deep/nested/.git" ] && echo 1 || echo 0)"
|
|
188
|
+
rm -rf "$R"
|
|
189
|
+
|
|
190
|
+
# ── ⑪ release / reap / show 도 앵커가 있다 (B-3 무앵커 경로 수리) ──────────
|
|
191
|
+
R=$(_mkrepo)
|
|
192
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
193
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" release >/dev/null 2>&1)
|
|
194
|
+
_ok "⑪ release 가 실제로 기록을 지운다" "$([ ! -f "$R/.git/fh-claims/me" ] && echo 1 || echo 0)"
|
|
195
|
+
_plant_peer "$R" x 999999
|
|
196
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" reap >/dev/null 2>&1)
|
|
197
|
+
_ok "⑪-b reap 이 죽은 세션 기록을 지운다" "$([ ! -f "$R/.git/fh-claims/session-PEER" ] && echo 1 || echo 0)"
|
|
198
|
+
_plant_peer "$R" zzz "$PEERPID"
|
|
199
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" show 2>&1); rc=$?
|
|
200
|
+
_lane "⑪-c show 가 HEAD·세션·peer 를 낸다" 0 "$rc" "HEAD" "살아있는 peer claim: 1" "zzz"
|
|
201
|
+
rm -rf "$R"
|
|
202
|
+
|
|
203
|
+
# ── ⑫ 복구 명령의 브랜치명이 shell-quote 된다 (cross-family B-6) ──────────
|
|
204
|
+
# git ref 는 공백은 못 쓰지만 `;` `$` 는 허용한다. 복붙 시 주입형 사고.
|
|
205
|
+
R=$(_mkrepo); B='a;touch$IFS/tmp/BCLAIM_PWNED'
|
|
206
|
+
git -C "$R" branch "$B" >/dev/null 2>&1 && git -C "$R" switch -q "$B"
|
|
207
|
+
(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim s >/dev/null 2>&1)
|
|
208
|
+
_plant_peer "$R" x "$PEERPID"; git -C "$R" switch -q main
|
|
209
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" check 2>&1); rc=$?
|
|
210
|
+
EXPECT="git switch -- '${B}'"
|
|
211
|
+
_lane "⑫ metachar 브랜치명이 인용된 채 출력된다" 1 "$rc" "$EXPECT"
|
|
212
|
+
rm -rf "$R"
|
|
213
|
+
|
|
214
|
+
# ── ⑬ --if-absent: 없으면 만들고, 있으면 안 덮는다 (세션시작 훅이 부르는 형태) ──
|
|
215
|
+
# 훅 안에 같은 로직을 복제하면 앵커가 안 걸리므로 스크립트에 두고 여기서 잡는다.
|
|
216
|
+
R=$(_mkrepo)
|
|
217
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim --if-absent s 2>&1); rc=$?
|
|
218
|
+
_ok "⑬ --if-absent: 기록 없으면 생성" \
|
|
219
|
+
"$([ "$rc" = 0 ] && [ -f "$R/.git/fh-claims/me" ] && echo 1 || echo 0)"
|
|
220
|
+
git -C "$R" switch -q -c moved # 브랜치가 옮겨진 뒤 다시 불러도
|
|
221
|
+
LANE_OUT=$(cd "$R" && FH_CLAIM_TEST=1 FH_CLAIM_SESSION=me bash "$SCRIPT" claim --if-absent s 2>&1); rc=$?
|
|
222
|
+
kept=$(sed -n 's/^branch=//p' "$R/.git/fh-claims/me")
|
|
223
|
+
_lane "⑬-b --if-absent: 기록 있으면 덮지 않는다" 0 "$rc" "이미 있다"
|
|
224
|
+
_ok "⑬-c 기존 값 보존 (main 이어야 함 · 지금=$kept)" "$([ "$kept" = main ] && echo 1 || echo 0)"
|
|
225
|
+
# ★ 이게 중요한 이유: 덮으면 「내가 믿는 브랜치」가 매 세션시작마다 HEAD 로 갱신돼
|
|
226
|
+
# 게이트가 영원히 통과한다 — 자동화가 게이트를 무장해제하는 경로다
|
|
227
|
+
rm -rf "$R"
|
|
228
|
+
|
|
229
|
+
echo
|
|
230
|
+
echo "[branch-claim] PASS=$PASS FAIL=$FAIL"
|
|
231
|
+
[ "$FAIL" -eq 0 ] || exit 1
|