@chrono-meta/fh-gate 2.15.1 β†’ 3.1.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 (134) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +88 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +13 -3
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +43 -6
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +35 -1
  21. package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
  22. package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
  23. package/package.json +48 -2
  24. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  25. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  26. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  27. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  30. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  31. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  32. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  33. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  34. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  35. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  36. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  37. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  38. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  39. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  40. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  41. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  42. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  43. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  44. package/plugins/fh-meta/CHANGELOG.md +67 -0
  45. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  47. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  49. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  50. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  51. package/plugins/fh-qp/README.md +71 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  53. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  54. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  56. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  59. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  60. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  61. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  62. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  63. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  64. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  65. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  66. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  67. package/scripts/backtick_guard.sh +194 -0
  68. package/scripts/chamber_run.sh +14 -5
  69. package/scripts/com.forge-harness.live-eval.plist +84 -0
  70. package/scripts/compaction_probe.sh +9 -35
  71. package/scripts/context_continuity_score.sh +49 -7
  72. package/scripts/directional_diff_gate.sh +14 -2
  73. package/scripts/fh-gate.sh +3 -3
  74. package/scripts/files_manifest_shipping_check.sh +19 -0
  75. package/scripts/frontier_digest_autopilot.sh +4 -1
  76. package/scripts/gate_pathspec_check.sh +1 -1
  77. package/scripts/map_postprocess.py +90 -0
  78. package/scripts/outbound_query_guard.sh +131 -0
  79. package/scripts/outbound_query_hook.sh +373 -0
  80. package/scripts/package_coverage_check.sh +79 -18
  81. package/scripts/pipe_verdict_guard.sh +41 -1
  82. package/scripts/probe_live_eval.sh +240 -0
  83. package/scripts/probe_live_eval_lib.py +579 -0
  84. package/scripts/proposal_hook.sh +192 -0
  85. package/scripts/public_surface_scan_files.sh +11 -2
  86. package/scripts/push_zone_check.sh +78 -0
  87. package/scripts/residency_closure_scan.py +252 -0
  88. package/scripts/revert_probe.sh +250 -0
  89. package/scripts/selfcheck.sh +106 -3
  90. package/scripts/session_close_check.sh +100 -0
  91. package/scripts/sim_isolated_run.sh +195 -9
  92. package/scripts/test_action_yml_lanes.sh +97 -0
  93. package/scripts/test_backtick_guard_lanes.sh +115 -0
  94. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  95. package/scripts/test_fh_qp_lanes.sh +105 -0
  96. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  97. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  98. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  99. package/scripts/test_lane_runner_lanes.sh +59 -33
  100. package/scripts/test_map_postprocess_lanes.sh +143 -0
  101. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  102. package/scripts/test_marker_affected_lanes.sh +93 -0
  103. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  104. package/scripts/test_marker_oracle_lanes.sh +136 -0
  105. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  106. package/scripts/test_node_check_lanes.sh +34 -34
  107. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  108. package/scripts/test_outbound_query_lanes.sh +87 -0
  109. package/scripts/test_package_coverage_lanes.sh +53 -27
  110. package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
  111. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  112. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  113. package/scripts/test_preprep_drift_anchor.sh +16 -7
  114. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  115. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  116. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  117. package/scripts/test_proposal_hook_lanes.sh +57 -0
  118. package/scripts/test_push_zone_lanes.sh +304 -0
  119. package/scripts/test_residency_closure_lanes.sh +70 -0
  120. package/scripts/test_revert_probe_lanes.sh +146 -0
  121. package/scripts/test_session_close_lanes.sh +3 -5
  122. package/scripts/test_sim_isolated_run_lanes.sh +136 -0
  123. package/scripts/test_utterance_intake_lanes.sh +414 -0
  124. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  125. package/scripts/transcript_utterances.py +222 -0
  126. package/scripts/utterance_intake.sh +424 -0
  127. package/scripts/utterance_landing_check.sh +2 -2
  128. package/scripts/validate_yaml.sh +27 -0
  129. package/scripts/worktree_reclaim.sh +95 -0
  130. package/templates/.git-hooks/pre-commit +380 -5
  131. package/templates/.git-hooks/pre-push +91 -0
  132. package/templates/RED_TEAM_REPORT.md +49 -0
  133. package/templates/settings.PreToolUse.snippet.json +101 -1
  134. package/plugins/fh-commons/README.md +0 -38
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env bash
2
+ # utterance_intake.sh β€” μ „μ‚¬λ³Έμ˜ 운영자 λ°œν™”λ₯Ό **μžλ™μœΌλ‘œ** ν”„λ‘œλΈŒλ‘œ λ§Œλ“€μ–΄ 기둝 μ°©μ§€λ₯Ό κ²€μ¦ν•œλ‹€.
3
+ #
4
+ # ── μ™œ (진단, 2026-09-05) ────────────────────────────────────────────────────
5
+ # FH 의 Β«μ§ˆλ¬Έν•˜κΈ°Β» 엔진은 external-grounding 엔진인데 **수용 절반이 산문이닀**
6
+ # (`fh_three_layer_canon.md Β§2`: *"λ¬»λŠ” κ²½λ‘œλŠ” 있고 λ“€μ–΄μ˜¨ 톡찰을 μžμ‚°ν™”ν•˜λŠ” κ²½λ‘œκ°€ μ•½ν•˜λ‹€"*).
7
+ # κ³„κΈ°λŠ” 이미 λ‘˜ λ‹€ μžˆμ—ˆλ‹€:
8
+ # compaction_probe.sh seal 전사본 β†’ 운영자 λ°œν™” μΆ•μž 원μž₯ (λˆλ‹€)
9
+ # utterance_landing_check.sh probes.tsv β†’ 기둝 파일 grep (λˆλ‹€, ν˜ΈμΆœλΆ€ 0)
10
+ # 그런데 **probes.tsv λ₯Ό λ§Œλ“œλŠ” 기계가 μ—†μ—ˆλ‹€.** μ†μœΌλ‘œ μ§œμ•Ό ν•˜λ‹ˆ 아무도 μ•ˆ μ§°κ³ ,
11
+ # `session_close_check.sh` μ–΄λ””μ—μ„œλ„ 두 λ²ˆμ§Έκ°€ ν˜ΈμΆœλ˜μ§€ μ•Šμ•˜λ‹€(grep 0). 두 계기 μ‚¬μ΄μ˜
12
+ # ν•œ 칸이 λΉ„μ–΄ μžˆμ–΄μ„œ 채널이 μ•„λ‹ˆλΌ μ‚°λ¬ΈμœΌλ‘œ 남아 있던 것 β€” 이 μŠ€ν¬λ¦½νŠΈκ°€ κ·Έ 칸이닀.
13
+ #
14
+ # μ‹€μΈ‘ κ·Όκ±°(같은 λ‚ ): μš΄μ˜μžκ°€ μ„Έμ…˜ 쀑 λ˜μ§„ ꡐ리·섀계급 λ°œν™” 6 건이 μ°©μ§€ν•œ κ²½λ‘œλŠ” μ „λΆ€
15
+ # **κ±°λ²„λ„ˆμ˜ 손**μ΄μ—ˆλ‹€(signal 파일 2 Β· UAP 블둝 2 Β· fh_completed). κΈ°κ³„λŠ” ν•œ 건도 μ•ˆ μž‘μ•˜λ‹€.
16
+ #
17
+ # ── 이 μŠ€ν¬λ¦½νŠΈκ°€ ν•˜λŠ” 것 / μ•ˆ ν•˜λŠ” 것 ──────────────────────────────────────
18
+ # ν•œλ‹€ : λ°œν™” μΆ”μΆœ β†’ ν‚€ 낱말 ν”„λ‘œλΈŒ 생성 β†’ 컨트둀 2 μ’… μ‚½μž… β†’ μ°©μ§€ 검증 β†’ λ―Έμ°©μ§€ λͺ©λ‘
19
+ # μ•ˆ ν•œλ‹€: 기둝을 **μ“°μ§€ μ•ŠλŠ”λ‹€.** 무엇을 어디에 μ μ„μ§€λŠ” νŒλ‹¨μ΄κ³ , νŒλ‹¨μ„ μ½”λ“œλ‘œ ꡳ히면
20
+ # 그게 천μž₯이 λœλ‹€(`CLAUDE.md Β§Mechanization Boundary`). 이건 «채널이 값을
21
+ # λ‚˜λ₯΄λŠ”κ°€Β» 만 λ³Έλ‹€ β€” Β«κ·Έ 값이 μ˜³μ€κ°€Β» λŠ” μ‚¬λžŒ λͺ«μ΄λ‹€.
22
+ #
23
+ # ── πŸŸ₯ λͺ…λͺ…λœ μž”μ—¬ μ…‹. μΈμš©ν•˜κΈ° 전에 읽어라 ─────────────────────────────────
24
+ # ⓐ **μ΄λ¦„ν‘œλŠ” 츑정이 μ•„λ‹ˆλ‹€** β€” ν”„λ‘œλΈŒλŠ” Β«ν‚€ 낱말» μ΄λΌμ„œ, 기둝이 같은 λœ»μ„ **λ‹€λ₯Έ λ‚±λ§λ‘œ**
25
+ # μ μ—ˆμœΌλ©΄ μ°©μ§€ν–ˆλŠ”λ° λ―Έμ°©μ§€λ‘œ λœ¬λ‹€. 상속: `utterance_landing_check.sh` ν—€λ”μ˜
26
+ # *"rc=1 은 Β«κΈ°λ‘ν•˜λΌΒ»κ°€ μ•„λ‹ˆλΌ Β«ν™•μΈν•˜λΌΒ»λ‘œ 읽어야 ν•œλ‹€"*. 이 λ°©ν–₯은 **과차단이라 μ•ˆμ „**ν•˜λ‹€.
27
+ # β“‘ **ν† ν°λ§ˆλ‹€ ν•œ ν–‰, λ°œν™”λŠ” 과반으둜 νŒμ •ν•œλ‹€.** μ΄ˆνŒμ€ ν•œ ν–‰ `(낱말1|낱말2|낱말3)` 이라
28
+ # **ν•˜λ‚˜λ§Œ 겹쳐도** μ°©μ§€μ˜€λ‹€ β€” 같은 μ£Όμ œμ–΄ ν•˜λ‚˜(Β«μΏ λ²„λ„€ν‹°μŠ€Β»)κ°€ λ“  기둝이 μ •μ±… λ°œν™”λ₯Ό Β«μ°©μ§€Β»
29
+ # 둜 λ Œλ”ν–ˆλ‹€(cross-family codex, 2026-09-05). 이제 λ°œν™”μ˜ 졜μž₯ 토큰(≀3)을 **각각 ν•œ ν–‰**으둜
30
+ # λ‚΄κ³ , λ°œν™”λŠ” μ°©μ§€ 행이 ⌈n/2βŒ‰ 이상일 λ•Œλ§Œ μ°©μ§€λ‹€(3 ν‚€ β†’ 2 Β· 2 ν‚€ β†’ 1 Β· 1 ν‚€ β†’ 1).
31
+ # πŸŸ₯ 남은 fail-open: ν‚€κ°€ 2 개뿐인 λ°œν™”λŠ” μ—¬μ „νžˆ ν•˜λ‚˜λ§Œ 겹쳐도 μ°©μ§€λ‹€(OR). rc=0 은 μ—¬μ „νžˆ
32
+ # Β«μ œλŒ€λ‘œ μ ν˜”λ‹€Β» 의 증λͺ…이 μ•„λ‹ˆλ‹€. λ°œν™” λ‹¨μœ„ 계상은 map 의 n_keys 둜 이 μŠ€ν¬λ¦½νŠΈκ°€ μ§‘κ³„ν•œλ‹€.
33
+ # β“’ **advisory λ‹€** β€” `session_close_check.sh β‘ -f` λŠ” 이걸 μ°¨λ‹¨μœΌλ‘œ μ“°μ§€ μ•ŠλŠ”λ‹€. λ…ΈμΆœ 사닀리
34
+ # (`fh_three_layer_canon Β§1-a-2 β“”` shadow) 둜 λͺ‡ μ„Έμ…˜ κ΄€μ°°ν•œ λ’€ 승격 μ—¬λΆ€λŠ” 운영자 κ²°μ •.
35
+ #
36
+ # ── 컨트둀 두 μ’… (ν•œ 쒅이 μ•„λ‹ˆλ‹€) ───────────────────────────────────────────
37
+ # `utterance_landing_check.sh` 의 CONTROL 은 **μ–‘μ„±λ§Œ** μžˆλ‹€ β€” γ€Œκ³„κΈ°κ°€ μ£½μ—ˆλ‚˜γ€λŠ” 재고
38
+ # γ€Œκ³„κΈ°κ°€ μ•„λ¬΄κ±°λ‚˜ λ‹€ μž‘λ‚˜γ€λŠ” λͺ» μž°λ‹€(`[[feedback_control_presence_is_not_discrimination]]`).
39
+ # POSITIVE **기둝 λ‚΄μš©μ—μ„œ 뽑은 졜μž₯ 토큰.** κ±°κΈ° μ‹€μ œλ‘œ μžˆμœΌλ‹ˆ μ •μ˜μƒ μž‘ν˜€μ•Ό ν•˜κ³ ,
40
+ # μ•ˆ 작히면 νŒŒμ΄ν”„λΌμΈμ΄ 죽은 것이닀(word-split Β· 경둜 Β· 인코딩) β†’ rc=10.
41
+ # πŸŸ₯ μ΄ˆνŒμ€ «였늘 λ‚ μ§œΒ» μ˜€λŠ”λ° **λ‚ μ§œλŠ” 파일λͺ…에 μžˆμ§€ λ‚΄μš©μ— μžˆμ„ μ˜λ¬΄κ°€ μ—†λ‹€** β€”
42
+ # 그러면 정상 λ§ˆκ°λ§ˆλ‹€ κ±°μ§“ 경보가 λœ¬λ‹€. μ‹€μΈ‘ κ·Όκ±°λŠ” μ•„λž˜ `_control_rows` 주석.
43
+ # λ‚ μ§œλŠ” Β«μžˆμ„ λ•Œλ§ŒΒ» 보쑰 컨트둀둜 λ”ν•œλ‹€.
44
+ # NEGATIVE λ¬΄μž‘μœ„ 토큰 β€” **μ—†μ–΄μ•Ό λ§ˆλ•…ν•˜λ‹€.** 작히면 계기가 μ•„λ¬΄κ±°λ‚˜ μž‘λŠ” κ²ƒμ΄λ―€λ‘œ μ—­μ‹œ rc=10
45
+ # NEGATIVE λŠ” 이 λž˜νΌκ°€ 직접 κ²€μ‚¬ν•œλ‹€(ν•˜μœ„ 도ꡬ가 λͺ¨λ₯΄λŠ” μ’…λ₯˜λΌ κ·Έ 행은 λ¬΄μ‹œν•˜κ³  μ§€λ‚˜κ°„λ‹€).
46
+ #
47
+ # ── μ‚¬μš© ────────────────────────────────────────────────────────────────────
48
+ # bash scripts/utterance_intake.sh <transcript.jsonl> <record-file> [<record-file>...]
49
+ # env:
50
+ # FH_INTAKE_MIN_CHARS κΈ°λ³Έ 20 β€” μ•„λž˜ Β§λ¬Έν„± μ°Έμ‘°
51
+ # FH_INTAKE_NEG_TOKEN μŒμ„± 컨트둀 토큰 κ³ μ •(레인용). 미섀정이면 λ§€ μ‹€ν–‰ λ¬΄μž‘μœ„
52
+ # FH_INTAKE_DUMP_PROBES μƒμ„±λœ probes.tsv λ₯Ό 이 κ²½λ‘œλ‘œλ„ 남긴닀(레인/λ””λ²„κ·Έμš©)
53
+ #
54
+ # ── Β§λ¬Έν„±: μ™œ 40 이 μ•„λ‹ˆλΌ 20 인가 (μ‹€μΈ‘ 2026-09-05) ────────────────────────
55
+ # 이 레포 전사본 μ „μˆ˜ 3191 λ°œν™”(λ΄‰νˆ¬Β·νˆ΄κ²°κ³Ό μ œμ™Έ)의 길이 뢄포:
56
+ # 00-09 524 Β· 10-19 329 Β· 20-29 284 Β· 30-39 187 Β· 40+ 1867
57
+ # 30–39 ꡬ간 ν‘œλ³Έμ— ν•˜μ€‘ μ§€λŠ” λ°œν™”κ°€ μ‚°λ‹€ β€” «그리고 μž…μž₯λ¦¬λ·°λŠ” 정적 동적 λͺ¨λ‘ μˆ˜ν–‰ν•΄μ•Όν•΄Β» Β·
58
+ # Β«4λŠ” 초둝으둜 μ˜¬λ¦¬λ„λ‘ ν•˜μžΒ». ν•œκΈ€μ€ μžλ‹Ή μ •λ³΄λŸ‰μ΄ μ»€μ„œ **40 자 λ°”λ‹₯은 ꡐ리 λ°œν™”λ₯Ό 쑰용히
59
+ # 버린닀**(μœ„ν—˜ν•œ λ°©ν–₯). 20 = 853 μ œμ™Έ Β· 2338 보쑴. 과포함은 μ†ŒμŒ(μ•ˆμ „), κ³Όμ†Œν¬ν•¨μ€ 무음 손싀.
60
+ #
61
+ # ── μ’…λ£Œμ½”λ“œ ────────────────────────────────────────────────────────────────
62
+ # 0 전건 μ°©μ§€ 1 λ―Έμ°©μ§€ 있음 (λͺ©λ‘ 인쇄)
63
+ # 10 HARNESS-ERROR / UNMEASURED β€” **PASS 도 FAIL 도 μ•„λ‹ˆλ‹€**
64
+ # (전사본 λΆ€μž¬ Β· μΆ”μΆœ λ°œν™” 0 Β· 기둝 파일 0 Β· 컨트둀 사망 Β· ν”„λ‘œλΈŒ 였λ₯˜ Β· rc↔λͺ©λ‘ 뢈일치)
65
+ #
66
+ # 이식성: bash 3.2 Β· λ°±ν‹± μ—†μŒ Β· heredoc 은 ν•¨μˆ˜ μ•ˆ + 인용 κ΅¬λΆ„μž. python3 ν‘œμ€€ 라이브러리만.
67
+
68
+ set -uo pipefail
69
+
70
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
71
+ EXTRACTOR="$REPO_ROOT/scripts/transcript_utterances.py"
72
+ LANDING="$REPO_ROOT/scripts/utterance_landing_check.sh"
73
+ MIN_CHARS="${FH_INTAKE_MIN_CHARS:-20}"
74
+
75
+ _die10() { echo "$1"; exit 10; }
76
+
77
+ # πŸŸ₯ `grep -c x f || echo 0` 은 무맀치일 λ•Œ Β«0Β» 을 **두 번** λ‚Έλ‹€ β€” grep 이 0 을 찍고 rc=1 을
78
+ # λ‚΄λ―€λ‘œ ν΄λ°±κΉŒμ§€ λΆ™λŠ”λ‹€. `[ "0\n0" -eq 0 ]` 은 Β«integer expression expectedΒ» 둜 μ£½κ³ ,
79
+ # κ·Έ 죽음이 계기 μ‚¬λ§μœΌλ‘œ λ Œλ”λœλ‹€(레인이 L1/L4b/L8b μ—μ„œ μž‘μ•˜λ‹€).
80
+ # `[[feedback_pipefail_fallback_disarms_guard]]` 의 μ‚¬μ΄Œ β€” 폴백이 값을 μ˜€μ—Όμ‹œν‚€λŠ” ν˜•νƒœ.
81
+ _num() { case "${1:-}" in ''|*[!0-9]*) echo 0 ;; *) printf '%s' "$1" ;; esac; }
82
+
83
+ # ── μ–‘μ„± 컨트둀 ───────────────────────────────────────────
84
+ # πŸŸ₯ μ΄ˆνŒμ€ «였늘 λ‚ μ§œΒ» λ₯Ό μ–‘μ„± 컨트둀둜 썼닀 β€” «였늘자 기둝 νŒŒμΌμ—λŠ” 였늘 λ‚ μ§œκ°€
85
+ # λ°˜λ“œμ‹œ μžˆλ‹€Β» λŠ” κ°€μ •μ΄μ—ˆκ³ , **거짓이닀.** λ‚ μ§œλŠ” 파일λͺ…에 μžˆμ§€ λ‚΄μš©μ— μžˆμ„ μ˜λ¬΄κ°€ μ—†λ‹€.
86
+ # μ‹€μΈ‘(2026-09-05 sim): ν”Œλ‘œμ–΄ ν‹°μ–΄ μ„Έμ…˜μ΄ λ§Œλ“  `fh_completed_2026-09-05.md` λ‚΄μš©μ€
87
+ # Β«- βœ… 계기 확인 β€” 클둠 μ•ˆ μ“°κΈ° κ°€λŠ₯Β» ν•œ 쀄뿐이닀. 그러면 컨트둀이 μ£½μ–΄ rc=10 이 λ‚˜κ³ ,
88
+ # 정상 λ§ˆκ°μ— κ±°μ§“ 경보가 λœ¬λ‹€ β€” 그것이 λ°”λ‘œ μŠ€ν‚€λ°μ„ ν›ˆλ ¨μ‹œν‚€λŠ” μ†ŒμŒμ΄λ‹€.
89
+ # β‡’ μ»¨νŠΈλ‘€μ„ **기둝 λ‚΄μš©μ—μ„œ λ½‘λŠ”λ‹€**: κ±°κΈ° μ‹€μ œλ‘œ μžˆλŠ” κΈ΄ 토큰은 μ •μ˜μƒ μž‘ν˜€μ•Ό ν•˜λ―€λ‘œ,
90
+ # μ•ˆ 작히면 **νŒŒμ΄ν”„λΌμΈμ΄ 죽은 것**(word-split Β· 경둜 Β· 인코딩)이닀 β€” μ›λž˜ 재렀던 λ°”λ‘œ κ·Έ μ‹€νŒ¨λ‹€.
91
+ # λ‚ μ§œλŠ” Β«μžˆμ„ λ•Œλ§ŒΒ» 두 번째 컨트둀둜 λ”ν•œλ‹€(있으면 더 κ°•ν•˜κ³ , 없어도 치λͺ…적이지 μ•Šλ‹€).
92
+ _control_rows() { # $1=μ˜€λŠ˜λ‚ μ§œ $2=슀크래치 $3...=기둝 νŒŒμΌλ“€
93
+ local _today="$1" _scratch="$2"; shift 2
94
+ local _py="$_scratch/mkcontrol.py"
95
+ cat > "$_py" <<'CTLPY'
96
+ import re, sys
97
+
98
+ today = sys.argv[1]
99
+ best = ''
100
+ seen_today = False
101
+ for path in sys.argv[2:]:
102
+ try:
103
+ txt = open(path, errors='replace').read()
104
+ except OSError:
105
+ continue
106
+ # degrade-scan C2(substring-boolean) λŒ€μ‘: 이 ν¬ν•¨κ²€μ‚¬λŠ” **가산적**이닀 β€” 맞으면 보쑰
107
+ # 컨트둀이 ν•˜λ‚˜ 늘고, 틀리면 μ•ˆ 늘 뿐이닀. ν•˜μ€‘μ€ μ•„λž˜ `best`(νŒŒμƒ 컨트둀)κ°€ μ§„λ‹€.
108
+ # μ–΄λŠ λ°©ν–₯으둜 틀렀도 PASS 둜 κΈ°μšΈμ§€ μ•ŠμœΌλ―€λ‘œ μ •ν™• 일치둜 쒁힐 μ΄μœ κ°€ μ—†λ‹€.
109
+ if today in txt: # noqa: degrade β€” additive only: ADDS a secondary control row, never grounds a verdict
110
+ seen_today = True
111
+ for tok in re.findall(r'[\uac00-\ud7a3]{4,}|[A-Za-z0-9_]{5,}', txt):
112
+ if len(tok) > len(best):
113
+ best = tok
114
+ if best:
115
+ print("CONTROL\t%s\tκΈ°λ‘νŒŒμƒ(μ–‘μ„±μ»¨νŠΈλ‘€)" % best)
116
+ if seen_today:
117
+ print("CONTROL\t%s\tμ˜€λŠ˜λ‚ μ§œ(보쑰컨트둀)" % today)
118
+ CTLPY
119
+ python3 "$_py" "$_today" "$@"
120
+ }
121
+
122
+ # ── ν‚€ 낱말 ν”„λ‘œλΈŒ 생성 ──────────────────────────────────────────────────────
123
+ # ν•œκΈ€/영숫자 토큰 쀑 **κ°€μž₯ κΈ΄** 것 3 개. 쑰사λ₯Ό λ²—κΈ°κ³ , λΆˆμš©μ–΄μ™€ 짧은 토큰을 λΊ€λ‹€.
124
+ # μ •κ·œμ‹ λ©”νƒ€λ¬Έμžκ°€ μ„žμ΄μ§€ μ•Šλ„λ‘ 토큰 λ¬Έμžκ΅°μ„ [\uac00-\ud7a3]Β·[A-Za-z0-9_] 둜 **λ‹«μ•„ λ‘”λ‹€** β€”
125
+ # `.` `-` λ₯Ό ν—ˆμš©ν•˜λ©΄ ERE μ—μ„œ 각각 μ™€μΌλ“œμΉ΄λ“œ/λ²”μœ„λ‘œ μƒˆκ³ , μ΄μŠ€μΌ€μ΄ν”„λ₯Ό μ†μœΌλ‘œ ν•˜λŠ” μˆœκ°„
126
+ # γ€Œκ΄€λŒ€ν•¨ 갈린 쀑볡 μ •κ·œν™”γ€κ°€ μ‹œμž‘λœλ‹€.
127
+ # πŸŸ₯ `python3 - <<'PY'` λ₯Ό μ“°λ©΄ **heredoc 이 python 의 stdin 이 λ˜μ–΄** λ°œν™” tsv κ°€ ν†΅μ§Έλ‘œ
128
+ # μ•ˆ μ½νžŒλ‹€(첫 판본이 그랬고, 레인 L4b κ°€ Β« TARGET 0 건 Β» 으둜 μž‘μ•˜λ‹€). ν”„λ‘œκ·Έλž¨μ€ 파일둜
129
+ # λ–¨μ–΄λœ¨λ¦¬κ³  stdin 은 ν˜ΈμΆœλΆ€μ˜ λ¦¬λ‹€μ΄λ ‰νŠΈκ°€ κ·ΈλŒ€λ‘œ λ‚˜λ₯΄κ²Œ λ‘”λ‹€.
130
+ _build_probes() { # stdin: n<TAB>ts<TAB>text stdout: probes rows $1=map 경둜 $2=슀크래치 디렉터리
131
+ local _py="$2/mkprobes.py"
132
+ cat > "$_py" <<'PY'
133
+ import os, re, sys
134
+
135
+ MAP = open(sys.argv[1], 'w')
136
+
137
+ # πŸŸ₯ 사섀 토큰 λ§ˆμŠ€ν‚Ή β€” **토큰화 전에** ν•œλ‹€. 이 좜λ ₯은 마감 화면에 찍히고 μ‚¬λžŒμ΄ μΉ΄λ“œμ—
138
+ # λΆ™μ—¬λ„£λŠ”λ‹€. 첫 μ‹€μ‚¬μš©(2026-09-05)μ—μ„œ μ‹€μ œλ‘œ `[Image: source: /var/folders/3h/…]` κ°€
139
+ # 라벨둜 λ‚˜μ™”λ‹€ β€” λ¨Έμ‹  고유 μž„μ‹œκ²½λ‘œλ‹€. λ§ˆμŠ€ν‚Ήμ„ λΌλ²¨μ—λ§Œ κ±Έλ©΄ ν‚€ 낱말 μͺ½μœΌλ‘œ μƒˆλ―€λ‘œ
140
+ # (μ‚¬μš©μžλͺ… 같은 것이 κ·ΈλŒ€λ‘œ 토큰이 λœλ‹€) μ•žλ‹¨μ—μ„œ μ§€μš΄λ‹€.
141
+ # πŸŸ₯ ν•œκΈ€ λ²”μœ„λ₯Ό `[\uac00-\ud7a3]` 둜 μ λŠ”λ‹€ β€” `[κ°€-힣]` κ³Ό **μ˜λ―ΈλŠ” κ°™μ§€λ§Œ**
142
+ # (파이썬 `re` μ—μ„œ λ™μΉ˜, known-pair 둜 확인), 이 νŒŒμΌμ€ `.sh` 라
143
+ # `test_card_drift_probe.sh` 의 locale-range λ¦°νŠΈκ°€ μŠ€μΊ”ν•œλ‹€. κ·Έ λ¦°νŠΈλŠ” μ…Έμ˜ `grep` 을
144
+ # 겨λƒ₯ν•œ 것이고(C λ‘œμΌ€μΌμ—μ„œ λΉ„ASCII λ²”μœ„λŠ” exit 2 β†’ «무맀치» 둜 μ½νžŒλ‹€), heredoc μ•ˆμ˜
145
+ # python 인지 κ΅¬λΆ„ν•˜μ§€ λͺ»ν•œλ‹€. 린트λ₯Ό λ„λŠ” λŒ€μ‹  **ν‘œκΈ°λ₯Ό λ°”κΎΌλ‹€** β€” λ‘œμΌ€μΌ 함정에 λŒ€ν•œ
146
+ # κ·Έ κ²½κ³„λŠ” μ‚΄μ•„ μžˆμ–΄μ•Ό ν•œλ‹€.
147
+ # (`transcript_utterances.py` λŠ” `.sh` κ°€ μ•„λ‹ˆλΌ μŠ€μΊ” λŒ€μƒμ΄ μ•„λ‹ˆλ―€λ‘œ 가독성 ν‘œκΈ°λ₯Ό μ“΄λ‹€.)
148
+ _HOME = os.environ.get('HOME', '')
149
+ _TMP_RE = re.compile(r'/(?:var/folders|private/var/folders)/[^\s\]\)]*')
150
+
151
+
152
+ _PATH_RE = re.compile(r'(?<![\w])/(?:[\w.@+-]+/)+[\w.@+-]*') # any absolute path, 2+ segments
153
+ _KV_RE = re.compile(r'\b([A-Z][A-Z0-9_]{2,})=[^\s]+') # KEY=value β†’ KEY=<v>
154
+ _HOST_RE = re.compile(r'\b[a-z0-9-]+(?:\.[a-z0-9-]+)*\.(?:internal|local|corp|lan|intra)\b')
155
+ def mask(t):
156
+ # Labels and probe tokens are an OUTPUT surface (close-check stdout, pasted into cards) β€” so
157
+ # the masking is not "the operator's own paths" but every absolute path, every KEY=value and
158
+ # every host-like name (codex finding 5, 2026-09-05). The suffix list is a small frozen
159
+ # judgment; it fails loud (an unmasked host shows up in a label), not silent.
160
+ t = _TMP_RE.sub('<tmp>', t)
161
+ if _HOME and len(_HOME) > 4:
162
+ t = t.replace(_HOME, '$HOME')
163
+ t = _PATH_RE.sub('<path>', t)
164
+ t = _KV_RE.sub(r'\1=<v>', t)
165
+ t = _HOST_RE.sub('<host>', t)
166
+ return t
167
+
168
+ # 쑰사/μ–΄λ―Έ β€” 토큰 λμ—μ„œ λ²—κΈ΄λ‹€. κΈ΄ 것뢀터 μ‹œλ„ν•œλ‹€(짧은 게 λ¨Όμ € 맞으면 덜 벗겨진닀).
169
+ PARTICLES = ['μœΌλ‘œλŠ”', 'μ—μ„œλŠ”', 'μ—κ²ŒλŠ”', '이라고', 'μœΌλ‘œλ„', 'κΉŒμ§€λŠ”', 'λΆ€ν„°λŠ”',
170
+ '으둜', 'μ—μ„œ', 'μ—κ²Œ', 'ν•œν…Œ', '처럼', '보닀', '라고', 'ν•˜κ³ ', 'κΉŒμ§€',
171
+ 'λΆ€ν„°', 'μ΄λ‚˜', 'λ“ μ§€', 'λ§ˆλ‹€', 'μ‘°μ°¨', '밖에', 'μ΄λž€', '이든',
172
+ '은', 'λŠ”', '이', 'κ°€', '을', 'λ₯Ό', '에', '의', '둜', '도', '만', 'κ³Ό', '와']
173
+
174
+ # λΆˆμš©μ–΄ β€” 이 μ½”νΌμŠ€μ—μ„œ ν”ν•΄μ„œ Β«μš°μ—° 일치» λ₯Ό λ§Œλ“œλŠ” 것듀. OR 결합이라 μ—¬κΈ°κ°€ fail-open 의
175
+ # 주된 μž…κ΅¬λ‹€(Β§λͺ…λͺ…λœ μž”μ—¬ β“‘). 길이 κ·œμΉ™λ§ŒμœΌλ‘œλŠ” μ•ˆ κ±ΈλŸ¬μ§„λ‹€.
176
+ STOP = set('''그리고 κ·ΈλŸ¬λ‹ˆκΉŒ κ·Έλž˜μ„œ 그러면 그런데 μ΄λ ‡κ²Œ κ·Έλ ‡κ²Œ μ–΄λ–»κ²Œ ν•˜μ§€λ§Œ κ·ΈλŸ¬λ‚˜
177
+ λ§ˆμ°¬κ°€μ§€ 이야기 생각 경우 λΆ€λΆ„ λ‚΄μš© 확인 μ§„ν–‰ μž‘μ—… 파일 μ„Έμ…˜ μ§€κΈˆ λ‹€μŒ 이번 정도
178
+ μ‚¬λžŒ 방법 이유 μƒνƒœ κ²°κ³Ό 문제 λŒ€ν•΄ μœ„ν•΄ 톡해 우리 λ„ˆλŠ” λ‚˜λŠ” 것을 것이 κ±°κΈ° μ—¬κΈ° μ €κΈ°
179
+ ν•΄μ€˜ ν•˜μž ν•˜λŠ” λ˜λŠ” μžˆλŠ” μ—†λŠ” 같은 같이 쒋은 λ‹€μ‹œ λ¨Όμ € 아직 이제 쑰금 κ·Έλƒ₯ λ°”λ‘œ
180
+ this that with from have been will your about would should could there which their
181
+ path host tmp home HOME
182
+ then than when what where because those these into over under more most only just'''.split())
183
+
184
+ def strip_particle(tok):
185
+ if not re.match(r'^[\uac00-\ud7a3]+$', tok):
186
+ return tok
187
+ for p in PARTICLES:
188
+ if tok.endswith(p) and len(tok) - len(p) >= 2:
189
+ return tok[:-len(p)]
190
+ return tok
191
+
192
+ n_rows = 0
193
+ n_unprobeable = 0
194
+ for line in sys.stdin:
195
+ line = line.rstrip('\n')
196
+ if not line:
197
+ continue
198
+ parts = line.split('\t', 2)
199
+ if len(parts) < 3:
200
+ continue
201
+ idx, _ts, text = parts
202
+ text = mask(text)
203
+ toks = re.findall(r'[\uac00-\ud7a3]+|[A-Za-z0-9_]+', text)
204
+ cand = []
205
+ seen = set()
206
+ for t in toks:
207
+ s = strip_particle(t)
208
+ if s in seen or s in STOP or s.lower() in STOP:
209
+ continue
210
+ if re.match(r'^[\uac00-\ud7a3]+$', s):
211
+ if len(s) < 3:
212
+ continue
213
+ else:
214
+ if len(s) < 4 or s.isdigit():
215
+ continue
216
+ seen.add(s)
217
+ cand.append(s)
218
+ # κ°€μž₯ κΈ΄ 것뢀터 3 개. 동λ₯ μ€ λ¨Όμ € λ‚˜μ˜¨ 것(λ°œν™” μ•žμͺ½μ΄ λŒ€κ°œ μ£Όμ œμ–΄).
219
+ cand.sort(key=lambda s: -len(s))
220
+ keys = cand[:3]
221
+ if not keys:
222
+ # ν”„λ‘œλΈŒλ₯Ό λ§Œλ“€ 수 μ—†λŠ” λ°œν™” β€” **버리지 μ•Šκ³  μ„Όλ‹€**(not found != 0).
223
+ n_unprobeable += 1
224
+ MAP.write("%s\tUNPROBEABLE\t%s\t0\n" % (idx, text[:60]))
225
+ continue
226
+ label = "#%s %s" % (idx, text[:40])
227
+ # One TARGET row per key: `#<idx>.<k>/<n> <text>` β€” the wrapper re-aggregates per utterance
228
+ # (codex finding 1). Keys are bare tokens ([κ°€-힣]+ / [A-Za-z0-9_]+): no ERE metacharacters,
229
+ # never dash-first, so the landing checker reads them as literals.
230
+ for k_i, key in enumerate(keys, 1):
231
+ print("TARGET\t%s\t#%s.%d/%d %s" % (key, idx, k_i, len(keys), text[:36]))
232
+ MAP.write("%s\t%s\t%s\t%d\n" % (idx, label, text[:60], len(keys)))
233
+ n_rows += 1
234
+ MAP.close()
235
+ sys.stderr.write("probe utterances=%d Β· ν”„λ‘œλΈŒλΆˆκ°€=%d\n" % (n_rows, n_unprobeable))
236
+ PY
237
+ python3 "$_py" "$1"
238
+ }
239
+
240
+ # ── 인자 ────────────────────────────────────────────────────────────────────
241
+ if [ "$#" -lt 2 ]; then
242
+ echo "usage: $0 <transcript.jsonl> <record-file> [<record-file>...]" >&2
243
+ exit 10
244
+ fi
245
+ TRANSCRIPT="$1"; shift
246
+ RECORDS=() # λ°°μ—΄ β€” zsh word-split 에 μ˜μ‘΄ν•˜μ§€ μ•ŠλŠ”λ‹€
247
+ MISSING_RECORDS=0
248
+ for f in "$@"; do
249
+ if [ -f "$f" ]; then RECORDS+=("$f"); else MISSING_RECORDS=$((MISSING_RECORDS+1)); fi
250
+ done
251
+
252
+ [ -f "$EXTRACTOR" ] || _die10 "πŸŸ₯ HARNESS-ERROR: μΆ”μΆœκΈ° μ—†μŒ: $EXTRACTOR"
253
+ [ -f "$LANDING" ] || _die10 "πŸŸ₯ HARNESS-ERROR: μ°©μ§€ 검사기 μ—†μŒ: $LANDING"
254
+
255
+ if [ ! -f "$TRANSCRIPT" ]; then
256
+ echo "ℹ️ UNMEASURED β€” 전사본 λΆ€μž¬: $TRANSCRIPT"
257
+ # πŸŸ₯ 성곡 νŒμ • λ¦¬ν„°λŸ΄(Β«μ „λΆ€ μ°©μ§€Β»)을 **λΆ€μ •λ¬Έ μ•ˆμ—λ„** μ“°μ§€ 마라 β€” grep ν•˜λŠ” μͺ½μ΄
258
+ # μ„±κ³΅μœΌλ‘œ μ½λŠ”λ‹€(Grep-Collision Treadmill). 레인 L7c κ°€ μ‹€μ œλ‘œ κ·Έλ ‡κ²Œ μ˜€λ…ν–ˆλ‹€.
259
+ echo " λΆ€μž¬λ₯Ό 0 으둜 μ ‘μ§€ 마라 β€” λ°œν™”κ°€ μ—†μ—ˆλ‹€λŠ” λœ»λ„, λ‹€ μ ν˜”λ‹€λŠ” λœ»λ„ μ•„λ‹ˆλ‹€."
260
+ exit 10
261
+ fi
262
+ if [ "${#RECORDS[@]}" -eq 0 ]; then
263
+ echo "ℹ️ UNMEASURED β€” 검사할 기둝 파일이 ν•˜λ‚˜λ„ μ—†λ‹€ (인자 $# 건 쀑 $MISSING_RECORDS 건 λΆ€μž¬)"
264
+ echo " 기둝이 μ—†λŠ” 것과 λ°œν™”κ°€ μ•ˆ 적힌 것은 λ‹€λ₯Έ λͺ…μ œλ‹€."
265
+ exit 10
266
+ fi
267
+
268
+ WORK="$(mktemp -d 2>/dev/null)" || _die10 "πŸŸ₯ HARNESS-ERROR: mktemp -d μ‹€νŒ¨ (ν™˜κ²½ λΆ€μž¬μ΄μ§€ 계기 결함이 μ•„λ‹ˆλ‹€)"
269
+ trap 'rm -rf "$WORK"' EXIT INT TERM
270
+
271
+ # ── 1. λ°œν™” μΆ”μΆœ ────────────────────────────────────────────────────────────
272
+ # `--strip-system-markers`: peer μ•Œλ¦Ό Β· 무인 μ‹€ν–‰ Β· 이미지 λ§ˆμ»€λŠ” **운영자 λ°œν™”κ°€ μ•„λ‹ˆλ‹€**.
273
+ # seal 은 이 ν”Œλž˜κ·Έλ₯Ό μ•ˆ μ£Όλ―€λ‘œ κΈ°μ‘΄ 원μž₯ 좜λ ₯은 κ·ΈλŒ€λ‘œλ‹€(레인 L10 골든이 κ·Έκ±Έ κ³ μ •ν•œλ‹€).
274
+ python3 "$EXTRACTOR" "$TRANSCRIPT" --min-chars "$MIN_CHARS" --strip-system-markers \
275
+ > "$WORK/utts.tsv" 2> "$WORK/extract.err"
276
+ _erc=$?
277
+ if [ "$_erc" -ne 0 ]; then
278
+ echo "πŸŸ₯ HARNESS-ERROR: λ°œν™” μΆ”μΆœ μ‹€νŒ¨ (rc=$_erc)"; sed 's/^/ /' "$WORK/extract.err"; exit 10
279
+ fi
280
+ N_UTT=$(_num "$(grep -c . "$WORK/utts.tsv" 2>/dev/null)")
281
+ if [ "${N_UTT:-0}" -eq 0 ]; then
282
+ echo "ℹ️ UNMEASURED β€” μΆ”μΆœλœ λ°œν™” 0 건 (min-chars=$MIN_CHARS)"
283
+ sed 's/^/ /' "$WORK/extract.err"
284
+ echo " 빈 ν”„λ‘œλΈŒ μ„ΈνŠΈλŠ” 합격이 μ•„λ‹ˆλ‹€ β€” «아무것도 μ•ˆ μŸ€λ‹€Β» λ‹€."
285
+ exit 10
286
+ fi
287
+
288
+ # ── 2. ν”„λ‘œλΈŒ 생성 (컨트둀 2 μ’… λ¨Όμ €) ───────────────────────────────────────
289
+ TODAY="$(date +%Y-%m-%d)"
290
+ NEG_TOKEN="${FH_INTAKE_NEG_TOKEN:-}"
291
+ if [ -z "$NEG_TOKEN" ]; then
292
+ # λ¬΄μž‘μœ„. `date +%N` 은 BSD 에 μ—†μœΌλ―€λ‘œ μ“°μ§€ μ•ŠλŠ”λ‹€.
293
+ NEG_TOKEN="FHNEG$(od -An -N6 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' | tr 'a-f' 'A-F')"
294
+ case "$NEG_TOKEN" in FHNEG) NEG_TOKEN="FHNEG$$X$(date +%s)" ;; esac
295
+ fi
296
+ {
297
+ _control_rows "$TODAY" "$WORK" "${RECORDS[@]}"
298
+ printf 'NEGATIVE\t%s\tλ¬΄μž‘μœ„ν† ν°(μŒμ„±μ»¨νŠΈλ‘€)\n' "$NEG_TOKEN"
299
+ } > "$WORK/probes.tsv"
300
+ _build_probes "$WORK/probes.map" "$WORK" < "$WORK/utts.tsv" >> "$WORK/probes.tsv" 2> "$WORK/build.err"
301
+ [ -n "${FH_INTAKE_DUMP_PROBES:-}" ] && cp "$WORK/probes.tsv" "$FH_INTAKE_DUMP_PROBES" 2>/dev/null
302
+ N_TARGET=$(_num "$(grep -c "^TARGET$(printf '\t')" "$WORK/probes.tsv" 2>/dev/null)")
303
+ N_PROBED=$(_num "$(LC_ALL=C awk -F'\t' '$2!="UNPROBEABLE"' "$WORK/probes.map" 2>/dev/null | grep -c .)")
304
+ UNPROBE=$(_num "$(LC_ALL=C awk -F'\t' '$2=="UNPROBEABLE"' "$WORK/probes.map" 2>/dev/null | grep -c .)")
305
+ if [ "${N_TARGET:-0}" -eq 0 ]; then
306
+ echo "ℹ️ UNMEASURED β€” λ°œν™” $N_UTT κ±΄μ—μ„œ ν”„λ‘œλΈŒλ₯Ό ν•˜λ‚˜λ„ λͺ» λ§Œλ“€μ—ˆλ‹€"
307
+ sed 's/^/ /' "$WORK/build.err"
308
+ exit 10
309
+ fi
310
+ # μ–‘μ„± 컨트둀이 ν•˜λ‚˜λ„ μ•ˆ λ‚˜μ™”μœΌλ©΄(기둝이 μ§§μ•„ 4자+ ν•œκΈ€ / 5자+ 영숫자 토큰이 μ—†λ‹€) 계기λ₯Ό λŒλ¦¬μ§€
311
+ # μ•ŠλŠ”λ‹€ β€” μ°©μ§€ 검사기도 CONTROL λΆ€μž¬μ— rc=10 을 λ‚΄μ§€λ§Œ, κ·Έ λ©”μ‹œμ§€λŠ” «계기 사망» 이고 μ—¬κΈ° 원인은
312
+ # Β«μ»¨νŠΈλ‘€μ„ λ§Œλ“€ μž¬λ£Œκ°€ μ—†λ‹€Β» 라 λ‹€λ₯΄λ‹€(codex finding 7). λ‘˜ λ‹€ UNMEASURED λ‹€.
313
+ N_CTL=$(_num "$(grep -c "^CONTROL$(printf '\t')" "$WORK/probes.tsv" 2>/dev/null)")
314
+ if [ "${N_CTL:-0}" -eq 0 ]; then
315
+ echo "ℹ️ UNMEASURED β€” κΈ°λ‘μ—μ„œ μ–‘μ„± 컨트둀 토큰을 λͺ» λ½‘μ•˜λ‹€(4자+ ν•œκΈ€ Β· 5자+ 영숫자 토큰 λΆ€μž¬, 였늘 λ‚ μ§œλ„ μ—†μŒ)"
316
+ echo " 컨트둀 μ—†λŠ” μ°©μ§€ νŒμ •μ€ κ·Όκ±°κ°€ μ•„λ‹ˆλ‹€ β€” 기둝이 λ„ˆλ¬΄ μ§§λ‹€."
317
+ exit 10
318
+ fi
319
+
320
+ # ── 3. μŒμ„± 컨트둀 β€” ν•˜μœ„ 도ꡬ가 λͺ¨λ₯΄λŠ” μ’…λ₯˜λΌ μ—¬κΈ°μ„œ κ²€μ‚¬ν•œλ‹€ ─────────────
321
+ # `grep -F` : 토큰을 λ¦¬ν„°λŸ΄λ‘œ λ³Έλ‹€. μ •κ·œμ‹μœΌλ‘œ ν•΄μ„λ˜λ©΄ Β«μ—†μ–΄μ•Ό ν•  것» 의 μ˜λ―Έκ°€ 바뀐닀.
322
+ if grep -qF -- "$NEG_TOKEN" "${RECORDS[@]}" 2>/dev/null; then
323
+ echo "πŸŸ₯ HARNESS-ERROR: μŒμ„± 컨트둀이 κΈ°λ‘μ—μ„œ μž‘ν˜”λ‹€ β€” [$NEG_TOKEN]"
324
+ echo " μ—†μ–΄μ•Ό λ§ˆλ•…ν•œ 토큰이 μž‘νžŒλ‹€ = 계기가 μ•„λ¬΄κ±°λ‚˜ 작고 μžˆλ‹€. 타깃 κ²°κ³ΌλŠ” μΈμ‡„ν•˜μ§€ μ•ŠλŠ”λ‹€."
325
+ exit 10
326
+ fi
327
+
328
+ # ── 4. μ°©μ§€ 검증 (μ–‘μ„± μ»¨νŠΈλ‘€μ€ ν•˜μœ„ 도ꡬ가 λ³Έλ‹€) ───────────────────────────
329
+ LAND_OUT="$WORK/land.txt"
330
+ bash "$LANDING" "$WORK/probes.tsv" "${RECORDS[@]}" > "$LAND_OUT" 2>&1
331
+ LRC=$?
332
+
333
+ # πŸŸ₯ νŒμ •μ€ **μ’…λ£Œμ½”λ“œ**둜 λ‚Έλ‹€. μ•„λž˜ νŒŒμ‹±μ€ Β«μ‚¬λžŒμ΄ 읽을 λͺ©λ‘Β» 을 λ§Œλ“€ 뿐이고, λ‘˜μ΄ μ–΄κΈ‹λ‚˜λ©΄
334
+ # κ·Έ μžμ²΄κ°€ μ‹ ν˜Έλ‹€(`[[feedback_summary_parsing_hides_failure]]` Β· 두 μ‹ ν˜Έμ˜ 어긋남).
335
+ ROW_MISS=$(_num "$(grep -c 'λ―Έμ°©μ§€$' "$LAND_OUT" 2>/dev/null)")
336
+ # λ°œν™” λ‹¨μœ„ 집계 β€” ν–‰(토큰)의 μ°©μ§€/λ―Έμ°©μ§€λ₯Ό `#idx.k/n` 라벨둜 λ°œν™”μ— 되돌리고, ⌈n/2βŒ‰ 이상 μ°©μ§€λ©΄
337
+ # κ·Έ λ°œν™”λŠ” μ°©μ§€λ‹€. 좜λ ₯: LANDED / UNLANDED 개수 + λ―Έμ°©μ§€ λ°œν™” idx λͺ©λ‘.
338
+ _AGG="$WORK/agg.py"
339
+ cat > "$_AGG" <<'AGGPY'
340
+ import re, sys
341
+ land, mapf = sys.argv[1], sys.argv[2]
342
+ n_keys = {}
343
+ for line in open(mapf, errors='replace'):
344
+ parts = line.rstrip('\n').split('\t')
345
+ if len(parts) >= 4 and parts[1] != 'UNPROBEABLE':
346
+ n_keys[parts[0]] = int(parts[3])
347
+ hits = {}
348
+ row_re = re.compile(r'^\s*(βœ…|πŸŸ₯)\s+#(\d+)\.(\d+)/(\d+)\s')
349
+ for line in open(land, errors='replace'):
350
+ m = row_re.match(line)
351
+ if not m:
352
+ continue
353
+ idx = m.group(2)
354
+ n_keys.setdefault(idx, int(m.group(4)))
355
+ if m.group(1) == 'βœ…':
356
+ hits[idx] = hits.get(idx, 0) + 1
357
+ landed, unlanded = [], []
358
+ for idx in sorted(n_keys, key=int):
359
+ n = n_keys[idx]
360
+ need = (n + 1) // 2
361
+ (landed if hits.get(idx, 0) >= need else unlanded).append(idx)
362
+ print("LANDED=%d" % len(landed))
363
+ print("UNLANDED=%d" % len(unlanded))
364
+ print("UNLANDED_IDX=%s" % " ".join(unlanded))
365
+ AGGPY
366
+ _AGG_OUT="$(python3 "$_AGG" "$LAND_OUT" "$WORK/probes.map" 2>/dev/null)"
367
+ HIT_N=$(_num "$(printf '%s\n' "$_AGG_OUT" | sed -n 's/^LANDED=//p')")
368
+ MISS_N=$(_num "$(printf '%s\n' "$_AGG_OUT" | sed -n 's/^UNLANDED=//p')")
369
+ MISS_IDX="$(printf '%s\n' "$_AGG_OUT" | sed -n 's/^UNLANDED_IDX=//p')"
370
+
371
+ echo "── λ°œν™” μ°©μ§€ 검사 ──"
372
+ echo " 전사본 : $TRANSCRIPT"
373
+ echo " 기둝 λŒ€μƒ: ${#RECORDS[@]} 파일$([ "$MISSING_RECORDS" -gt 0 ] && printf ' (λΆ€μž¬ %s 건 μ œμ™Έ)' "$MISSING_RECORDS")"
374
+ echo " μΆ”μΆœ : λ°œν™” $N_UTT 건 (min-chars=$MIN_CHARS) β†’ ν”„λ‘œλΈŒ λ°œν™” $N_PROBED 건 (토큰 ν–‰ $N_TARGET) Β· ν”„λ‘œλΈŒλΆˆκ°€ $UNPROBE 건"
375
+ sed 's/^/ /' "$WORK/build.err" 2>/dev/null
376
+ sed 's/^/ /' "$WORK/extract.err" 2>/dev/null
377
+
378
+ case "$LRC" in
379
+ 10)
380
+ echo
381
+ sed 's/^/ /' "$LAND_OUT"
382
+ echo " πŸŸ₯ 계기 사망 β€” μ°©μ§€ νŒμ •μ„ λ‚΄μ§€ μ•ŠλŠ”λ‹€(죽은 κ³„κΈ°μ˜ 좜λ ₯은 데이터가 μ•„λ‹ˆλ‹€)."
383
+ exit 10 ;;
384
+ esac
385
+
386
+ if [ "$LRC" -eq 0 ] && [ "$ROW_MISS" -ne 0 ]; then
387
+ echo "πŸŸ₯ HARNESS-ERROR: rc=0 인데 λ―Έμ°©μ§€ 행이 $ROW_MISS 개 β€” 두 μ‹ ν˜Έκ°€ μ–΄κΈ‹λ‚œλ‹€. 원문:"
388
+ sed 's/^/ /' "$LAND_OUT"; exit 10
389
+ fi
390
+ if [ "$LRC" -eq 1 ] && [ "$ROW_MISS" -eq 0 ]; then
391
+ echo "πŸŸ₯ HARNESS-ERROR: rc=1 인데 λ―Έμ°©μ§€ 행이 0 개 β€” 두 μ‹ ν˜Έκ°€ μ–΄κΈ‹λ‚œλ‹€. 원문:"
392
+ sed 's/^/ /' "$LAND_OUT"; exit 10
393
+ fi
394
+ if [ $((HIT_N + MISS_N)) -ne "$N_PROBED" ]; then
395
+ echo "πŸŸ₯ HARNESS-ERROR: λ°œν™” 집계 $HIT_N+$MISS_N β‰  ν”„λ‘œλΈŒ λ°œν™” $N_PROBED β€” 집계기가 행을 μžƒμ—ˆλ‹€. 원문:"
396
+ sed 's/^/ /' "$LAND_OUT"; exit 10
397
+ fi
398
+
399
+ echo " μ°©μ§€ $HIT_N Β· λ―Έμ°©μ§€ $MISS_N Β· ν”„λ‘œλΈŒλΆˆκ°€ $UNPROBE (ν–‰: μ°©μ§€ $((N_TARGET - ROW_MISS)) Β· λ―Έμ°©μ§€ $ROW_MISS Β· νŒμ • = λ°œν™” 토큰 ⌈n/2βŒ‰ 이상)"
400
+ if [ "$MISS_N" -gt 0 ]; then
401
+ echo
402
+ echo " λ―Έμ°©μ§€ λͺ©λ‘ (λ°œν™” μ•ž 60자):"
403
+ # 라벨의 `#<n>` 으둜 map 을 λ˜μ§šλŠ”λ‹€. 숫자 비ꡐ라 λΉ„ASCII 동등성 함정에 μ•ˆ κ±Έλ¦°λ‹€
404
+ # (`[[feedback_locale_string_equality_breaks_nonascii]]`).
405
+ for _k in $MISS_IDX; do
406
+ LC_ALL=C awk -F'\t' -v k="$_k" '$1==k{printf " Β· %s\n", $3}' "$WORK/probes.map"
407
+ done
408
+ fi
409
+ # ν”„λ‘œλΈŒλΆˆκ°€ λ°œν™”λŠ” Β«λͺ» μŸ€λ‹€Β» μ§€ Β«μ°©μ§€Β» κ°€ μ•„λ‹ˆλ‹€ β€” 미착지와 λ³„κ°œλ‘œ μ„Έλ˜ rc μ—λŠ” 같이 λ“ λ‹€
410
+ # (codex finding 2: MISS_N=0 이면 쑰용히 rc=0 이던 것을 λ‹«μŒ). μ‚¬λžŒμ΄ 확인할 λͺ©λ‘μœΌλ‘œ λ‚Έλ‹€.
411
+ if [ "${UNPROBE:-0}" -gt 0 ]; then
412
+ echo
413
+ echo " ν”„λ‘œλΈŒλΆˆκ°€ $UNPROBE 건 (ν‚€ 낱말을 λͺ» 뽑은 λ°œν™” β€” λ―ΈμΈ‘μ •, μ°©μ§€ μ•„λ‹˜. 눈으둜 확인):"
414
+ LC_ALL=C awk -F'\t' '$2=="UNPROBEABLE"{printf " Β· %s\n", $3}' "$WORK/probes.map"
415
+ fi
416
+ if [ "$MISS_N" -gt 0 ] || [ "${UNPROBE:-0}" -gt 0 ]; then
417
+ echo
418
+ echo " ⚠️ rc=1 은 Β«κΈ°λ‘ν•˜λΌΒ» κ°€ μ•„λ‹ˆλΌ Β«ν™•μΈν•˜λΌΒ» λ‹€ β€” ν”„λ‘œλΈŒ λ…Έν›„κ°€ 첫 번째 μ˜μ‹¬ λŒ€μƒμ΄λ‹€."
419
+ echo " κ°€μž₯ 잘 λΉ μ§€λŠ” 것은 μžŠμ€ λ°œν™”κ°€ μ•„λ‹ˆλΌ **ν–‰λ™μœΌλ‘œ λŒ€μ‘ν•œ λ°œν™”**λ‹€."
420
+ exit 1
421
+ fi
422
+ echo " βœ… ν”„λ‘œλΈŒ λ°œν™” $N_PROBED 건 μ „λΆ€ μ°©μ§€ (λ°œν™”λ³„ 졜μž₯ 토큰 ≀3 쀑 ⌈n/2βŒ‰ 이상 일치)"
423
+ echo " ⚠️ ν‚€ 2 개 μ΄ν•˜ λ°œν™”λŠ” ν•˜λ‚˜λ§Œ 겹쳐도 μ°©μ§€λ‹€ β€” Β«μ œλŒ€λ‘œ μ ν˜”λ‹€Β» 의 증λͺ…이 μ•„λ‹ˆλ‹€."
424
+ exit 0
@@ -132,7 +132,7 @@ done
132
132
  _probe_hits() {
133
133
  local pat="$1"; shift
134
134
  grep -lE -- "$pat" "$@" 2>/dev/null
135
- return "${PIPESTATUS[0]:-$?}"
135
+ return "${PIPESTATUS[0]:-$?}" # portability-noqa: shebang (line 1) pins bash β€” PIPESTATUS is a real array there, unlike zsh
136
136
  }
