@chrono-meta/fh-gate 1.4.99 → 2.0.1

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 (41) hide show
  1. package/.claude/capabilities/adapters/gstack-content-safety.cap +110 -0
  2. package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
  3. package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
  4. package/.claude/capabilities/degrade-direction-scan.cap +24 -0
  5. package/.claude/capabilities/public-surface-scan.cap +24 -0
  6. package/.claude-plugin/marketplace.json +3 -3
  7. package/AGENTS.md +9 -3
  8. package/CHEATSHEET.md +17 -1
  9. package/CLAUDE.md +2 -2
  10. package/README.ja.md +55 -20
  11. package/README.ko.md +86 -20
  12. package/README.md +57 -20
  13. package/README.zh.md +49 -19
  14. package/knowledge/shared/harness-core/fh_three_layer_canon.md +96 -5
  15. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +36 -0
  16. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +1 -1
  17. package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +214 -0
  18. package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
  19. package/knowledge/shared/harness-core/ship_readiness_gate.md +29 -10
  20. package/knowledge/shared/learnings/subagent_invocations_log.yaml +73 -0
  21. package/package.json +13 -3
  22. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  23. package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
  24. package/plugins/fh-meta/CHANGELOG.md +45 -0
  25. package/scripts/adapters/gstack_content_safety.sh +186 -0
  26. package/scripts/adapters/mate_agent_boundary.sh +120 -0
  27. package/scripts/adapters/peer_resolve.sh +99 -0
  28. package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
  29. package/scripts/branch_claim.sh +14 -0
  30. package/scripts/capability_effect_probe.sh +701 -0
  31. package/scripts/capability_registry_check.sh +313 -16
  32. package/scripts/cluster_capability_scan.sh +629 -0
  33. package/scripts/fh_session_load.sh +70 -0
  34. package/scripts/portability_lint.sh +257 -0
  35. package/scripts/publish_freshness_check.sh +204 -0
  36. package/scripts/relay_channel.sh +70 -7
  37. package/scripts/selfcheck.sh +166 -1
  38. package/scripts/test_adapter_lanes.sh +296 -0
  39. package/scripts/test_capability_entrypoint_shipping.sh +83 -6
  40. package/scripts/test_relay_channel_lanes.sh +76 -0
  41. package/templates/.git-hooks/pre-commit +14 -2
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "fh-meta",
3
- "version": "1.4.99",
3
+ "version": "2.0.1",
4
4
  "engines": {
5
5
  "claudeCode": ">=1.0.0"
6
6
  },
