@chrono-meta/fh-gate 2.5.1 โ†’ 2.7.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 (57) hide show
  1. package/.claude/rules/fh_4axis_gate.md +26 -3
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/AGENTS.md +24 -3
  4. package/CATALOG.md +41 -0
  5. package/CLAUDE.md +77 -15
  6. package/README.ja.md +36 -11
  7. package/README.ko.md +36 -11
  8. package/README.md +116 -18
  9. package/README.zh.md +32 -11
  10. package/docs/USER_GUIDE.md +118 -0
  11. package/docs/platform_sustainability.md +174 -0
  12. package/knowledge/shared/GLOSSARY.md +26 -1
  13. package/knowledge/shared/harness-core/fh_detail_protocols.md +44 -2
  14. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +19 -5
  15. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +129 -2
  16. package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
  17. package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
  18. package/knowledge/shared/learnings/subagent_invocations_log.yaml +110 -0
  19. package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
  20. package/package.json +17 -3
  21. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  22. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  23. package/plugins/fh-meta/CHANGELOG.md +136 -0
  24. package/plugins/fh-meta/agents/persona-innovator.md +170 -0
  25. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +44 -0
  26. package/plugins/fh-meta/skills/fh/SKILL.md +31 -0
  27. package/plugins/fh-meta/skills/goal-quench/SKILL_detail.md +1 -1
  28. package/plugins/fh-meta/skills/harness-doctor/SKILL.md +7 -1
  29. package/plugins/fh-meta/skills/harvest-loop/SKILL.md +32 -0
  30. package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
  31. package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
  32. package/scripts/adapters/fixtures/mate_agent_boundary_known_negative.md +31 -0
  33. package/scripts/adapters/fixtures/mate_agent_boundary_known_positive.md +56 -0
  34. package/scripts/adapters/peer_resolve.sh +58 -6
  35. package/scripts/cluster_capability_scan.sh +60 -18
  36. package/scripts/digest_landing_check.sh +181 -10
  37. package/scripts/fh_hub_identity.sh +83 -0
  38. package/scripts/fh_session_load.sh +53 -5
  39. package/scripts/fh_track_resolve.sh +114 -0
  40. package/scripts/field_canon_preload.sh +50 -5
  41. package/scripts/package_coverage_check.sh +17 -0
  42. package/scripts/prior_art_prompt.sh +168 -0
  43. package/scripts/psa_scan_lib.sh +201 -15
  44. package/scripts/residency_admission_check.sh +204 -0
  45. package/scripts/selfcheck.sh +117 -1
  46. package/scripts/test_adapter_lanes.sh +67 -2
  47. package/scripts/test_heavy_classifier_lanes.sh +144 -0
  48. package/scripts/test_marker_defense_lanes.sh +152 -0
  49. package/scripts/test_marker_soul_check_lanes.sh +211 -0
  50. package/scripts/test_prior_art_prompt_lanes.sh +128 -0
  51. package/scripts/test_psa_singlefile_lanes.sh +351 -1
  52. package/scripts/test_residency_admission_lanes.sh +60 -0
  53. package/scripts/test_track_resolve_lanes.sh +158 -0
  54. package/templates/.git-hooks/pre-commit +400 -4
  55. package/templates/.git-hooks/pre-push +17 -2
  56. package/templates/settings.PriorArt.snippet.json +15 -0
  57. package/scripts/test_satellite_publish_gate_lanes.sh +0 -339