137
137
 
138
138
  # ── 1단계: 컨트둀. μ‚΄μ•„μžˆμŒμ„ 증λͺ…ν•˜κΈ° μ „μ—λŠ” 타깃을 μΈμ‡„ν•˜μ§€ μ•ŠλŠ”λ‹€ ──
@@ -179,7 +179,7 @@ MISS=0; N=0
179
179
  while IFS=$'\t' read -r kind pat label; do
180
180
  [ "${kind:-}" = "TARGET" ] || continue
181
181
  N=$((N+1))
182
- hits=$(_probe_hits "$pat" "${FILES[@]}" | wc -l | tr -d ' '); _rc=${PIPESTATUS[0]}
182
+ hits=$(_probe_hits "$pat" "${FILES[@]}" | wc -l | tr -d ' '); _rc=${PIPESTATUS[0]} # portability-noqa: shebang (line 1) pins bash β€” PIPESTATUS is a real array there, unlike zsh
183
183
  if [ "$_rc" -ge 2 ]; then
184
184
  printf " πŸŸ₯ %-46s ν”„λ‘œλΈŒ 였λ₯˜\n" "${label:-$pat}"
185
185
  BAD_PAT="${BAD_PAT:+$BAD_PAT, }TARGET/${label:-$pat}"
@@ -133,6 +133,33 @@ if [ "$_skills" -eq 0 ] || [ "$_agents" -eq 0 ]; then
133
133
  fi