7
- "description": "New in 1.4.74: shipped documents no longer point at files the package omits. 35 such paths were measured on 2026-07-28 — CLAUDE.md instructed consumers to run templates/predelete_check.sh before a destructive op and the file did not ship. 20 more paths added; the 4 that remain absent are enumerated with a written reason each (per-environment registries and operator-private sync). Closing the set once is worth little because it regrows, so scripts/package_coverage_check.sh now fails selfcheck on any new occurrence, known-pair calibrated, and it caught itself on first wiring. New in 1.4.73: the mechanical floor now ships — templates/.git-hooks (pre-commit/pre-push) plus every script they actually invoke, their regression anchors, and the .claude/rules data they read. Until now the package shipped the prose that tells you to run the gates while omitting the gates, and 35 shipped-doc references pointed at files the tarball did not contain. Verified by unpacking the tarball, installing the hooks in a scratch repo and committing through them, not by inspecting files[]. degrade_direction_scan gains shell probes: it collected .sh files that no probe could match and reported them clean (known-positive scored 0/4); git hooks were invisible entirely because the extension test ran against the path, not the basename. The field-propagated templates/ copy had drifted from scripts/ and is now byte-pinned by an anchor. Hub meta-engineering toolkit — 35 skills + 7 agents. New in 1.4.71: SKILL_detail.md brought inside the 4-axis gate — the gate matched the literal `SKILL.md`, which `SKILL_detail.md` does not contain, leaving 27.7% of the skill-spec surface ungated (measured; it leaked twice for real). Fix escalated from a name list to a directory scope after an enumeration sweep found a real uncovered file on its first run; anchored by scripts/gate_pathspec_check.sh (known-pair, wired into pre-commit). One-way mirror sync gains a destination-newer abort + mechanically injected mirror banner after two session cards were silently overwritten, anchored by scripts/sync_guard_check.sh. dialogue-harvest gains Step 4-b cross-corpus provenance (absorbed / held-unused / declined) for single-author corpora, where the original provenance labels were free and measured nothing. frontier-digest arxiv leg category-scoped after a relevance drift the staleness rule structurally could not catch (known-pair calibration recorded). templates/CLAUDE.md hub paths switched to {FH_ROOT} so a copied harness stops carrying one operator's absolute paths into org-visible checkouts. New in 1.4.70: dialogue-harvest (mines AI-dialogue logs — sycophancy strip first, induced-vs-independent provenance labeling, EN+KO known-pair calibration shipped); asset-placement-gate Step 0.6 official-corpora check (Claude Cookbook as Tier-0 consult corpus, judged-flag semantics); context-doctor built-in-/doctor-first sister anchor. New in 1.4.48: phantom-quench + steel-quench gain external frontier anchors (arXiv:2607.02052 package-hallucination; arXiv:2607.02057 prompt-coverage-adequacy); README model-flat claim reframed from a per-release point-curve to structural invariants (operation flattens across tiers; depth tier-order fixed within a generation). New in 1.4.47: onboarding step ① surfaces the Mode D companion-store session-start load in the auto-read salience anchor (previously only in the local binding + rules, so a greeting could skip the load). New in 1.4.46: context-doctor gains a command-output axis — routes to a command-output proxy/hook (rtk) to trim verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce environments (lossy filtering, off gate-input paths). New in 1.4.41: context-doctor 2026 trigger vocab (context engineering/rot/collapse) + phantom-citation hardening; hub measurement-integrity-checklist (cross-model measurement pre-flight: display-name pin/reps≥3/discriminating probe). New in 1.4.40: install-wizard scaffolds the companion store as a queryable wiki (INDEX + session-start read + Raw/Wiki/Conversation ingest axis). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment, calibration-gated) + video-ingest (capability-routed video ingestion). New in 1.4.37: corpus-grounding-expander + persona-roster-expander (field-harvested verbatim-relay capability skills). New in 1.3.0: public-surface-audit (git-tracked private-token leak scan), field-harvest Mode B session-end auto-trigger, 4-axis gate scope extension (docs/ + AGENTS.md). New in 1.2.0: pipeline-conductor (4-pipeline gated sweep), return-path-gate (chain closure audit), goal-quench (Stop hook + quality gate), steel-quench Wave 5 (multi-model sidecar challenger), 2-layer architecture docs, YAML validation script. Validated cross-CLI: Claude Code, Codex, Gemini.",
7
+ "description": "New in 2.0.1: harness-doctor cadence promoted from prose to a SessionStart hook (30-day overdue check no longer silent); portability lint (BSD/GNU, zsh/bash pattern denylist) wired into pre-commit as the shell-layer sibling of the existing lints; branch_claim.sh gains an explicit warning that live-claim count is not a same-tree concurrent-edit count; the public-surface confidentiality scan's fail-open notice (operator pattern override absent) is now a hard-to-miss banner instead of a two-line aside; fh-gate.sh survives a missing package.json instead of dying under set -euo pipefail; identity ① (harness cluster) reclassified 🟢 under a redefined ⓐⓑⓒ bar (reuse is real and callable, FH did not grow to get it, sharpness is preserved via strictest-wins merge) — cross-harness adapters + relay_channel.sh --cap-args node-specific argument channel; §1-a-2's 6-axis verification canon synchronized across the file (a prior draft still said 4 in one place). New in 1.4.74: shipped documents no longer point at files the package omits. 35 such paths were measured on 2026-07-28 — CLAUDE.md instructed consumers to run templates/predelete_check.sh before a destructive op and the file did not ship. 20 more paths added; the 4 that remain absent are enumerated with a written reason each (per-environment registries and operator-private sync). Closing the set once is worth little because it regrows, so scripts/package_coverage_check.sh now fails selfcheck on any new occurrence, known-pair calibrated, and it caught itself on first wiring. New in 1.4.73: the mechanical floor now ships — templates/.git-hooks (pre-commit/pre-push) plus every script they actually invoke, their regression anchors, and the .claude/rules data they read. Until now the package shipped the prose that tells you to run the gates while omitting the gates, and 35 shipped-doc references pointed at files the tarball did not contain. Verified by unpacking the tarball, installing the hooks in a scratch repo and committing through them, not by inspecting files[]. degrade_direction_scan gains shell probes: it collected .sh files that no probe could match and reported them clean (known-positive scored 0/4); git hooks were invisible entirely because the extension test ran against the path, not the basename. The field-propagated templates/ copy had drifted from scripts/ and is now byte-pinned by an anchor. Hub meta-engineering toolkit — 35 skills + 7 agents. New in 1.4.71: SKILL_detail.md brought inside the 4-axis gate — the gate matched the literal `SKILL.md`, which `SKILL_detail.md` does not contain, leaving 27.7% of the skill-spec surface ungated (measured; it leaked twice for real). Fix escalated from a name list to a directory scope after an enumeration sweep found a real uncovered file on its first run; anchored by scripts/gate_pathspec_check.sh (known-pair, wired into pre-commit). One-way mirror sync gains a destination-newer abort + mechanically injected mirror banner after two session cards were silently overwritten, anchored by scripts/sync_guard_check.sh. dialogue-harvest gains Step 4-b cross-corpus provenance (absorbed / held-unused / declined) for single-author corpora, where the original provenance labels were free and measured nothing. frontier-digest arxiv leg category-scoped after a relevance drift the staleness rule structurally could not catch (known-pair calibration recorded). templates/CLAUDE.md hub paths switched to {FH_ROOT} so a copied harness stops carrying one operator's absolute paths into org-visible checkouts. New in 1.4.70: dialogue-harvest (mines AI-dialogue logs — sycophancy strip first, induced-vs-independent provenance labeling, EN+KO known-pair calibration shipped); asset-placement-gate Step 0.6 official-corpora check (Claude Cookbook as Tier-0 consult corpus, judged-flag semantics); context-doctor built-in-/doctor-first sister anchor. New in 1.4.48: phantom-quench + steel-quench gain external frontier anchors (arXiv:2607.02052 package-hallucination; arXiv:2607.02057 prompt-coverage-adequacy); README model-flat claim reframed from a per-release point-curve to structural invariants (operation flattens across tiers; depth tier-order fixed within a generation). New in 1.4.47: onboarding step ① surfaces the Mode D companion-store session-start load in the auto-read salience anchor (previously only in the local binding + rules, so a greeting could skip the load). New in 1.4.46: context-doctor gains a command-output axis — routes to a command-output proxy/hook (rtk) to trim verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce environments (lossy filtering, off gate-input paths). New in 1.4.41: context-doctor 2026 trigger vocab (context engineering/rot/collapse) + phantom-citation hardening; hub measurement-integrity-checklist (cross-model measurement pre-flight: display-name pin/reps≥3/discriminating probe). New in 1.4.40: install-wizard scaffolds the companion store as a queryable wiki (INDEX + session-start read + Raw/Wiki/Conversation ingest axis). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment, calibration-gated) + video-ingest (capability-routed video ingestion). New in 1.4.37: corpus-grounding-expander + persona-roster-expander (field-harvested verbatim-relay capability skills). New in 1.3.0: public-surface-audit (git-tracked private-token leak scan), field-harvest Mode B session-end auto-trigger, 4-axis gate scope extension (docs/ + AGENTS.md). New in 1.2.0: pipeline-conductor (4-pipeline gated sweep), return-path-gate (chain closure audit), goal-quench (Stop hook + quality gate), steel-quench Wave 5 (multi-model sidecar challenger), 2-layer architecture docs, YAML validation script. Validated cross-CLI: Claude Code, Codex, Gemini.",
8
8
  "author": {
9
9
  "name": "chrono-meta",
10
10
  "email": "chrono-meta@users.noreply.github.com"
@@ -10,6 +10,51 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
10
10
 
11
11
  ## Plugin Level
12
12
 
13
+ ### [2.0.1] — 2026-08-16
14
+
15
+ - **feat(cadence)**: `harness-doctor` 30일 캐던스가 산문 제안에서 SessionStart 훅으로 승격 —
16
+ 초과 상태가 더 이상 침묵으로 렌더되지 않는다.
17
+ - **feat(portability)**: BSD/GNU·zsh/bash 패턴 denylist 린트를 pre-commit에 배선 —
18
+ `degrade_direction_scan` 옆 셸 레이어 자매 레인, advisory(비차단).
19
+ - **fix(branch-claim)**: `branch_claim.sh show`가 live claim 수를 실제 동시편집 스레드 수처럼
20
+ 오독시키는 문제 — 헤더 주석 + 출력 자체에 경계선 명시.
21
+ - **fix(confidentiality)**: `.public-surface-patterns`(operator override) 부재 시 fail-open
22
+ 경고가 두 줄짜리 아쉬운 존재였던 것을 놓칠 수 없는 배너로 — 방향(PASS)은 그대로, 가시성만.
23
+ - **fix(fh-gate)**: `package.json` 부재 시 `set -euo pipefail` 아래서 즉사하던 결함 수리 —
24
+ known-pair 확정(수정본 rc=12 / package.json 있으면 rc=12 / HEAD 사본 rc=1).
25
+ - **feat(cluster)**: 정체성 ① 하네스 클러스터 🔵→🟢 — 기준을 dominance에서 재정의된 ⓐⓑⓒ로
26
+ 교체(운영자 결정). 크로스하네스 어댑터 + `relay_channel.sh --cap-args` 노드별 인자 채널.
27
+ 잔여 명시: (d) 등록 바가 선언의 진위를 검증하지 못하는 구조는 안 닫힘 — 별도 항목 추적.
28
+ - **docs(canon)**: 6축 검증 정본(§1-a-2) 표기가 파일 내 두 자리에서 어긋나 있던 것 동기화.
29
+
30
+ ### [2.0.0] — 2026-08-16
31
+
32
+ **BREAKING — M6 로 인해 종전 통과하던 capability 등록이 거부될 수 있다. 그리고 그 M6 는 1.4.99 에서 출하되지 않았다.**
33
+
34
+ - 🔴 **BREAKING: `capability_registry_check.sh` 에 M6(선언 진위 관측)가 실제로 도달한다.**
35
+ M1–M5 를 전부 통과하던 capfile 이 `writes:` 선언과 실제 부작용이 다르면 이제 **REJECTED** 된다.
36
+ major 인 이유는 이것 하나다 — 소비자의 초록이 빨강이 될 수 있다. `^1.4.x` 범위가 이 변경을 자동
37
+ 수용하지 않도록 minor 가 아니라 major 로 낸다. 🟥 **M6 를 opt-in 으로 낮추는 선택지는 거부했다**:
38
+ 비가역 표면(등록 → 이후 자동 호출)의 fail-closed 축을 끌 수 있게 만들면 그건 floor 가 아니다.
39
+ - 🔴 **fix: `scripts/capability_effect_probe.sh` 가 `files[]` 에 없어 출하되지 않았다.**
40
+ M6 를 배선한 커밋(`a48e644`)이 그 M6 가 호출하는 프로브를 출하 목록에 넣지 않았다. 실측 결과
41
+ 1.4.99 tarball 의 `capability_registry_check.sh` 에는 **M6 가 0회** 등장한다 — 소비자는 깨진 M6 가
42
+ 아니라 **M6 자체를 못 받았다**(출하본이 main 보다 뒤처져 있었다). 고치지 않고 재출하했다면 그때
43
+ 프로브 부재 → fail-closed → **모든 등록 거부**가 됐다.
44
+ - **feat: 형제 의존 출하 검사** — `test_capability_entrypoint_shipping.sh` 가 세 번째 방향을 얻었다.
45
+ 출하되는 셸 스크립트가 `${0%/*}/x.sh` 형태로 부르는 **형제 파일**도 `files[]` 에 있어야 한다.
46
+ 기존 두 방향은 `*_capability.sh` **진입점 규약**에만 걸려 있어 이번 결함에 구조적으로 눈이 멀었고,
47
+ `package_coverage_check.sh` 는 참조 정규식이 `scripts/` 로 **시작하는 리터럴**만 봐서 런타임에
48
+ 조립되는 경로를 볼 수 없다. 이 클래스의 4번째 발생이라 앞단에 기계로 세웠다.
49
+ - **fix: script-dir 해석이 symlink 를 통과한다** — `${0%/*}` 는 심링크 경유 호출에서 링크의
50
+ 디렉토리를 가리켜 M6 가 «프로브 부재» 로 fail-closed 했다(있는데 없다고 말한다). `BASH_SOURCE` +
51
+ symlink 해소로 교체.
52
+ - **feat: publish 신선도 게이트** — `prepublishOnly` 가 «출하 트리가 커밋된 main 인가» 를 본다.
53
+ 이번 사고와 2026-08-13 의 «남의 미커밋 초안 출하» 가 같은 표면의 두 발생이다.
54
+ - **fix: `sync-to-be.sh` 락 경합 경로가 rc=64 로 죽었다** — `log()` 가 호출부보다 아래에 정의돼
55
+ macOS `/usr/bin/log` 로 떨어졌고, `set -euo pipefail` 이 의도한 `exit 0` 도착 전에 스크립트를
56
+ 죽였다. 경합 경로는 **경합할 때만** 실행되므로 병렬 세션 둘이 붙기 전까지 출하된 채로 있었다.
57
+
13
58
  ### [1.4.99] — 2026-08-15
14
59
 
15
60
  **fix: 검출기가 «호출부 0» 을 «10/10 초록» 으로 보고하고 있었다 — 그리고 이 릴리스는 #388 의 첫 출하이기도 하다**
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bash
2
+ # gstack_content_safety.sh — FH 소유 **어댑터**. peer `gstack` 의 redact 엔진을
3
+ # 클러스터 호출 가능한 typed capability 로 감싼다.
4
+ #
5
+ # ─────────────────────────────────────────────────────────────────────────────
6
+ # 어댑터가 왜 FH 안에 사나 (운영자 결정 2026-08-16)
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+ # 「없는 능력을 남의 하네스에서 호출해 쓴다」가 크로스하네스의 요점이고, 그 «쓰는 쪽»은
9
+ # FH 다. peer 레포 안에 선언을 심는 것은 **그 하네스를 소유하고 동시에 노출을 결정한**
10
+ # 특권적 경우이고, 일반 경로가 아니다. 그래서 선언과 래퍼가 여기 산다.
11
+ # gstack 레포에도 같은 목적의 래퍼(`scripts/content-safety-capability.sh`)가 있다 —
12
+ # 이 파일은 그것의 **FH 측 이식본**이다(로직·enum·근거를 옮겼다). peer 가 그 래퍼를
13
+ # 갖고 있든 아니든 돌게 하려고 엔진(`bin/gstack-redact`)을 직접 부른다.
14
+ #
15
+ # ─────────────────────────────────────────────────────────────────────────────
16
+ # 왜 `bin/gstack-redact` 를 그냥 선언하지 않나 (gstack 래퍼 헤더에서 옮겨온 근거)
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+ # 그 CLI 의 exit 은 {0 clean, 2 MEDIUM, 3 HIGH} 에 «git 레포가 아니다» 용 1 이 붙는다.
19
+ # **「아무것도 안 쟀다」를 뜻하는 값이 없다** — 빈 파일도, 스캔 대상이 0개인 디렉토리도,
20
+ # 인터프리터 부재도 전부 exit 0 = CLEAN 으로 렌더된다. 「PASS 가 no-op 과 구분 불가」는
21
+ # 등록 바가 정확히 거부하는 형태다(§ⓑ.4 B1).
22
+ # CLI 의 코드를 바꾸는 선택지는 **일부러 버렸다** — gstack 의 다른 스킬과 pre-push 훅이
23
+ # 그 코드를 소비한다. 설명하는 행위로 남의 레포 계약을 조용히 바꾸지 않는다.
24
+ #
25
+ # ─────────────────────────────────────────────────────────────────────────────
26
+ # VERDICT ENUM (선언과 정확히 일치해야 한다)
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+ # 0 CLEAN 실제 내용 1바이트 이상을 쟀고 HIGH·MEDIUM 둘 다 없다
29
+ # 1 BLOCK_HIGH HIGH 등급 findings 있음
30
+ # 2 REVIEW_MEDIUM MEDIUM 만 있음 — 자동 차단이 아니라 사람 판단
31
+ # 3 NO_TARGET 잴 게 없었다(빈 파일 · 후보 0개 디렉토리)
32
+ # *** CLEAN 이 아니다 — 스캔이 시작조차 안 했다 ***
33
+ # 10 HARNESS_ERROR 계기가 없거나 깨졌다(bun 부재 · CLI/엔진 부재 · 대상 불가독 ·
34
+ # 열거 실패 · 하위 CLI 가 자기 계약 밖 코드 반환)
35
+ # 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
36
+ #
37
+ # 🟥 셋은 서로 다른 명제다 — 접으면 안 된다:
38
+ # PEER_ABSENT = 그 노드가 이 머신에 없다 (클러스터에 대한 진술)
39
+ # HARNESS_ERROR = 노드는 있는데 계기가 깨졌다 (계기에 대한 진술)
40
+ # NO_TARGET = 계기는 멀쩡한데 잴 대상이 없다 (대상에 대한 진술)
41
+ # `verdict_binding` 에 셋 다 들어간다. 미측정이 통과로 렌더되면 이 어댑터는 장식이다.
42
+ #
43
+ # ⚠️ 하위 계기의 exit 을 **그대로 전파하지 않는다.** 아래 case 가 명시 재매핑이다
44
+ # (같은 숫자, 다른 사건 — gstack 의 3=HIGH 는 여기서 1=BLOCK_HIGH 다).
45
+ #
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # CLEANUP-TRAP 규율 (물려받은 실패 — 가정이 아니다)
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+ # 자매 하네스의 첫 capability 래퍼가 자기 레포의 `scripts/` 를 지웠다:
50
+ # `TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT` 를 걸고 다른 분기에서 $TMP 를 실제
51
+ # 레포 경로로 **재대입**했다. 트랩은 «만든 것» 이 아니라 «지금 가리키는 것» 을 지운다.
52
+ # 구조 처방: OWNED_TMP(내 것, 지운다)와 TARGET(재는 것, 절대 안 지운다)을 다른 변수로
53
+ # 두고, OWNED_TMP 는 최초 1회만 대입하며, 트랩이 임시경로인지 한 번 더 검문한다.
54
+ #
55
+ # 사용법
56
+ # gstack_content_safety.sh --target <file|dir>
57
+ # gstack_content_safety.sh --known-positive # 캘리브레이션 arm (expect BLOCK_HIGH)
58
+ # gstack_content_safety.sh --known-negative # 캘리브레이션 arm (expect CLEAN)
59
+ set -o pipefail
60
+
61
+ PEER_NAME="gstack"
62
+ RC_CLEAN=0; RC_BLOCK=1; RC_REVIEW=2; RC_NO_TARGET=3; RC_HARNESS=10; RC_PEER_ABSENT=20
63
+
64
+ _harness_error() { printf 'gstack-adapter: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
65
+ _peer_absent() { printf 'gstack-adapter: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
66
+
67
+ # ── peer 해석 — 절대경로를 이 파일에 싣지 않는다 ─────────────────────────────
68
+ # tracked 파일에 운영자 홈 절대경로가 들어가면 그건 공개 표면 유출이다.
69
+ _LIB="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/peer_resolve.sh"
70
+ [ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
71
+ # shellcheck source=/dev/null
72
+ . "$_LIB"
73
+
74
+ PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
75
+ case "$_rrc" in
76
+ 0) ;;
77
+ 1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(깨끗한 게 아니다)" ;;
78
+ 2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
79
+ *) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
80
+ esac
81
+
82
+ CLI="$PEER_ROOT/bin/gstack-redact"
83
+ ENGINE="$PEER_ROOT/lib/redact-engine.ts"
84
+
85
+ # ── 계기 전제 (계기의 부재는 결코 «깨끗함» 이 아니다) ────────────────────────
86
+ # peer 레포는 있는데 그 안의 엔진이 없다 = 계기 파손이지 노드 부재가 아니다.
87
+ [ -r "$CLI" ] || _harness_error "스캐너 CLI 부재: $CLI (peer 는 있으나 계기가 없다)"
88
+ [ -r "$ENGINE" ] || _harness_error "스캔 엔진 부재: $ENGINE"
89
+ command -v bun >/dev/null 2>&1 || _harness_error "bun 이 PATH 에 없다 — 스캐너를 돌릴 수 없다(깨끗한 결과가 아니다)"
90
+ command -v find >/dev/null 2>&1 || _harness_error "find 부재 — 대상을 열거할 수 없다(열거 불가는 부재가 아니다)"
91
+
92
+ OWNED_TMP="$(mktemp -d)" || _harness_error "mktemp 실패" # 1회 대입. 절대 재대입 금지.
93
+ _cleanup() {
94
+ case "$OWNED_TMP" in
95
+ /tmp/*|/var/folders/*|/private/var/folders/*)
96
+ [ -d "$OWNED_TMP" ] && rm -rf "$OWNED_TMP" ;;
97
+ *) printf 'gstack-adapter: cleanup 거부 — OWNED_TMP 가 임시경로가 아니다: %s\n' "$OWNED_TMP" >&2 ;;
98
+ esac
99
+ }
100
+ trap _cleanup EXIT
101
+
102
+ TARGET="" # 실제 레포 내용을 가리킬 수 있다. 절대 안 지우고 rm 에 넘기지 않는다.
103
+
104
+ case "${1:-}" in
105
+ --known-positive)
106
+ # 리터럴을 printf 인자로 **쪼갠다** — 이 소스 자신이 자격증명 형태 스캐너에 걸리면
107
+ # 픽스처가 자기 레포를 발행 불가로 만든다.
108
+ TARGET="$OWNED_TMP/known_positive.txt"
109
+ {
110
+ printf 'deploy notes\n'
111
+ printf 'aws_access_key_id=%s%s\n' 'AKIA' '1234567890ABCDEF'
112
+ printf 'contact ssn %s-%s-%s for payroll\n' '123' '45' '6789'
113
+ } > "$TARGET" || _harness_error "known-positive 픽스처를 쓸 수 없다"
114
+ ;;
115
+ --known-negative)
116
+ # 같은 형태·같은 크기대, 자격증명도 PII 도 없다. 컨트롤이 있어야 PASS 가
117
+ # 「이 계기가 가른다」이지 「이 계기가 조용하다」가 아니게 된다.
118
+ TARGET="$OWNED_TMP/known_negative.txt"
119
+ {
120
+ printf 'deploy notes\n'
121
+ printf 'the build pipeline reads its credentials from the platform keystore\n'
122
+ printf 'contact the payroll team through the internal ticket queue\n'
123
+ } > "$TARGET" || _harness_error "known-negative 픽스처를 쓸 수 없다"
124
+ ;;
125
+ --target)
126
+ TARGET="${2:-}"
127
+ [ -n "$TARGET" ] || _harness_error "--target 에 경로가 필요하다"
128
+ [ -e "$TARGET" ] || _harness_error "--target 경로가 없다: $TARGET (부재 대상은 빈 대상이 아니다)"
129
+ [ -r "$TARGET" ] || _harness_error "--target 경로를 읽을 수 없다: $TARGET"
130
+ ;;
131
+ '') _harness_error "대상 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
132
+ *) _harness_error "알 수 없는 모드 '$1'" ;;
133
+ esac
134
+
135
+ # ── 후보 열거 ────────────────────────────────────────────────────────────────
136
+ # 「열거 실패」와 「후보 0개」는 다른 사실이다. find 자체가 실패하면 대상을 **안 잰**
137
+ # 것이므로 HARNESS_ERROR 이지 NO_TARGET 이 아니다.
138
+ CAND_LIST="$OWNED_TMP/.candidates"
139
+ if [ -d "$TARGET" ]; then
140
+ _ferr=$(find -H "$TARGET" \( -name .git -o -name node_modules -o -name dist \) -prune -o \
141
+ -type f -print 2>&1 >"$CAND_LIST")
142
+ _frc=$?
143
+ if [ "$_frc" -ne 0 ] || [ -n "$_ferr" ]; then
144
+ _harness_error "대상 열거 실패(부재 대상이 아니다): ${_ferr:-find exited $_frc}"
145
+ fi
146
+ else
147
+ printf '%s\n' "$TARGET" > "$CAND_LIST"
148
+ fi
149
+
150
+ SCANNED=0
151
+ WORST=0 # 심각도 순서: CLEAN < REVIEW_MEDIUM < BLOCK_HIGH (숫자 순서가 아니다)
152
+ _rank() { case "$1" in 0) echo 0 ;; 2) echo 1 ;; 1) echo 2 ;; *) echo 9 ;; esac; }
153
+
154
+ while IFS= read -r f; do
155
+ [ -n "$f" ] || continue
156
+ [ -f "$f" ] && [ -r "$f" ] || continue
157
+ LC_ALL=C grep -Iq . "$f" 2>/dev/null || continue # 바이너리는 대상이 아니다
158
+ [ -n "$(tr -d '[:space:]' < "$f" 2>/dev/null | head -c 1)" ] || continue # 공백뿐 = 내용 없음
159
+
160
+ # 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
161
+ out=$(cd "$PEER_ROOT" && bun "$CLI" --from-file "$f" 2>&1); rc=$?
162
+ case "$rc" in
163
+ 0|2|3) ;;
164
+ *) _harness_error "스캐너가 자기 계약 {0,2,3} 밖의 $rc 를 반환했다: $f" ;;
165
+ esac
166
+ SCANNED=$((SCANNED + 1))
167
+ # 명시 재매핑 — gstack 3=HIGH → 1 · 2=MEDIUM → 2 · 0 → 0
168
+ case "$rc" in 3) mapped=1 ;; 2) mapped=2 ;; *) mapped=0 ;; esac
169
+ if [ "$(_rank "$mapped")" -gt "$(_rank "$WORST")" ]; then WORST="$mapped"; fi
170
+ if [ "$mapped" != "0" ]; then
171
+ printf 'gstack-adapter: %s\n' "$f"
172
+ printf '%s\n' "$out" | sed -n '2,6p'
173
+ fi
174
+ done < "$CAND_LIST"
175
+
176
+ if [ "$SCANNED" -eq 0 ]; then
177
+ printf 'gstack-adapter: NO_TARGET — %s 아래 스캔 가능한 파일 0개 (CLEAN 이 아니다. 아무것도 안 쟀다)\n' "$TARGET"
178
+ exit "$RC_NO_TARGET"
179
+ fi
180
+
181
+ case "$WORST" in
182
+ 0) printf 'gstack-adapter: CLEAN — %d개 파일 스캔, HIGH·MEDIUM 없음\n' "$SCANNED"; exit "$RC_CLEAN" ;;
183
+ 2) printf 'gstack-adapter: REVIEW_MEDIUM — %d개 파일 스캔, MEDIUM findings 있음\n' "$SCANNED"; exit "$RC_REVIEW" ;;
184
+ 1) printf 'gstack-adapter: BLOCK_HIGH — %d개 파일 스캔, HIGH findings 있음\n' "$SCANNED"; exit "$RC_BLOCK" ;;
185
+ *) _harness_error "내부 심각도 집계가 '$WORST' 를 냈다" ;;
186
+ esac
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ # mate_agent_boundary.sh — FH 소유 **어댑터**. peer `mate` 의 기존 회귀 앵커
3
+ # `scripts/blackbox_invariant_check.sh` 를 클러스터 호출 가능한 typed capability 로 감싼다.
4
+ #
5
+ # ─────────────────────────────────────────────────────────────────────────────
6
+ # 무엇을 재나
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+ # 에이전트 명세서가 **정보경계 불변식을 여전히 담고 있는가**. peer 쪽 앵커는 산문 규칙
9
+ # 11종(입력 어휘 allowlist · 출처 기준 금지 · 도구 무관 선언 · 신뢰도 상한 · 메모리 쓰기
10
+ # 금지 …)이 편집 한 번으로 조용히 되돌아가는 것을 막는다. 그 스크립트는 **peer 소유 코드**이고
11
+ # 이 어댑터가 만든 것이 아니다 — 여기서 하는 일은 호출과 enum 재매핑뿐이다.
12
+ #
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
+ # 어댑터가 왜 FH 안에 사나 (운영자 결정 2026-08-16)
15
+ # ─────────────────────────────────────────────────────────────────────────────
16
+ # 「없는 능력을 남의 하네스에서 호출해 쓴다」의 «쓰는 쪽»이 FH 다. peer 레포 안에 선언을
17
+ # 심는 것은 그 하네스를 소유하고 **동시에 노출을 결정한** 특권적 경우이지 일반 경로가 아니다.
18
+ #
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ # VERDICT ENUM (선언과 정확히 일치해야 한다)
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ # 0 PASS 전건 통과 — 불변식이 대상 문서에 살아 있다
23
+ # 1 FAIL 회귀 — 불변식 문장 중 하나 이상이 사라졌다
24
+ # 10 HARNESS_ERROR 계기가 없거나 대상을 못 읽었다
25
+ # 20 PEER_ABSENT **peer 하네스가 이 머신에 없다**
26
+ #
27
+ # 🟥 peer 스크립트 헤더가 이미 못 박아 뒀다 — *"exit 10 은 «위반 0» 이 아니다. 대상을 못 읽은
28
+ # 것이므로 검사가 시작조차 안 한 상태다."* 어댑터는 그 위에 **한 층 더** 구분을 얹는다:
29
+ # PEER_ABSENT = 그 하네스가 이 머신에 없다 (클러스터에 대한 진술)
30
+ # HARNESS_ERROR = 하네스는 있는데 못 쟀다 (계기/대상에 대한 진술)
31
+ # 둘을 접으면 「이 머신에 mate 를 안 깔았다」가 「mate 가 깨졌다」와 같은 얼굴이 된다.
32
+ # `verdict_binding` 에 PASS 를 뺀 전부가 들어간다 — 미측정을 통과로 렌더하지 않는다.
33
+ #
34
+ # ⚠️ 하위 계기의 exit 을 그대로 전파하지 않는다. 아래 case 가 명시 재매핑이다.
35
+ # 지금은 0→0 · 1→1 로 숫자가 같지만 **그건 우연이고 계약이 아니다** — peer 가 코드를
36
+ # 바꾸면 여기 case 가 갈라진다. 계약 밖 exit 은 PASS 로 흘리지 않고 HARNESS_ERROR 다.
37
+ #
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+ # 픽스처 — **어댑터가 자기 픽스처를 갖는다**
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ # `scripts/adapters/fixtures/mate_agent_boundary_known_{positive,negative}.md`.
42
+ # peer 레포의 같은 이름 파일에서 바이트 동일하게 복사했다(원본: peer 의
43
+ # `scripts/fixtures/agent_boundary_known_*.md`).
44
+ # ★왜 peer 의 실물 명세서를 arm 으로 안 쓰나: 실물은 팀이 계속 고치는 살아있는 문서라
45
+ # 그것을 양성 arm 으로 쓰면 이 어댑터의 판정이 **남의 편집에 따라 조용히 뒤집힌다**
46
+ # (감사 대상은 얼려야 한다). 실물은 인자 없는 기본 호출로 계속 검사된다.
47
+ # ★잔여(명명): 복사본이라 **드리프트한다.** peer 가 불변식을 늘리면 이 사본은 낡는다.
48
+ # 대가를 알고 고른 것이다 — peer 트리를 실행 시점에 읽으면 계기가 남의 편집에 흔들리고,
49
+ # 그건 캘리브레이션 arm 이 가져서는 안 되는 성질이다. 정합성 확인은 손으로 한다.
50
+ # ★residency: 두 픽스처는 **합성**이고 조직 식별자를 담지 않는다 — 이 레포에 tracked 로
51
+ # 들어오기 전에 손으로 전문을 읽고, peer 자신의 식별자 denylist grep(컨트롤 포함) ·
52
+ # 경로/URL/이메일/IP grep(컨트롤 포함) · 라틴 토큰 전수 열거로 확인했다.
53
+ #
54
+ # 사용법
55
+ # mate_agent_boundary.sh --target <file> # 임의 명세서 파일
56
+ # mate_agent_boundary.sh --peer-default # peer 의 기본 대상(실물 명세서)
57
+ # mate_agent_boundary.sh --known-positive # 캘리브레이션 arm (expect PASS)
58
+ # mate_agent_boundary.sh --known-negative # 캘리브레이션 arm (expect FAIL)
59
+ set -o pipefail
60
+
61
+ PEER_NAME="mate"
62
+ RC_PASS=0; RC_FAIL=1; RC_HARNESS=10; RC_PEER_ABSENT=20
63
+
64
+ _harness_error() { printf 'mate-adapter: HARNESS_ERROR — %s\n' "$1" >&2; exit "$RC_HARNESS"; }
65
+ _peer_absent() { printf 'mate-adapter: PEER_ABSENT — %s\n' "$1" >&2; exit "$RC_PEER_ABSENT"; }
66
+
67
+ SELF_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
68
+ _LIB="$SELF_DIR/peer_resolve.sh"
69
+ [ -r "$_LIB" ] || _harness_error "peer 해석 라이브러리 부재: $_LIB"
70
+ # shellcheck source=/dev/null
71
+ . "$_LIB"
72
+
73
+ # ── peer 해석 — tracked 파일에 운영자 홈 절대경로를 싣지 않는다 ──────────────
74
+ PEER_ROOT="$(fh_peer_resolve "$PEER_NAME")"; _rrc=$?
75
+ case "$_rrc" in
76
+ 0) ;;
77
+ 1) _peer_absent "peer «$PEER_NAME» 가 이 머신에 없다 — 안 쟀다(통과가 아니다)" ;;
78
+ 2) _harness_error "peer «$PEER_NAME» 해석이 모호하다 — 어느 트리를 쟀는지 말할 수 없으므로 판정하지 않는다" ;;
79
+ *) _harness_error "peer 루트 목록 자체가 없다 — 전제 파손" ;;
80
+ esac
81
+
82
+ ENTRY="$PEER_ROOT/scripts/blackbox_invariant_check.sh"
83
+ [ -r "$ENTRY" ] || _harness_error "peer 진입점 부재: $ENTRY (하네스는 있으나 계기가 없다)"
84
+
85
+ TARGET=""
86
+ case "${1:-}" in
87
+ --known-positive) TARGET="$SELF_DIR/fixtures/mate_agent_boundary_known_positive.md" ;;
88
+ --known-negative) TARGET="$SELF_DIR/fixtures/mate_agent_boundary_known_negative.md" ;;
89
+ --peer-default) TARGET="" ;; # peer 스크립트의 기본 대상에 맡긴다
90
+ --target)
91
+ TARGET="${2:-}"
92
+ [ -n "$TARGET" ] || _harness_error "--target 에 경로가 필요하다"
93
+ case "$TARGET" in /*) ;; *) TARGET="$PWD/$TARGET" ;; esac
94
+ ;;
95
+ '') _harness_error "모드 미지정 — 돌지 않은 실행을 판정으로 렌더하지 않는다" ;;
96
+ *) _harness_error "알 수 없는 모드 '$1'" ;;
97
+ esac
98
+ if [ -n "$TARGET" ]; then
99
+ [ -e "$TARGET" ] || _harness_error "대상 경로가 없다: $TARGET (부재 대상은 빈 대상이 아니다)"
100
+ [ -r "$TARGET" ] || _harness_error "대상 경로를 읽을 수 없다: $TARGET"
101
+ fi
102
+
103
+ # peer 트리를 **변이하지 않는다** — cwd 만 peer 로 두고 읽기만 한다.
104
+ # (기본 대상이 peer 레포 상대경로라 cwd 가 peer 여야 한다.)
105
+ # 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
106
+ if [ -n "$TARGET" ]; then
107
+ out=$(cd "$PEER_ROOT" && bash "$ENTRY" "$TARGET" 2>&1); rc=$?
108
+ else
109
+ out=$(cd "$PEER_ROOT" && bash "$ENTRY" 2>&1); rc=$?
110
+ fi
111
+ printf '%s\n' "$out"
112
+
113
+ # ── 명시 재매핑 (같은 숫자, 다른 사건 — 전파가 아니다) ───────────────────────
114
+ case "$rc" in
115
+ 0) printf 'mate-adapter: PASS — 정보경계 불변식 전건 통과\n'; exit "$RC_PASS" ;;
116
+ 1) printf 'mate-adapter: FAIL — 불변식 회귀\n'; exit "$RC_FAIL" ;;
117
+ 10) printf 'mate-adapter: HARNESS_ERROR — peer 계기가 대상을 못 읽었다(위반 0 이 아니다)\n' >&2
118
+ exit "$RC_HARNESS" ;;
119
+ *) _harness_error "peer 진입점이 자기 계약 {0,1,10} 밖의 $rc 를 반환했다" ;;
120
+ esac
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # peer_resolve.sh — **어댑터 공용** peer 레포 해석. `source` 전용(직접 실행하지 않는다).
3
+ #
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # 왜 이 파일이 따로 있나 — divergent-normalizer 를 안 만들기 위해
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # `scripts/cluster_capability_scan.sh` 가 이미 «track 이름 → 레포 루트» 를 푼다
8
+ # (`_resolve_root`, `_enumerate_harnesses`). 어댑터가 자기 규칙을 따로 쓰면 **두 계기가
9
+ # 서로 다른 트리를 본다** — 스캐너는 `~/projects/qasp-dev` 를 보고 어댑터는 못 찾는 식이다.
10
+ # 이 레포는 그 결함 계열을 «관대함 갈린 중복 정규화» 로 이름 붙여 두고 있다.
11
+ # 그래서 규칙을 **여기 한 벌만** 두고 세 어댑터가 전부 이걸 부른다.
12
+ #
13
+ # 🟥 규칙은 `cluster_capability_scan.sh` 를 읽고 그대로 옮긴 것이다:
14
+ # · 루트 목록 : `FH_CLUSTER_ROOTS`(콜론 구분 **레포 루트** 절대경로 목록)가 있으면 그것이
15
+ # 전부다. 없으면 `FH_PROJECTS_HOME`(기본 `$HOME/projects`) 아래를 본다.
16
+ # · 별칭 : `<name>` · `<name>-dev` · `<name>` 의 `_`→`-` — **닫힌 목록**.
17
+ # · 모호 : 둘 이상이 동시에 맞으면 **고르지 않는다.** 조용히 하나를 고르면 «어느
18
+ # 하네스를 쟀는지» 아무도 모르게 된다.
19
+ #
20
+ # ⚠️ 한 곳에서 의도적으로 갈린다 — 그리고 갈리는 이유를 적는다.
21
+ # 스캐너는 «루트 목록이 비었다» 를 `HARNESS_ERROR`(전제 파손)로 낸다. 어댑터도 같다.
22
+ # 다만 스캐너는 track 디렉토리를 열거하는 반면 어댑터는 **이름 하나를 찾는다** —
23
+ # 그래서 어댑터에는 「루트 목록은 멀쩡한데 그 안에 이 peer 가 없다」는 상태가 따로 있고,
24
+ # 그게 `PEER_ABSENT` 다. 스캐너에는 그 자리가 `UNREACHABLE` 로 있다(같은 명제, 다른 이름).
25
+ #
26
+ # 사용법
27
+ # . "$(dirname "${BASH_SOURCE[0]}")/peer_resolve.sh"
28
+ # if root="$(fh_peer_resolve gstack)"; then ... ; else rc=$?; ... ; fi
29
+ #
30
+ # fh_peer_resolve <name>
31
+ # stdout : 해석된 레포 루트 (rc=0 일 때만)
32
+ # rc=0 FOUND
33
+ # rc=1 ABSENT — 루트 목록은 읽었는데 이 peer 가 그 안에 없다
34
+ # rc=2 AMBIGUOUS — 별칭 여럿이 동시에 맞는다. 고르지 않는다
35
+ # rc=3 NO_ROOTS — 루트 목록 자체가 비었다(전제 파손. 「없다」가 아니다)
36
+ # stderr : rc!=0 일 때 한 줄 사유
37
+
38
+ # 별칭 후보 — 닫힌 목록. 스캐너의 `_resolve_root` 와 **같은 순서**여야 한다.
39
+ fh_peer_alias_candidates() { # $1=name → 후보를 줄 단위로
40
+ printf '%s\n' "$1" "$1-dev" "$(printf '%s' "$1" | tr '_' '-')"
41
+ }
42
+
43
+ fh_peer_resolve() { # $1=peer 이름
44
+ local n="${1:-}" c hits="" first="" nh
45
+ [ -n "$n" ] || { printf 'peer-resolve: 이름이 비었다\n' >&2; return 3; }
46
+
47
+ if [ -n "${FH_CLUSTER_ROOTS:-}" ]; then
48
+ # 명시 루트 목록. 각 항목이 **레포 루트 자체**다(스캐너와 같은 의미).
49
+ local r b usable=0
50
+ while IFS= read -r r; do
51
+ # 공백만 있는 항목은 항목이 아니다 — 스캐너가 자기 self-test 로 잡은 그 접힘.
52
+ r="$(printf '%s' "$r" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
53
+ [ -n "$r" ] || continue
54
+ usable=$((usable + 1))
55
+ b="$(basename "$r")"
56
+ while IFS= read -r c; do
57
+ [ "$b" = "$c" ] || continue
58
+ [ -d "$r" ] || continue
59
+ case " $hits " in *" $r "*) continue ;; esac
60
+ hits="$hits $r"; [ -n "$first" ] || first="$r"
61
+ done <<EOF
62
+ $(fh_peer_alias_candidates "$n")
63
+ EOF
64
+ done <<EOF
65
+ $(printf '%s\n' "$FH_CLUSTER_ROOTS" | tr ':' '\n')
66
+ EOF
67
+ if [ "$usable" -eq 0 ]; then
68
+ printf 'peer-resolve: FH_CLUSTER_ROOTS 에 쓸 수 있는 항목이 0개 — 어디를 볼지 자체가 없다(전제 파손이지 «peer 없음» 이 아니다)\n' >&2
69
+ return 3
70
+ fi
71
+ else
72
+ local home="${FH_PROJECTS_HOME:-$HOME/projects}"
73
+ if [ ! -d "$home" ]; then
74
+ printf 'peer-resolve: 프로젝트 홈이 없다: %s (전제 파손이지 «peer 없음» 이 아니다)\n' "$home" >&2
75
+ return 3
76
+ fi
77
+ while IFS= read -r c; do
78
+ [ -d "$home/$c" ] || continue
79
+ case " $hits " in *" $home/$c "*) continue ;; esac
80
+ hits="$hits $home/$c"; [ -n "$first" ] || first="$home/$c"
81
+ done <<EOF
82
+ $(fh_peer_alias_candidates "$n")
83
+ EOF
84
+ fi
85
+
86
+ nh=$(printf '%s' "$hits" | wc -w | tr -d ' ')
87
+ if [ "${nh:-0}" -gt 1 ]; then
88
+ printf 'peer-resolve: 한 이름에 레포 여럿 — 고르지 않는다: %s\n' \
89
+ "$(printf '%s' "$hits" | sed 's/^ //; s/ /, /g')" >&2
90
+ return 2
91
+ fi
92
+ if [ -z "$first" ]; then
93
+ printf 'peer-resolve: peer «%s» 가 이 머신에 없다(별칭 %s 전부 미해석)\n' \
94
+ "$n" "$(fh_peer_alias_candidates "$n" | tr '\n' '/' | sed 's|/$||')" >&2
95
+ return 1
96
+ fi
97
+ printf '%s' "$first"
98
+ return 0
99
+ }