@@ -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
@@ -110,6 +118,15 @@ ACCEPTED_ABSENT=(
110
118
  # honest state is "declared, and the declaration records a debt" โ€” carried to the card.
111
119
  "scripts/test_chamber_run_lanes.sh"
112
120
  "scripts/test_frontier_digest_retry.sh"
121
+ # ยท test_satellite_publish_gate_lanes.sh / test_satellite_profile_schema_lanes.sh
122
+ # โ€” 2026-08-20. ๊ฐ™์€ ๋ชจ์–‘์ด๊ณ , ๋‘˜ ๋‹ค **์ด๋ฏธ files[] ์— ๋“ค์–ด๊ฐ„ ์ฑ„ ๋ฐฐํฌ๋œ ๋’ค์—** ์žกํ˜”๋‹ค.
123
+ # ์ฃผ์ฒด๊ฐ€ frontier_digest_daily.sh(์•„๋ž˜ ACCEPTED_ABSENT)๋ผ ์†Œ๋น„์ž ๋จธ์‹ ์—” ์žด ๋Œ€์ƒ์ด ์—†๋‹ค.
124
+ # ๐ŸŸฅ ์‹ค์ธก(๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์‹ค๋ฌผ 2.5.1, ์†Œ๋น„์ž ์„ค์น˜์—์„œ ์‹คํ–‰): publish_gate 4 passed / 21 failed,
125
+ # `rc=127`(๋Ÿฌ๋„ˆ ๋ถ€์žฌ) โ†’ selfcheck ๊ฐ€ ํ•˜๋“œ FAIL โ†’ **๋ฐฐํฌ๋ณธ์ด `SELFCHECK: FAIL` ์ด์—ˆ๋‹ค.**
126
+ # ๐ŸŸฅ ์ด ๊ฒ€์‚ฌ๊ธฐ๋Š” ๊ทธ๊ฑธ rc=0 ์œผ๋กœ ํ†ต๊ณผ์‹œ์ผฐ๋‹ค โ€” ์—ฌ๊ธฐ๋Š” ใ€Œ์ฐธ์กฐ๋œ ๊ฒฝ๋กœ๊ฐ€ ์ถœํ•˜๋˜๋‚˜ใ€๋ฅผ ๋ณด์ง€
127
+ # ใ€Œ์ถœํ•˜๋œ ๋ ˆ์ธ์˜ **์ฃผ์ฒด**๊ฐ€ ์ถœํ•˜๋˜๋‚˜ใ€๋ฅผ ์•ˆ ๋ณธ๋‹ค. ๊ทธ ๊ฐญ์€ ์•„์ง ์•ˆ ๋‹ซํ˜”๋‹ค(๋ช…์‹œ ์ž”์—ฌ).
128
+ "scripts/test_satellite_publish_gate_lanes.sh"
129
+ "scripts/test_satellite_profile_schema_lanes.sh"
113
130
  "scripts/residency_closure_scan.py"
114
131
  "scripts/test_residency_closure_lanes.sh"
115
132
  # โ”€โ”€ The two settings destinations, surfaced 2026-08-13 by fixing this file's own extractor โ”€โ”€โ”€โ”€
@@ -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
@@ -112,7 +112,19 @@ psa_low_allowlisted() {
112
112
  # Prints diagnostics for every degraded state; sets the flags above. NEVER exits โ€” the caller owns
113
113
  # the degrade direction, because it differs by surface (see the header).
114
114
  psa_load() {
115
- local defaults="$1" override="$2" raw ov
115
+ local defaults="$1" override="$2" raw ov _tabtest
116
+ # ๐ŸŸฅ R5-3: ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” `$'\t'` (ANSI-C quoting) ์— ์˜์กดํ•œ๋‹ค โ€” **POSIX ๊ฐ€ ์•„๋‹ˆ๋‹ค.**
117
+ # dash ์—์„œ๋Š” ๋ฆฌํ„ฐ๋Ÿด `$\t` ๋กœ ์ฝํ˜€ ์ •์ƒ ํ–‰์ด ยซTAB ์—†์Œยป ์œผ๋กœ ๋–จ์–ด์ง€๊ณ , ์ŠคํŠธ๋ฆผ์ด ํ†ต์งธ๋กœ
118
+ # ๋น„๋ฉด์„œ **bad=1 ยท stream=[] ๋กœ ์กฐ์šฉํžˆ ๋ฌด๋ ฅํ™”**๋๋‹ค(cross-family ์‹ค์ธก). ๊ทธ๊ฑด ๋ฏธ์ธก์ •์ด์ง€
119
+ # ใ€ŒํŒจํ„ด์ด ์—†๋‹คใ€๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ง€์› ๋ฒ”์œ„๋Š” bash/zsh ์ด๊ณ , ๊ทธ๊ฑธ ์‚ฐ๋ฌธ์ด ์•„๋‹ˆ๋ผ ์—ฌ๊ธฐ์„œ ํ™•์ธํ•œ๋‹ค.
120
+ # ํŒ๋ณ„์€ ๊ธธ์ด๋‹ค โ€” ์‹ค์ธก: bash/zsh `len=1`(์ง„์งœ TAB) ยท dash `len=3`(๋ฆฌํ„ฐ๋Ÿด `$\t`).
121
+ _tabtest=$'\t'
122
+ if [ "${#_tabtest}" -ne 1 ]; then
123
+ echo " โŒ INSTRUMENT DEAD โ€” this shell does not support \$'\\t' (ANSI-C quoting)." >&2
124
+ echo " psa_scan_lib.sh requires bash or zsh. Re-run under one of them; NOT SCANNED." >&2
125
+ PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=1
126
+ return 3
127
+ fi
116
128
  PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=0
117
129
 
118
130
  # Layer 1 โ€” committed defaults. This file ships with the repo, so missing/unreadable/empty is a
@@ -156,11 +168,23 @@ $ov"; PSA_OVERRIDE_PRESENT=1
156
168
  echo " โŒ unusable pattern row (no TAB between severity and regex): ${line%%$'\t'*}"
157
169
  PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
158
170
  fi
159
- printf '' | grep -E "$re" >/dev/null 2>&1; rc=$? # 0/1 = compiled; >=2 = regex error
171
+ # ๐ŸŸฅ R4-1: ์—ฌ๊ธฐ๋„ `cmd; rc=$?` ์˜€๋‹ค โ€” P3 ๋ฅผ ์Šค์บ” ๊ฒฝ๋กœ์—์„œ๋งŒ ๊ณ ์น˜๊ณ  **๋กœ๋” ๊ฒฝ๋กœ๋กœ ์ „ํŒŒ๋ฅผ ์•ˆ ๋ดค๋‹ค**.
172
+ # ์ •์ƒ ํŒจํ„ด์€ ๋นˆ ์ž…๋ ฅ์— rc=1 ์„ ๋‚ด๋ฏ€๋กœ `set -e` ํ˜ธ์ถœ์ž๋Š” psa_load ์—์„œ ์ฃฝ๋Š”๋‹ค(์„ ์žฌ, ์–‘ ์…ธ ์‹ค์ธก).
173
+ if printf '' | grep -E "$re" >/dev/null 2>&1; then rc=0; else rc=$?; fi # >=2 = regex error
160
174
  if [ "$rc" -ge 2 ]; then
161
175
  echo " โŒ unusable pattern (invalid regex) โ€” this detector would match nothing: [${line%%$'\t'*}]"
162
176
  PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
163
177
  fi
178
+ # ๐ŸŸฅ P4 (์„ ์žฌ ๊ฒฐํ•จ): ํŒ๋ณ„๋ ฅ ์—†๋Š” ํŒจํ„ด์„ ๊ฑฐ๋ถ€ํ•œ๋‹ค โ€” ๋นˆ ํŒจํ„ด ยท `^` ยท `a*` ยท `SECRET|^$`.
179
+ # ๋ชจ๋“  ์ž…๋ ฅ์— ๋งค์น˜ํ•˜๋ฏ€๋กœ ๊ฒ€์ถœ๊ธฐ๋กœ **์“ธ ์ˆ˜ ์—†๋Š”๋ฐ** ์ดˆํŒ ๋กœ๋”๋Š” ์œ ํšจ๋กœ ๋ฐ›์•„๋“ค์˜€๋‹ค.
180
+ # ๐ŸŸฅ ๊ทธ๋ฆฌ๊ณ  ์ด ๊ฒ€์‚ฌ๋Š” **์œ„์˜ ์ปดํŒŒ์ผ ๊ฒ€์‚ฌ๋กœ๋Š” ๋ชป ํ•œ๋‹ค**: `printf ''` ๋Š” ์ค„์„ 0๊ฐœ ์ฃผ๋ฏ€๋กœ
181
+ # grep ์ด ๋งค์น˜ํ•  ๋Œ€์ƒ ์ž์ฒด๊ฐ€ ์—†์–ด **์–ด๋–ค ํŒจํ„ด์ด๋“  rc=1** ์ด๋‹ค(์‹ค์ธก). ์œ„ ์ฃผ์„์˜
182
+ # ใ€Œ0 = ์ปดํŒŒ์ผ๋จใ€์€ ๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฐ’์„ ์„œ์ˆ ํ•œ ๊ฒƒ์ด์—ˆ๋‹ค. ๋นˆ ยซ์ค„ยป ์„ ๋”ฐ๋กœ ๋จน์ธ๋‹ค.
183
+ if printf '\n' | grep -E "$re" >/dev/null 2>&1; then rc=0; else rc=$?; fi
184
+ if [ "$rc" -eq 0 ]; then
185
+ echo " โŒ unusable pattern (matches the EMPTY string โ€” no discrimination): [${line%%$'\t'*}]"
186
+ PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
187
+ fi
164
188
  valid="$valid
165
189
  $line"
166
190
  done <<PSA_ROWS
@@ -208,31 +232,128 @@ psa_detect_operator_context() {
208
232
  # Why path-tagged input: the LOW allowlist is per FILE, so a scanner that flattens content loses the
209
233
  # only information needed to apply it. The push copy did exactly that and blocked its own first real
210
234
  # push on a companion-store name inside the script whose job is to sync to the companion store.
235
+ # _psa_in_selftest โ€” ยซ์ง€๊ธˆ psa_require_live ์•ˆ์—์„œ ๋ถˆ๋ฆฐ ๊ฒƒ์ธ๊ฐ€ยป๋ฅผ **ํ˜ธ์ถœ ์Šคํƒ**์œผ๋กœ ํŒ๋ณ„ํ•œ๋‹ค.
236
+ # ๐ŸŸฅ ๋ณ€์ˆ˜ ํ‘œ์‹์„ ์“ฐ๋ฉด ์•ˆ ๋œ๋‹ค: ์™ธ๋ถ€๊ฐ€ `_PSA_IN_SELFTEST=1` ์„ ๋ฏธ๋ฆฌ export ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ
237
+ # ์ž๊ฐ€๊ฒ€์‚ฌ๊ฐ€ ํ†ต์งธ๋กœ ์ƒ๋žต๋œ๋‹ค(cross-family ๋ฐ˜๋ก€ 1b, ์‹คํ–‰ ์žฌํ˜„). ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ถŒํ•œ ํ‘œ์‹์ฒ˜๋Ÿผ
238
+ # ๋ฏฟ๋Š” ํ˜•ํƒœ๋‹ค. ํ˜ธ์ถœ ์Šคํƒ์€ **ํ˜ธ์ถœ์ž๊ฐ€ ๋ฏธ๋ฆฌ ์‹ฌ์„ ์ˆ˜ ์—†๋‹ค.**
239
+ # bash=FUNCNAME ยท zsh=funcstack. ๋‘˜ ๋‹ค ์—†์œผ๋ฉด ํŒ๋ณ„ ๋ถˆ๊ฐ€์ด๋ฏ€๋กœ **์žฌ์ง„์ž…์ด ์•„๋‹ˆ๋ผ๊ณ  ๋‹ตํ•œ๋‹ค** โ€”
240
+ # ๊ทธ๋Ÿฌ๋ฉด ์ž๊ฐ€๊ฒ€์‚ฌ๊ฐ€ ํ•œ ๋ฒˆ ๋” ๋Œ ๋ฟ(๋ฌดํ•œ์žฌ๊ท€๋Š” psa_require_live ๊ฐ€ ์ด ํ•จ์ˆ˜๋ฅผ ํ•œ ๊ฒน๋งŒ ๋ถ€๋ฅด๋ฏ€๋กœ
241
+ # ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค), ๋ฐ˜๋Œ€๋กœ ยซ์žฌ์ง„์ž…์ด๋‹คใ€๋กœ ์ ‘์œผ๋ฉด ๊ทธ๊ฒŒ fail-open ์ด๋‹ค.
242
+ _psa_in_selftest() {
243
+ local _st=""
244
+ # shellcheck disable=SC2154
245
+ if [ -n "${FUNCNAME[*]:-}" ]; then _st=" ${FUNCNAME[*]} "
246
+ elif [ -n "${funcstack[*]:-}" ]; then _st=" ${funcstack[*]} "
247
+ else return 1; fi
248
+ case "$_st" in *" psa_require_live "*) return 0 ;; esac
249
+ return 1
250
+ }
251
+
211
252
  psa_scan_tagged() {
212
- local input hit=0 row sev re path body tok
213
- input=$(cat)
253
+ # ๐ŸŸฅ `path` ๋ฅผ ์ง€์—ญ์„ ์–ธํ•˜์ง€ ๋งˆ๋ผ โ€” zsh ์—์„œ `path` ๋Š” `PATH` ์™€ **tied ๋œ ํŠน์ˆ˜ ๋ฐฐ์—ด**์ด๋ผ
254
+ # `local path` ํ•˜๋Š” ์ˆœ๊ฐ„ ๊ทธ ์Šค์ฝ”ํ”„์˜ `PATH` ๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด ๋˜๊ณ , ๋ฐ”๋กœ ๋‹ค์Œ ์ค„์˜
255
+ # `$(cat)` ๋ถ€ํ„ฐ `command not found: cat` ์œผ๋กœ ์ฃฝ๋Š”๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด ํ•จ์ˆ˜์˜ ๊ณ„์•ฝ์ด
256
+ # ใ€Œ๋นˆ ์ž…๋ ฅ = ์‹ ๊ณ ํ•  ๊ฒƒ ์—†์Œ = return 0ใ€์ด๋ผ **๊ณ„๊ธฐ ์‚ฌ๋ง์ด ยซ๊นจ๋—ํ•œ ์Šค์บ”ยป๊ณผ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ
257
+ # ๊ฐ™์€ ์ถœ๋ ฅ**์ด ๋œ๋‹ค โ€” ์ด ์ €์žฅ์†Œ๊ฐ€ ์ด๋ฆ„ ๋ถ™์ธ ยซ๋ฏธ์ธก์ •์„ 0์œผ๋กœ ๋ Œ๋”ยป ์˜ ๊ฐ€์žฅ ์กฐ์šฉํ•œ ์–ผ๊ตด์ด๋‹ค.
258
+ # ์‹ค์ธก 2026-08-21: zsh ์—์„œ known-positiveยทknown-negative ๊ฐ€ **๋‘˜ ๋‹ค rc=0** ์ด์—ˆ๋‹ค.
259
+ # ํ˜•์ œ ํŠน์ˆ˜๋ณ€์ˆ˜(`fpath` `status` `argv` `cdpath` โ€ฆ)๋„ ๊ฐ™์€ ํ•จ์ •์ด๋‹ค.
260
+ local input hit=0 row sev re _psa_path body tok _line_hits _tok_hits _lg _tg
261
+
262
+ # โ”€โ”€ E: ์ง„์ž… ์ƒ์กด์„ฑ ๊ฐ€๋“œ (2026-08-21, cross-family FAIL ํŒ์ • ํ›„ ์žฌ์ž‘์„ฑ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
263
+ # ๐ŸŸฅ **์ดˆํŒ E ๋Š” fail-open ์„ ๋ชป ๋‹ซ์•˜๋‹ค.** codex/gpt-5.6-sol ์ด ๋ฐ˜๋ก€ 6๊ฐœ๋ฅผ ์ „๋ถ€ ์‹คํ–‰์œผ๋กœ
264
+ # ์žฌํ˜„ํ–ˆ๊ณ  ํŒ์ •์€ FAIL ์ด์—ˆ๋‹ค. ์ดˆํŒ์˜ ๊ตฌ์กฐ์  ๋งน์ : **ยซ์ž๊ฐ€๊ฒ€์‚ฌ๊ฐ€ ํ•œ ๋ฒˆ ํ†ต๊ณผํ–ˆ๋‹คยป๋ฅผ
265
+ # ยซ๊ทธ ๋’ค์˜ ์‹ค์ œ ์Šค์บ”๋„ ์‹คํ–‰๋๋‹คยป๋กœ ํ™•์žฅ**ํ•œ ๊ฒƒ. ๊ทธ ์‚ฌ์ด์—” ์‹œ๊ฐ„ยท์ „์—ญ์ƒํƒœยทํŒŒ์ดํ”„๋ผ์ธ
266
+ # ์ƒํƒœ๋ผ๋Š” ๋‹จ์ ˆ์ ์ด ์…‹ ์žˆ๋‹ค. ์žฌ์ž‘์„ฑ์ด ๋‹ซ๋Š” ๊ฒƒ๊ณผ ๋ชป ๋‹ซ๋Š” ๊ฒƒ์„ ์•„๋ž˜์— ์ด๋ฆ„์œผ๋กœ ์ ๋Š”๋‹ค.
267
+ #
268
+ # ๐ŸŸฅ **์บ์‹œ ์ œ๊ฑฐ.** ์ดˆํŒ์€ `_PSA_LIVE_OK` ๋กœ ํ”„๋กœ์„ธ์Šค๋‹น 1ํšŒ๋งŒ ๊ฒ€์‚ฌํ–ˆ๋‹ค โ€” ์ฒซ ์–‘์„ฑ ๋’ค
269
+ # grep ์„ ์ฃฝ์ด๋ฉด ๊ฐ™์€ ์–‘์„ฑ์ด rc=0 ์ด ๋๋‹ค(codex ์žฌํ˜„). ๊ทธ๋ฆฌ๊ณ  **์บ์‹œ๋Š” ๋šซ๋ฆฐ ์ชฝ(๊ธด
270
+ # ์Šคํ‚ฌ ์„ธ์…˜)์—์„œ๋งŒ ์œ„ํ—˜ํ•˜๊ณ  ์•ˆ์ „ํ•œ ์ชฝ(์งง์€ ํ›…)์—์„œ๋งŒ ์ด๋“**์ด๋ผ ๋ฐฉํ–ฅ์ด ๋ฐ˜๋Œ€์˜€๋‹ค.
271
+ # ๋น„์šฉ์€ **์‹ค์ธกํ–ˆ๋‹ค: psa_require_live 1ํšŒ โ‰ˆ 64ms** (ํ›… ๊ฒฝ๋กœ ์ตœ๋Œ€ 4ํšŒ = 0.26์ดˆ).
272
+ # ์ดˆํŒ์€ ์ด ๋น„์šฉ์„ **์•ˆ ์žฌ๊ณ ** ์บ์‹œ๋ฅผ ์ •๋‹นํ™”ํ–ˆ๋‹ค โ€” ๊ทธ๊ฒŒ ์ง„์งœ ๊ฒฐํ•จ์ด์—ˆ๋‹ค.
273
+ #
274
+ # ๐ŸŸฅ **์žฌ์ง„์ž… ํ‘œ์‹์€ `local` ๋กœ ยซ๋™์  ์Šค์ฝ”ํ”„ยป์— ๋‘”๋‹ค.** ์ดˆํŒ์€ ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ๊ถŒํ•œ ํ‘œ์‹์ฒ˜๋Ÿผ
275
+ # ๋ฏฟ์–ด์„œ, ์™ธ๋ถ€๊ฐ€ `_PSA_IN_SELFTEST=1` ์„ ๋ฏธ๋ฆฌ export ํ•˜๋ฉด ์ž๊ฐ€๊ฒ€์‚ฌ๊ฐ€ **ํ†ต์งธ๋กœ ์ƒ๋žต**๋๋‹ค
276
+ # (codex ์žฌํ˜„: `grep(){ return 127; }` ์ƒํƒœ์—์„œ known-positive ๊ฐ€ rc=0 ๋ฌด์ถœ๋ ฅ).
277
+ # `local` ์„ ์–ธ์€ ์™ธ๋ถ€ ๊ฐ’์„ **๊ฐ€๋ฆฌ๊ณ **, ํ•จ์ˆ˜๊ฐ€ ๋๋‚˜๋ฉด **์ž๋™ ๋ณต๊ตฌ**๋œ๋‹ค โ€” unset ์‹คํŒจ๋‚˜
278
+ # ์‹œ๊ทธ๋„ยทํŠธ๋žฉ์œผ๋กœ ๊ฐ’์ด ๋‚จ๋Š” ๊ฒฝ๋กœ๊ฐ€ ์—†์–ด์ง„๋‹ค.
279
+ if ! _psa_in_selftest; then
280
+ # ๐ŸŸฅ ์‹ค์ œ ํŒจํ„ด ์ŠคํŠธ๋ฆผ์„ ์—ฌ๊ธฐ์„œ ๋ณธ๋‹ค. ์ž๊ฐ€๊ฒ€์‚ฌ๋Š” PSA_STREAM ์„ ์นด๋‚˜๋ฆฌ์•„๋กœ **๋ฐ”๊ฟ”์น˜๊ธฐ**
281
+ # ํ•˜๋ฏ€๋กœ ยซ์ง„์งœ ํŒจํ„ด์ด ๋น„์—ˆ๊ฑฐ๋‚˜ ๊นจ์กŒ๋Š”์ง€ยป๋ฅผ ๊ตฌ์กฐ์ ์œผ๋กœ ๋ชป ๋ณธ๋‹ค(codex ๋ฐ˜๋ก€ 3).
282
+ if [ -z "${PSA_STREAM:-}" ]; then
283
+ echo " โŒ INSTRUMENT DEAD โ€” PSA_STREAM is empty; nothing to match against. NOT SCANNED."
284
+ echo " โŒ INSTRUMENT DEAD โ€” PSA_STREAM is empty; NOT SCANNED (call psa_load first)." >&2
285
+ return 3
286
+ fi
287
+ # ๐ŸŸฅ R4-6: **๋ถ€๋ถ„ ๋กœ๋“œ๋œ ํŒจํ„ด ์ง‘ํ•ฉ์œผ๋กœ ยซ๊นจ๋—ยป์„ ๋งํ•˜์ง€ ์•Š๋Š”๋‹ค.** ์ดˆํŒ์€ ์ด๊ฒƒ์„ ยซํ˜ธ์ถœ์ž
288
+ # ๊ณ„์•ฝยป์œผ๋กœ ๋‘์—ˆ๊ณ (ํ—ค๋”๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ์ ์–ด๋’€๋‹ค), cross-family ๊ฐ€ ๊ทธ ๊ณ„์•ฝ์„ ์•ˆ ์ง€ํ‚ค๋Š” ํ˜ธ์ถœ์ž๋ฅผ
289
+ # ์‹ค์ธก ์žฌํ˜„ํ–ˆ๋‹ค โ€” `SECRET|^$` ํ•œ ํ–‰์ด ๋–จ์–ด์ ธ ๋‚˜๊ฐ„ ์ง‘ํ•ฉ์œผ๋กœ `SECRET` ์ž…๋ ฅ์ด rc=0 ยซ๊นจ๋—ยป.
290
+ # ์‚ฐ๋ฌธ ๊ณ„์•ฝ์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ธต์œผ๋กœ ๋‚ด๋ฆฐ๋‹ค: ์•ˆ ์‹ค๋ฆฐ ๊ฒ€์ถœ๊ธฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฑด ๋ฏธ์ธก์ •์ด๋‹ค.
291
+ # (psa_load ๋ฅผ ์•ˆ ๋ถ€๋ฅธ ํ˜ธ์ถœ์ž๋Š” PSA_BAD_ROWS ๊ฐ€ ๋ฏธ์„ค์ •์ด๊ณ , ๊ทธ ๊ฒฝ๋กœ๋Š” ์œ„ ๋นˆ-์ŠคํŠธ๋ฆผ
292
+ # ๊ฒ€์‚ฌ๊ฐ€ ์ด๋ฏธ ์žก๋Š”๋‹ค. `case` ๋กœ ๋ณด๋Š” ์ด์œ ๋Š” ๋น„์ˆ˜์น˜ ์˜ค์—ผ์—์„œ fail-open ํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด์„œ๋‹ค.)
293
+ case "${PSA_BAD_ROWS:-0}" in
294
+ 0) ;;
295
+ *) echo " โŒ INSTRUMENT DEAD โ€” ${PSA_BAD_ROWS} pattern row(s) were dropped; the detector set is PARTIAL. NOT SCANNED."
296
+ echo " โŒ INSTRUMENT DEAD โ€” ${PSA_BAD_ROWS} pattern row(s) dropped โ€” a partial set cannot report clean." >&2
297
+ return 3 ;;
298
+ esac
299
+ if ! psa_require_live >/dev/null 2>&1; then
300
+ echo " โŒ INSTRUMENT DEAD โ€” psa_scan_tagged self-test failed. NOT SCANNED."
301
+ echo " โŒ INSTRUMENT DEAD โ€” psa_scan_tagged self-test failed. NOT SCANNED." >&2
302
+ echo " A 0 from this scanner is UNMEASURED, not clean. Re-run under bash." >&2
303
+ return 3
304
+ fi
305
+ fi
306
+
307
+ # ๐ŸŸฅ `cat` ์˜ ์ƒํƒœ๋ฅผ ๋ฒ„๋ฆฌ์ง€ ๋งˆ๋ผ (codex ๋ฐ˜๋ก€ 1). ์ดˆํŒ์€ `input=$(cat)` ๋กœ ๋Œ€์ž…๋งŒ ํ•˜๊ณ 
308
+ # rc ๋ฅผ ๋ฒ„๋ ค์„œ, cat ์ด ์ฃฝ์–ด ์ž…๋ ฅ์ด ๋น„๋ฉด ๊ทธ๊ฒŒ ๊ณง ยซ์‹ ๊ณ ํ•  ๊ฒƒ ์—†์Œยป์ด ๋๋‹ค.
309
+ if ! input=$(cat); then
310
+ echo " โŒ INSTRUMENT DEAD โ€” could not read stdin. NOT SCANNED."
311
+ echo " โŒ INSTRUMENT DEAD โ€” could not read stdin. NOT SCANNED." >&2
312
+ return 3
313
+ fi
214
314
  [ -n "$input" ] || return 0
215
315
  while IFS= read -r row; do
216
316
  case "$row" in ''|\#*) continue;; esac
217
317
  re="${row#*$'\t'}"; [ "$re" = "$row" ] && continue
218
318
  sev="${row%%$'\t'*}"
319
+ # ๐ŸŸฅ grep ์ƒํƒœ๋ฅผ ยซ๋งค์น˜ ์—†์Œยป ์œผ๋กœ ์ ‘์ง€ ๋งˆ๋ผ (cross-family ๋ฐ˜๋ก€ 2, 2026-08-21).
320
+ # `2>/dev/null || true` ๋Š” rc 0(๋งค์น˜)ยท1(์—†์Œ)ยท**2 ์ด์ƒ(๊ณ„๊ธฐ ์˜ค๋ฅ˜)**์„ ํ•˜๋‚˜๋กœ ๋ญ‰๊ฐ ๋‹ค.
321
+ # ์‹ค์ธก ๋„๋‹ฌ ๊ฒฝ๋กœ: ํŒจํ„ด ํŒŒ์ผ์— **๊นจ์ง„ ์ •๊ทœ์‹**์ด ํ•œ ์ค„ ์žˆ์œผ๋ฉด ๊ทธ ํ–‰๋งŒ ์กฐ์šฉํžˆ ์•„๋ฌด๊ฒƒ๋„
322
+ # ์•ˆ ์žก๊ณ  ์Šค์บ”์€ rc=0 ยซ๊นจ๋—ยป ์„ ๋‚ธ๋‹ค. ์ž๊ฐ€๊ฒ€์‚ฌ๋Š” ์ž๊ธฐ ์นด๋‚˜๋ฆฌ์•„ ํŒจํ„ด์„ ์“ฐ๋ฏ€๋กœ
323
+ # **์ง„์งœ ํŒจํ„ด์˜ ๊นจ์ง์„ ๊ตฌ์กฐ์ ์œผ๋กœ ๋ชป ๋ณธ๋‹ค** โ€” ๊ทธ๋ž˜์„œ ์—ฌ๊ธฐ์„œ ๋ด์•ผ ํ•œ๋‹ค.
324
+ # ๐ŸŸฅ P3 ๋™ํ˜•: grep ์˜ ์ •์ƒ ยซ๋ฏธ๋งค์น˜ยป(rc=1)๊ฐ€ `set -e` ํ˜ธ์ถœ์ž๋ฅผ ์ฃฝ์ธ๋‹ค.
325
+ if _line_hits=$(printf '%s\n' "$input" | grep -iE "$re" 2>/dev/null); then _lg=0; else _lg=$?; fi
326
+ if [ "$_lg" -gt 1 ]; then
327
+ echo " โŒ INSTRUMENT DEAD โ€” grep failed (rc=$_lg) on pattern: $re"
328
+ echo " โŒ INSTRUMENT DEAD โ€” grep failed (rc=$_lg) on pattern: $re" >&2
329
+ echo " A broken pattern row scans NOTHING and reads as clean. NOT SCANNED." >&2
330
+ return 3
331
+ fi
219
332
  while IFS= read -r line; do
220
333
  [ -z "$line" ] && continue
221
- path="${line%%$'\t'*}"; body="${line#*$'\t'}"
334
+ _psa_path="${line%%$'\t'*}"; body="${line#*$'\t'}"
222
335
  # EVERY match on the line. Taking only the first let a documented placeholder earlier on the
223
336
  # line shield a real token later on it.
337
+ if _tok_hits=$(printf '%s' "$body" | grep -oiE "$re" 2>/dev/null); then _tg=0; else _tg=$?; fi
338
+ if [ "$_tg" -gt 1 ]; then
339
+ echo " โŒ INSTRUMENT DEAD โ€” grep -o failed (rc=$_tg) on pattern: $re"
340
+ echo " โŒ INSTRUMENT DEAD โ€” grep -o failed (rc=$_tg) on pattern: $re" >&2
341
+ return 3
342
+ fi
224
343
  while IFS= read -r tok; do
225
344
  [ -z "$tok" ] && continue
226
345
  printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
227
- if [ "$sev" = "LOW" ] && psa_low_allowlisted "$path"; then continue; fi
228
- if psa_pair_allowlisted "$path" "$tok"; then continue; fi
229
- echo " โŒ $sev leak โ€” ${path}: '$tok'"
346
+ if [ "$sev" = "LOW" ] && psa_low_allowlisted "$_psa_path"; then continue; fi
347
+ if psa_pair_allowlisted "$_psa_path" "$tok"; then continue; fi
348
+ # ๐ŸŸฅ P5: zsh ์˜ `echo` ๋Š” ๋ฐฑ์Šฌ๋ž˜์‹œ๋ฅผ ํ•ด์„ํ•œ๋‹ค โ€” ๊ฒฝ๋กœ `dir\two` ๊ฐ€ ํƒญ์œผ๋กœ ๋ณ€ํ•ด
349
+ # ์ฆ๊ฑฐ ๋ฌธ์ž์—ด์ด ์†์ƒ๋๋‹ค(์‹ค์ธก). fail-open ์€ ์•„๋‹ˆ๋‚˜ ์ฆ๊ฑฐ๋Š” ์ฆ๊ฑฐ์—ฌ์•ผ ํ•œ๋‹ค.
350
+ printf ' โŒ %s leak โ€” %s: \x27%s\x27\n' "$sev" "${_psa_path}" "$tok"
230
351
  hit=1
231
352
  done <<PSA_TOK
232
- $(printf '%s' "$body" | grep -oiE "$re" 2>/dev/null || true)
353
+ $_tok_hits
233
354
  PSA_TOK
234
355
  done <<PSA_LINES
235
- $(printf '%s\n' "$input" | grep -iE "$re" 2>/dev/null || true)
356
+ $_line_hits
236
357
  PSA_LINES
237
358
  done <<PSA_PAT
238
359
  $PSA_STREAM
@@ -285,15 +406,42 @@ _psa_can_assign() {
285
406
  # the caller's shell before the restore line. Status capture is now inside an `if`.
286
407
  psa_require_live() {
287
408
  local saved_stream saved_allow saved_stream_set saved_allow_set tmpd out rc _canary
409
+ local _nonce _tag_rc
288
410
  saved_stream="${PSA_STREAM-}"; saved_allow="${PSA_ALLOWLIST-}"
289
411
  saved_stream_set=""; saved_allow_set=""
290
412
  [ "${PSA_STREAM+x}" = "x" ] && saved_stream_set=1
291
413
  [ "${PSA_ALLOWLIST+x}" = "x" ] && saved_allow_set=1
292
414
  tmpd=$(mktemp -d 2>/dev/null) || { echo " โŒ INSTRUMENT DEAD โ€” mktemp unavailable." >&2; return 1; }
415
+ # ๐ŸŸฅ P1 (cross-family 3๋ผ์šด๋“œ): ์นด๋‚˜๋ฆฌ์•„๊ฐ€ **๊ณ ์ • ๋ฌธ์ž์—ด**์ด๋ฉด, ์ž…๋ ฅ์„ ์ฝ์ง€๋„ ์•Š๊ณ 
416
+ # ๊ทธ ๋ฌธ์ž์—ด์„ rc=0 ์œผ๋กœ ๋ฑ‰๋Š” ์œ„์กฐ ์ƒ์‚ฐ์ž๊ฐ€ ์ž๊ฐ€๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•œ๋‹ค. ์„ธ ๋ผ์šด๋“œ๊ฐ€ ๊ณต์œ ํ•œ
417
+ # ์–ผ๊ตด โ€”ใ€Œ์ƒํƒœ(rc=0)๋ฅผ ์˜๋ฏธ(์ƒ์‚ฐ์ด ์œ ํšจํ•˜๋‹ค)๋กœ ํ™•์žฅใ€โ€” ์˜ ๋งˆ์ง€๋ง‰ ์ž”์žฌ๋‹ค.
418
+ # ์‹คํ–‰๋งˆ๋‹ค ์ƒˆ nonce ๋ฅผ ๋งŒ๋“ค๋ฉด **ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์œ„์กฐ๋Š” ๊ทธ ๊ฐ’์„ ์•Œ ์ˆ˜ ์—†๋‹ค.**
419
+ _nonce=$(head -c 12 /dev/urandom 2>/dev/null | od -An -tx1 2>/dev/null | tr -d ' \n')
420
+ # ๐ŸŸฅ **์‹ ๋ขฐ ๊ฒฝ๊ณ„ โ€” ์—ฌ๊ธฐ์„œ ๋ผ์šด๋“œ๋ฅผ ๋ฉˆ์ถ”๋Š” ์ด์œ (2026-08-21, 6๋ผ์šด๋“œ ํ›„ ํŒ์ •).**
421
+ # ์ด ์ž๊ฐ€๊ฒ€์‚ฌ๊ฐ€ ๋ง‰๋Š” ๊ฒƒ: ์ƒ์‚ฐ์ž๊ฐ€ **๊นจ์กŒ๊ฑฐ๋‚˜**(awk ๋ถ€์žฌยทcrashยท์ถœ๋ ฅ ํ›ผ์†), **PATH ๋กœ
422
+ # ๊ฐ€๋ ค์กŒ๊ฑฐ๋‚˜**(์™ธ๋ถ€ ๋ฐ”์ด๋„ˆ๋ฆฌ shadowing), ์ž…๋ ฅ์„ **์•ˆ ์ฝ๊ณ ** ๊ณ ์ •/์œ ๋„๊ฐ’์„ ๋ฑ‰๋Š” ๊ฒฝ์šฐ.
423
+ # ์ด ์ž๊ฐ€๊ฒ€์‚ฌ๊ฐ€ **๋ชป** ๋ง‰๋Š” ๊ฒƒ: ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ **์†Œ์‹ฑํ•œ ์…ธ ์•ˆ์—์„œ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋Š”**
424
+ # ์ƒ๋Œ€. ๊ทธ ์ƒ๋Œ€๋Š” `psa_scan_tagged(){ return 0; }` ํ•œ ์ค„์ด๋ฉด ๋์ด๋ผ nonceยทcmpยท์‹ฌ๋งํฌ๋ฅผ
425
+ # ์•„๋ฌด๋ฆฌ ์กฐ์—ฌ๋„ ์†Œ์šฉ์ด ์—†๋‹ค โ€” **์ด๋ฏธ ์‹ ๋ขฐ ์•ˆ์ชฝ**์ด๋‹ค. 6๋ผ์šด๋“œ ๋ฐ˜๋ก€ ๋‘˜์ด ์ •ํ™•ํžˆ ๊ทธ ๋ถ€๋ฅ˜์˜€๊ณ 
426
+ # (`cmp(){ return 0; }` ยท `ln -sf` ๋กœ tagged ๋ฐ”๊ฟ”์น˜๊ธฐ), ๊ฐ’์‹ผ ๋‘ ์ˆ˜๋ฆฌ(`command cmp` ยท
427
+ # ์‹ฌ๋งํฌ ๊ฑฐ๋ถ€)๋Š” ํ–ˆ์ง€๋งŒ **๊ทธ ์ถ•์„ ๋‹ซ์•˜๋‹ค๊ณ  ์ฃผ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค.**
428
+ # ๐ŸŸฅ R5-1: **fallback ์„ ์—†์•ด๋‹ค.** 5๋ผ์šด๋“œ ์‹ค์ธก โ€”
429
+ # `${tmpd##*/}` ํŒŒ์ƒ โ†’ ์ƒ์‚ฐ์ž๊ฐ€ ๊ฒฝ๋กœ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์œผ๋ฏ€๋กœ ์œ ๋„ ๊ฐ€๋Šฅ (4๋ผ์šด๋“œ์—์„œ ๊ต์ฒด)
430
+ # `${RANDOM}${RANDOM}$$${SECONDS}` โ†’ **์ƒ์‚ฐ์ž๊ฐ€ ๊ฐ™์€ ์…ธ์—์„œ ๊ทธ๋Œ€๋กœ ์žฌ๊ณ„์‚ฐํ•ด ํ†ต๊ณผ**(zsh ์‹ค์ธก rc=0),
431
+ # `RANDOM`/`SECONDS` ๊ฐ€ unset ์ด๋ฉด ์‚ฌ์‹ค์ƒ `$$` ํ•˜๋‚˜๋ผ `ps` ๋กœ๋„ ์ฝํžŒ๋‹ค(bash ์‹ค์ธก rc=0)
432
+ # โ‡’ ใ€Œ์˜ˆ์ธก ๋ถˆ๊ฐ€ํ•œ ๊ฐ’ใ€์€ **์ปค๋„ ์—”ํŠธ๋กœํ”ผ์—์„œ๋งŒ** ๋‚˜์˜จ๋‹ค. ๋ชป ์–ป์œผ๋ฉด ๊ทธ๊ฑด ์•ฝํ•œ nonce ๊ฐ€ ์•„๋‹ˆ๋ผ
433
+ # **๋ฏธ์ธก์ •**์ด๋‹ค. ๋‘ ๋ฒˆ์งธ ์ถœ์ฒ˜(openssl)๊นŒ์ง€ ์‹œ๋„ํ•˜๊ณ , ๊ทธ๊ฒƒ๋„ ์—†์œผ๋ฉด fail-closed.
434
+ [ -n "$_nonce" ] || _nonce=$(openssl rand -hex 12 2>/dev/null | tr -cd 'A-Za-z0-9')
435
+ if [ -z "$_nonce" ]; then
436
+ echo " โŒ INSTRUMENT DEAD โ€” no kernel entropy source (/dev/urandom, openssl); cannot mint an unguessable nonce." >&2
437
+ echo " A guessable nonce is not a weaker probe, it is NO probe โ€” a producer can replay it." >&2
438
+ rm -rf "$tmpd" 2>/dev/null || :
439
+ return 1
440
+ fi
293
441
  # Round 4 refuted the round-3 fix: `|| :` does NOT rescue an assignment to a readonly variable โ€”
294
442
  # bash aborts the shell before the `||` is ever considered. So the only safe move is to REFUSE
295
443
  # before mutating anything. Detected, reported as DEAD, and the caller survives.
296
- _canary=$(printf 'HIGH\tPSA_SELFTEST_CANARY')
444
+ _canary=$(printf 'HIGH\tPSA_SELFTEST_CANARY_%s' "$_nonce")
297
445
  if ! _psa_can_assign PSA_STREAM "$_canary" || ! _psa_can_assign PSA_ALLOWLIST /dev/null; then
298
446
  echo " โŒ INSTRUMENT DEAD โ€” PSA_STREAM/PSA_ALLOWLIST cannot take the values the self-test needs." >&2
299
447
  echo " (readonly, or an attribute such as \`declare -i\` that rejects the value)" >&2
@@ -303,9 +451,47 @@ psa_require_live() {
303
451
  fi
304
452
  PSA_STREAM="$_canary"
305
453
  PSA_ALLOWLIST=/dev/null
306
- printf 'PSA_SELFTEST_CANARY\n' > "$tmpd/canary" 2>/dev/null
307
- # Same pipeline shape as psa_scan_file: awk tags the file, psa_scan_tagged matches it.
308
- if out=$(awk -v P="psa/selftest" '{print P "\t" $0}' "$tmpd/canary" 2>&1 | psa_scan_tagged 2>&1); then
454
+ printf 'PSA_SELFTEST_CANARY_%s\n' "$_nonce" > "$tmpd/canary" 2>/dev/null
455
+ # ๐ŸŸฅ ์ƒ์‚ฐ์ž์™€ ๋งค์ฒ˜๋ฅผ **๋ถ„๋ฆฌํ•ด์„œ** ์ƒํƒœ๋ฅผ ๊ฐ๊ฐ ๋ณธ๋‹ค (cross-family ๋ฐ˜๋ก€ 4, 2026-08-21).
456
+ # ์ดˆํŒ์€ `awk โ€ฆ | psa_scan_tagged` ํ•œ ํŒŒ์ดํ”„์˜ ์ตœ์ข… rc ๋งŒ ๋ดค๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด
457
+ # **ยซ์ƒ์‚ฐ์ž ์‹คํŒจ์˜ rc=1ยป** ๊ณผ **ยซ์Šค์บ๋„ˆ๊ฐ€ ์œ ์ถœ์„ ์ฐพ์Œ์˜ rc=1ยป** ์ด ๊ฐ™์€ ๊ฐ’์œผ๋กœ ํ•ฉ์ณ์ง„๋‹ค.
458
+ # ์‹ค์ธก ์œ„์กฐ: ์‹คํŒจํ•˜๋Š” `awk` ๊ฐ€ ์นด๋‚˜๋ฆฌ์•„ ํ–‰์„ ์ธ์‡„ํ•˜๊ณ  `return 1` ํ•˜๊ฒŒ ๋งŒ๋“ค๋ฉด
459
+ # ์–‘ ์…ธ์—์„œ `psa_require_live` ๊ฐ€ **rc=0(์‚ด์•„์žˆ์Œ)** ์„ ๋ƒˆ๋‹ค. ์ฆ‰ ์ž๊ฐ€๊ฒ€์‚ฌ ํŒ์ • ์ž์ฒด๊ฐ€
460
+ # ์œ„์กฐ ๊ฐ€๋Šฅํ–ˆ๋‹ค. ํƒœ๊น… ๊ฒฐ๊ณผ๋ฅผ ๋จผ์ € **๋ฌผ์งˆํ™”**ํ•˜๊ณ  ๊ทธ rc ๋ฅผ ๋”ฐ๋กœ ํ™•์ธํ•œ๋‹ค.
461
+ # ๐ŸŸฅ P3: `x=$(cmd); rc=$?` ๋Š” `set -e` ์•„๋ž˜์„œ **์…ธ์„ ์ฆ‰์‹œ ์ฃฝ์ธ๋‹ค** โ€” ์•„๋ž˜ ๋ณต์›๋ฌธ๊ณผ
462
+ # `rm -rf` ์— ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•˜๊ณ  PSA_STREAM ์ด **์นด๋‚˜๋ฆฌ์•„์ธ ์ฑ„๋กœ ๋‚จ๋Š”๋‹ค**(์‹ค์ธก, bash).
463
+ # ์กฐ๊ฑด๋ฌธ ์•ˆ์˜ ๋Œ€์ž…์€ errexit ๋ฉด์ œ๋‹ค.
464
+ # ๐ŸŸฅ R4-3: `$(...)` ๋Š” **ํ›„ํ–‰ ๊ฐœํ–‰์„ ์ง€์šด๋‹ค** โ€” ์ •๋‹ต ์ค„ ๋’ค์— ๋นˆ ์ค„์„ ์•„๋ฌด๋ฆฌ ๋ถ™์—ฌ๋„
465
+ # ๋ฌธ์ž์—ด ๋น„๊ต๊ฐ€ ํ†ต๊ณผํ–ˆ๋‹ค(์‹ค์ธก rc=0). ใ€Œ์ •ํ™•ํ•œ ๋ฌธ์ž์—ด ๋Œ€์กฐใ€๋ผ๊ณ  ์ ์–ด๋†“๊ณ  ์ •ํ™•ํ•˜์ง€ ์•Š์•˜๋‹ค.
466
+ # ์ƒ์‚ฐ์ž ์ถœ๋ ฅ์„ **ํŒŒ์ผ๋กœ ๋ฌผ์งˆํ™”**ํ•ด์„œ `cmp` ๋กœ ๋ฐ”์ดํŠธ ๋น„๊ตํ•œ๋‹ค. stderr ๋Š” ๋”ฐ๋กœ ๋ฐ›๋Š”๋‹ค.
467
+ if awk -v P="psa/selftest" '{print P "\t" $0}' "$tmpd/canary" > "$tmpd/tagged" 2>"$tmpd/err"; then _tag_rc=0; else _tag_rc=$?; fi
468
+ # ๐ŸŸฅ P1: ์ƒํƒœ(rc)๋งŒ์ด ์•„๋‹ˆ๋ผ **๋‚ด์šฉ**์„ ๋Œ€์กฐํ•œ๋‹ค. ์ƒ์‚ฐ์ž๊ฐ€ ยซ์ž…๋ ฅ์„ ์ฝ์–ด์„œ ํƒœ๊น…ํ–ˆ๋Š”๊ฐ€ยป ๋Š”
469
+ # rc ๋กœ ์•Œ ์ˆ˜ ์—†๊ณ , nonce ๋ฅผ ํฌํ•จํ•œ ์ •ํ™•ํ•œ ๋ฌธ์ž์—ด ์ผ์น˜๋กœ๋งŒ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
470
+ # ๐ŸŸฅ R5-2: ์ดˆํŒ์€ ๊ธฐ๋Œ€๊ฐ’๋„ `$tmpd/expect` ํŒŒ์ผ์— ๋’€๋Š”๋ฐ, **์ƒ์‚ฐ์ž๋Š” `$tmpd/canary` ๋ฅผ ์ธ์ž๋กœ
471
+ # ๋ฐ›์œผ๋ฏ€๋กœ `$tmpd` ๋ฅผ ์•Œ๊ณ  expect ๋ฅผ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ๋‹ค** โ€” ํ‹€๋ฆฐ ๋ฐ”์ดํŠธ๋ฅผ ์–‘์ชฝ์— ์จ๋„ฃ์œผ๋ฉด
472
+ # `cmp` ๊ฐ€ ํ†ต๊ณผํ–ˆ๋‹ค(์–‘ ์…ธ ์‹ค์ธก rc=0). ๊ธฐ๋Œ€๊ฐ’์€ **ํ”„๋กœ์„ธ์Šค ์•ˆ์—๋งŒ** ๋‘๊ณ  ํŒŒ์ดํ”„๋กœ ๋จน์ธ๋‹ค.
473
+ # (`cmp file -` ๋Š” stdin ์„ ๋‘ ๋ฒˆ์งธ ํ”ผ์—ฐ์‚ฐ์ž๋กœ ์ฝ๋Š”๋‹ค. cmp ๋ถ€์žฌ๋Š” rcโ‰ 0 โ†’ fail-closed, ์‹ค์ธก.)
474
+ # ๐ŸŸฅ R6-1: `cmp` ๋ฅผ ์…ธ์ด ํ•ด์„ํ•˜๋ฉด **ํ•จ์ˆ˜๋กœ ๊ฐ€๋ ค์ง„๋‹ค** โ€” `cmp(){ return 0; }` ํ•˜๋‚˜๋กœ ๋Œ€์กฐ๊ฐ€
475
+ # ํ†ต์งธ๋กœ ๋ฌด๋ ฅํ™”๋๋‹ค(์–‘ ์…ธ ์‹ค์ธก rc=0). `command` ๋Š” ํ•จ์ˆ˜๋ฅผ ๊ฑด๋„ˆ๋›ด๋‹ค.
476
+ # ๐ŸŸฅ R6-2: ์ƒ์‚ฐ์ž๋Š” `$tmpd` ๋ฅผ ์•Œ๋ฏ€๋กœ stdout ์—๋Š” ํ‹€๋ฆฐ ๋ฐ”์ดํŠธ๋ฅผ ์“ฐ๊ณ  `tagged` ๋ฅผ **์ •๋‹ต ํŒŒ์ผ๋กœ
477
+ # ๊ฐ€๋Š” ์‹ฌ๋งํฌ๋กœ ๋ฐ”๊ฟ”์น˜๊ธฐ**ํ•  ์ˆ˜ ์žˆ๋‹ค(์–‘ ์…ธ ์‹ค์ธก rc=0). ๋น„๊ต ๋Œ€์ƒ์ด ยซawk ๊ฐ€ ์‹ค์ œ๋กœ ์“ด ํŒŒ์ผยป
478
+ # ์ธ์ง€ ๋จผ์ € ํ™•์ธํ•œ๋‹ค โ€” ์‹ฌ๋งํฌ๊ฐ€ ์•„๋‹Œ ๋ณดํ†ต ํŒŒ์ผ์ด์–ด์•ผ ํ•œ๋‹ค.
479
+ if [ -L "$tmpd/tagged" ] || [ ! -f "$tmpd/tagged" ]; then
480
+ rm -rf "$tmpd" 2>/dev/null || :
481
+ if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
482
+ if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
483
+ echo " โŒ INSTRUMENT DEAD โ€” the self-test output was replaced (symlink or non-regular file)." >&2
484
+ return 1
485
+ fi
486
+ if [ "$_tag_rc" -ne 0 ] || ! printf 'psa/selftest\tPSA_SELFTEST_CANARY_%s\n' "$_nonce" | command cmp -s "$tmpd/tagged" -; then
487
+ rm -rf "$tmpd" 2>/dev/null || :
488
+ if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
489
+ if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
490
+ echo " โŒ INSTRUMENT DEAD โ€” the self-test PRODUCER did not tag this run's nonce (rc=$_tag_rc)." >&2
491
+ echo " A canary printed by a failing โ€” or input-ignoring โ€” producer is not evidence the matcher ran." >&2
492
+ return 1
493
+ fi
494
+ if out=$(psa_scan_tagged < "$tmpd/tagged" 2>&1); then
309
495
  rc=0
310
496
  else
311
497
  rc=$?
@@ -317,7 +503,7 @@ psa_require_live() {
317
503
  if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
318
504
  if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
319
505
  case "$out" in
320
- *PSA_SELFTEST_CANARY*) [ "$rc" -eq 1 ] && return 0 ;;
506
+ *"PSA_SELFTEST_CANARY_$_nonce"*) [ "$rc" -eq 1 ] && return 0 ;;
321
507
  esac
322
508
  echo " โŒ INSTRUMENT DEAD โ€” the scanner did not report a synthetic known-positive." >&2
323
509
  echo " self-test rc=$rc out=[$out]" >&2