134
134
  echo " (scanned: $_skills skill(s), $_agents agent(s))"
135
135
 
136
+ # ── Whole-file YAML documents that machinery READS (not frontmatter) ─────────────────────────
137
+ # The dispatch ledger is parsed by session_close_check β‘£-e / activity_log / round/gatecheck_qset and
138
+ # by the 60/40 promotion gate. Measured 2026-09-04: PR #611 merged an entry whose unquoted scalar
139
+ # carried `: ` (`… API error: 529Β»`) and the file had been UNPARSEABLE at HEAD since β€” while its own
140
+ # marker cited "validate_yaml 레인" as the live control. That lane never read this file: it checks
141
+ # SKILL/agent FRONTMATTER only. This block closes that gap for the files listed here. Known pair at
142
+ # wiring time: the #611 HEAD ledger β†’ ❌ (line 3090), the quoted fix β†’ βœ….
143
+ _LEDGERS="knowledge/shared/learnings/subagent_invocations_log.yaml"
144
+ for f in $_LEDGERS; do
145
+ [ -f "$f" ] || { echo " ❌ ledger missing: $f (not found β‰  empty)"; ERRORS=$((ERRORS + 1)); continue; }
146
+ if [ -z "$PARSER" ]; then
147
+ echo " ⚠️ $f: UNCALIBRATED β€” no YAML parser, NOT verified"; UNCALIBRATED=$((UNCALIBRATED + 1)); continue
148
+ fi
149
+ out=$(python3 - "$f" <<'PY' 2>&1
150
+ import sys, yaml
151
+ try:
152
+ d = yaml.safe_load(open(sys.argv[1], encoding='utf-8'))
153
+ except yaml.YAMLError as e:
154
+ print('PARSE ' + str(e).replace('\n', ' ')[:160]); sys.exit(1)
155
+ if not isinstance(d, list) or not d:
156
+ print('NOTLIST-OR-EMPTY'); sys.exit(2)
157
+ print('OK %d entries' % len(d))
158
+ PY
159
+ ); rc=$?
160
+ if [ "$rc" -eq 0 ]; then echo " βœ… $f: $out"; else echo " ❌ $f: $out"; ERRORS=$((ERRORS + 1)); fi
161
+ done
162
+
136
163
  echo ""
137
164
  if [ "$UNCALIBRATED" -gt 0 ]; then
138
165
  echo " ⚠️ UNCALIBRATED β€” $UNCALIBRATED skill(s) unverified (no parser). Not a pass."
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ # worktree_reclaim.sh β€” reclaim gitignored `tracks/**` artifacts from a git worktree BEFORE it is removed.
3
+ #
4
+ # WHY (N=2, so a script β€” 2026-09-02 signal + 2026-09-05 push-zone dispatch): the Destructive-Op gate
5
+ # enumerates COMMITS, and `tracks/**` is gitignored, so a worktree removal can delete a session's only
6
+ # copy of a signal file / governance log / dispatch report without any gate ever seeing it. Both
7
+ # incidents lost real files (51-line signal Β· a day's governance log). The manual recipe that replaced
8
+ # them β€” `diff -rq <wt>/tracks tracks | grep "^Only in <wt>"` β†’ list to a FILE β†’ copy β†’ only then
9
+ # remove β€” is exactly what this script does, in that order, because after the copy Β«never existedΒ»
10
+ # and Β«already movedΒ» are indistinguishable (the list file is the evidence).
11
+ #
12
+ # USAGE
13
+ # bash scripts/worktree_reclaim.sh <worktree-path> # enumerate only (writes the list file)
14
+ # bash scripts/worktree_reclaim.sh <worktree-path> --apply # enumerate + copy + verify
15
+ #
16
+ # EXIT 0 = nothing to reclaim, or everything reclaimed and byte-verified
17
+ # 1 = files exist on BOTH sides with different content β€” a human must merge (listed, not copied)
18
+ # 2 = usage / not a worktree of a main checkout
19
+ # 10 = harness error (list file unwritable, copy failed verification)
20
+ #
21
+ # NEVER removes the worktree. Removal stays an explicit `git worktree remove` after this exits 0.
22
+ set -uo pipefail
23
+ # bash 3.2 + `set -u`: expanding an EMPTY array with "${a[@]}" is a fatal unbound-variable error, so every
24
+ # array loop below uses the ${a[@]+"${a[@]}"} idiom (measured on the first fixture run, 2026-09-05).
25
+
26
+ WT="${1:-}"; MODE="${2:-}"
27
+ [ -n "$WT" ] || { echo "usage: $0 <worktree-path> [--apply]" >&2; exit 2; }
28
+ [ -d "$WT" ] || { echo "πŸŸ₯ not a directory: $WT" >&2; exit 2; }
29
+ case "$MODE" in ''|--apply) ;; *) echo "usage: $0 <worktree-path> [--apply]" >&2; exit 2 ;; esac
30
+
31
+ COMMON="$(git -C "$WT" rev-parse --git-common-dir 2>/dev/null)" || { echo "πŸŸ₯ not a git worktree: $WT" >&2; exit 2; }
32
+ GITDIR="$(git -C "$WT" rev-parse --git-dir 2>/dev/null)"
33
+ case "$COMMON" in /*) ;; *) COMMON="$WT/$COMMON" ;; esac
34
+ case "$GITDIR" in /*) ;; *) GITDIR="$WT/$GITDIR" ;; esac
35
+ COMMON="$(cd "$COMMON" && pwd -P)"; GITDIR="$(cd "$GITDIR" && pwd -P)"
36
+ if [ "$COMMON" = "$GITDIR" ]; then
37
+ echo "πŸŸ₯ $WT is the MAIN checkout (git-dir == git-common-dir), not a worktree β€” nothing to reclaim from itself" >&2; exit 2
38
+ fi
39
+ ROOT="$(dirname "$COMMON")" # main checkout root = parent of the shared .git
40
+ WT="$(cd "$WT" && pwd -P)"
41
+ SRC="$WT/tracks"; DST="$ROOT/tracks"
42
+ [ -d "$SRC" ] || { echo "ℹ️ no tracks/ in worktree β€” nothing to reclaim (rc=0)"; exit 0; }
43
+ mkdir -p "$DST/_meta/dispatch" 2>/dev/null || { echo "πŸŸ₯ cannot create $DST/_meta/dispatch" >&2; exit 10; }
44
+
45
+ NAME="$(basename "$WT")"; TS="$(date +%Y%m%d-%H%M%S)"
46
+ LIST="$DST/_meta/dispatch/reclaim_${NAME}_${TS}.txt"
47
+
48
+ # 1. ENUMERATE β€” the list lands in a file FIRST (the evidence that survives the copy)
49
+ ONLY=(); DIFF=()
50
+ while IFS= read -r line; do
51
+ case "$line" in
52
+ "Only in $SRC"*)
53
+ d="${line#Only in }"; dir="${d%%: *}"; f="${d#*: }"
54
+ rel="${dir#$SRC}"; rel="${rel#/}"
55
+ p="${rel:+$rel/}$f"
56
+ if [ -d "$SRC/$p" ]; then
57
+ while IFS= read -r ff; do ONLY+=("${ff#$SRC/}"); done < <(find "$SRC/$p" -type f)
58
+ else
59
+ ONLY+=("$p")
60
+ fi ;;
61
+ "Files $SRC"*" differ") DIFF+=("$(printf '%s' "${line#Files }" | sed "s# and .*##; s#^$SRC/##")") ;;
62
+ esac
63
+ done < <(diff -rq "$SRC" "$DST" 2>/dev/null)
64
+
65
+ {
66
+ echo "# worktree_reclaim β€” $WT β†’ $ROOT ($TS)"
67
+ echo "# only-in-worktree: ${#ONLY[@]} differ-both-sides: ${#DIFF[@]} mode: ${MODE:-enumerate}"
68
+ for p in ${ONLY[@]+"${ONLY[@]}"}; do echo "ONLY $p"; done
69
+ for p in ${DIFF[@]+"${DIFF[@]}"}; do echo "DIFF $p"; done
70
+ } > "$LIST" || { echo "πŸŸ₯ cannot write list file $LIST" >&2; exit 10; }
71
+ echo "── worktree_reclaim: $NAME ──"
72
+ echo " only-in-worktree: ${#ONLY[@]} Β· differ-both-sides: ${#DIFF[@]} Β· list: ${LIST#$ROOT/}"
73
+ for p in ${ONLY[@]+"${ONLY[@]}"}; do echo " ONLY $p"; done
74
+ for p in ${DIFF[@]+"${DIFF[@]}"}; do echo " DIFF $p (both sides exist and differ β€” NOT copied, merge by hand)"; done
75
+
76
+ # 2. RECLAIM (--apply) β€” copy, then verify byte-identical; a copy that cannot be verified is a harness error
77
+ if [ "$MODE" = "--apply" ] && [ "${#ONLY[@]}" -gt 0 ]; then
78
+ bad=0
79
+ for p in ${ONLY[@]+"${ONLY[@]}"}; do
80
+ mkdir -p "$(dirname "$DST/$p")" && cp -p "$SRC/$p" "$DST/$p" && cmp -s "$SRC/$p" "$DST/$p" \
81
+ && echo " βœ… reclaimed $p" || { echo " πŸŸ₯ copy/verify FAILED $p"; bad=$((bad+1)); }
82
+ done
83
+ [ "$bad" -eq 0 ] || { echo "πŸŸ₯ $bad file(s) not reclaimed β€” do NOT remove the worktree" >&2; exit 10; }
84
+ fi
85
+
86
+ if [ "${#DIFF[@]}" -gt 0 ]; then
87
+ echo "⚠️ ${#DIFF[@]} file(s) differ on both sides β€” reconcile by hand before removing the worktree (rc=1)"
88
+ exit 1
89
+ fi
90
+ if [ "${#ONLY[@]}" -gt 0 ] && [ "$MODE" != "--apply" ]; then
91
+ echo "ℹ️ ${#ONLY[@]} file(s) exist only in the worktree β€” re-run with --apply to copy them (rc=1 until reclaimed)"
92
+ exit 1
93
+ fi
94
+ echo "βœ… nothing left only in the worktree β€” safe to: git worktree remove $WT"
95
+ exit 0