@chrono-meta/fh-gate 3.0.0 → 3.1.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 (111) 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 +50 -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 +12 -2
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +37 -5
  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 +30 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -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 +20 -0
  21. package/knowledge/shared/learnings/subagent_invocations_log.yaml +451 -4
  22. package/package.json +40 -2
  23. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  24. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  25. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  26. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  27. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  30. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  31. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  32. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  33. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  34. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  35. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  36. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  37. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  38. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  39. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  40. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  41. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  42. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  43. package/plugins/fh-meta/CHANGELOG.md +76 -1
  44. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  45. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  47. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  50. package/plugins/fh-qp/README.md +71 -0
  51. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  53. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  54. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  56. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  59. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  60. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  61. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  62. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  63. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  64. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  65. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  66. package/scripts/chamber_run.sh +14 -5
  67. package/scripts/com.forge-harness.live-eval.plist +84 -0
  68. package/scripts/compaction_probe.sh +9 -35
  69. package/scripts/directional_diff_gate.sh +14 -2
  70. package/scripts/frontier_digest_autopilot.sh +4 -1
  71. package/scripts/map_postprocess.py +90 -0
  72. package/scripts/outbound_query_guard.sh +131 -0
  73. package/scripts/outbound_query_hook.sh +373 -0
  74. package/scripts/package_coverage_check.sh +55 -16
  75. package/scripts/pipe_verdict_guard.sh +41 -1
  76. package/scripts/probe_live_eval.sh +240 -0
  77. package/scripts/probe_live_eval_lib.py +579 -0
  78. package/scripts/proposal_hook.sh +120 -17
  79. package/scripts/push_zone_check.sh +78 -0
  80. package/scripts/residency_closure_scan.py +252 -0
  81. package/scripts/selfcheck.sh +41 -1
  82. package/scripts/session_close_check.sh +100 -0
  83. package/scripts/sim_isolated_run.sh +98 -2
  84. package/scripts/test_action_yml_lanes.sh +160 -0
  85. package/scripts/test_fh_qp_lanes.sh +105 -0
  86. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  87. package/scripts/test_map_postprocess_lanes.sh +143 -0
  88. package/scripts/test_marker_affected_lanes.sh +93 -0
  89. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  90. package/scripts/test_marker_oracle_lanes.sh +136 -0
  91. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  92. package/scripts/test_outbound_query_lanes.sh +87 -0
  93. package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
  94. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  95. package/scripts/test_preprep_drift_anchor.sh +3 -3
  96. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  97. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  98. package/scripts/test_proposal_hook_lanes.sh +22 -1
  99. package/scripts/test_push_zone_lanes.sh +304 -0
  100. package/scripts/test_residency_closure_lanes.sh +70 -0
  101. package/scripts/test_sim_isolated_run_lanes.sh +119 -0
  102. package/scripts/test_utterance_intake_lanes.sh +414 -0
  103. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  104. package/scripts/transcript_utterances.py +222 -0
  105. package/scripts/utterance_intake.sh +424 -0
  106. package/scripts/validate_yaml.sh +27 -0
  107. package/scripts/worktree_reclaim.sh +95 -0
  108. package/templates/.git-hooks/pre-commit +353 -1
  109. package/templates/.git-hooks/pre-push +91 -0
  110. package/templates/RED_TEAM_REPORT.md +49 -0
  111. package/templates/settings.PreToolUse.snippet.json +65 -1
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env bash
2
+ # test_push_zone_lanes.sh — known-pair anchor for the pre-push hook's Push-Zone verdict, and for
3
+ # the session-close ①-f advisory that mirrors it.
4
+ #
5
+ # ── WHY THIS FILE EXISTS ───────────────────────────────────────────────────────────────────────
6
+ # 2026-09-05: templates/.git-hooks/pre-push gained a new verdict path and scripts/session_close_
7
+ # check.sh gained a new ①-f advisory, both implementing CLAUDE.local.md §REST API push 계정규칙
8
+ # mechanically — a push to a github.com remote whose owner is NOT on the operator's owner-account
9
+ # list needs the REST Contents API channel, not plain git push. Before this suite the new code had
10
+ # no known-pair anchor at all. Per CLAUDE.md §Instrument Calibration, a gate is not trusted until
11
+ # calibrated on at least one known-positive and one known-negative, hand-verified.
12
+ #
13
+ # ── DISCIPLINE INHERITED FROM THE SIBLING SUITE (test_prepush_destructive_lanes.sh) ────────────
14
+ # (a) Run the hook FOR REAL — `bash -n` is not an instrument (a runtime fault can parse clean,
15
+ # abort the hook, and still exit 0).
16
+ # (b) A runtime fault is checked BEFORE the verdict, else an aborted hook scores as a clean pass.
17
+ # (c) "It blocked" is NOT "it blocked correctly" — a block must be ATTRIBUTED to a named FH gate.
18
+ # (d) NEVER read or assert on the operator's real owner-account list
19
+ # (.claude/rules/.push-zone-owners). Every lane below builds its own throwaway fixture list
20
+ # via the PUSH_ZONE_OWNERS override, with fictitious account names. The real file, if present
21
+ # on this machine, is never opened by this suite in either direction.
22
+ #
23
+ # Runs entirely in mktemp throwaway repos: never touches this repo's index, worktree, refs, or its
24
+ # real .claude/rules/.push-zone-owners.
25
+ #
26
+ # Usage: bash scripts/test_push_zone_lanes.sh
27
+ # FH_PUSH_ZONE_HOOK_SRC=<path> → test a DIFFERENT copy of the pre-push hook (fail-before proof).
28
+ # FH_PUSH_ZONE_SCC_SRC=<path> → same, for session_close_check.sh.
29
+ set -uo pipefail
30
+ . "$(dirname "${BASH_SOURCE[0]}")/fixture_guard_lib.sh"
31
+
32
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
33
+ HOOK_SRC="${FH_PUSH_ZONE_HOOK_SRC:-$REPO_ROOT/templates/.git-hooks/pre-push}"
34
+ SCC_SRC="${FH_PUSH_ZONE_SCC_SRC:-$REPO_ROOT/scripts/session_close_check.sh}"
35
+ DEF_SRC="$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults"
36
+ ZERO="0000000000000000000000000000000000000000"
37
+
38
+ if [ ! -f "$HOOK_SRC" ]; then
39
+ echo "HARNESS_ERROR — pre-push hook not found at $HOOK_SRC (subject absent; NOT a pass)"; exit 1
40
+ fi
41
+
42
+ WORK="$(fh_fixture_root "$(mktemp -d)")" || exit 1
43
+ : "${WORK:?fixture root unset — refusing to run git in cwd}"
44
+ trap 'rm -rf "$WORK"' EXIT
45
+ cp "$HOOK_SRC" "$WORK/hook" || exit 1
46
+ HOOK="$WORK/hook"
47
+
48
+ # Fictitious owner-account fixture — NEVER the operator's real list. The second entry
49
+ # (acme-shared-org) is asserted to never appear in any captured output — it is a decoy for the
50
+ # "list content is never printed" requirement, not used by any URL in this suite.
51
+ OWNERS_OK="$WORK/owners-ok.txt"
52
+ printf '# fixture owner list — test only, not the operator real list\nknown-owner\nacme-shared-org\n' > "$OWNERS_OK"
53
+
54
+ PASSED=0; FAILED=0; ALL_OUT=""
55
+
56
+ # ── fixture: same shape as test_prepush_destructive_lanes.sh's newrepo() ───────────────────────
57
+ newrepo() {
58
+ local d; d="$(fh_fixture_root "$(mktemp -d)")" || return 1
59
+ : "${d:?fixture root unset — refusing to run git in cwd}"
60
+ mkdir -p "$d/.claude/rules" "$d/templates/.git-hooks" "$d/tracks/_meta" "$d/scripts" || return 1
61
+ [ -f "$DEF_SRC" ] && cp "$DEF_SRC" "$d/.claude/rules/.public-surface-patterns.defaults"
62
+ [ -f "$REPO_ROOT/scripts/psa_scan_lib.sh" ] && cp "$REPO_ROOT/scripts/psa_scan_lib.sh" "$d/scripts/psa_scan_lib.sh"
63
+ printf '.claude/rules/.public-surface-patterns\n' > "$d/.gitignore"
64
+ cp "$HOOK" "$d/templates/.git-hooks/pre-push" || return 1
65
+ printf 'base\n' > "$d/a.txt"
66
+ (
67
+ cd "$d" || exit 1
68
+ git init -q || exit 1
69
+ git symbolic-ref HEAD refs/heads/main || exit 1
70
+ git config user.email t@example.com || exit 1
71
+ git config user.name t || exit 1
72
+ git add -A >/dev/null 2>&1 || exit 1
73
+ git commit -qm base >/dev/null 2>&1 || exit 1
74
+ git update-ref refs/remotes/origin/main HEAD || exit 1
75
+ ) || return 1
76
+ printf '%s' "$d"
77
+ }
78
+
79
+ require_repo() { # $1 = candidate repo path, $2 = lane name
80
+ if [ -z "${1:-}" ] || [ ! -d "$1/.git" ]; then
81
+ echo "HARNESS_ERROR — fixture repo not built for $2 (setup failed; NOT a pass)"
82
+ exit 1
83
+ fi
84
+ }
85
+
86
+ # check_zone <name> <repo> <owners-file|""> <url> <push_zone_ok:0|1> <expect:block|pass> <cause-regex|""> <refline>
87
+ check_zone() {
88
+ local name="$1" repo="$2" owners="$3" url="$4" pzok="$5" expect="$6" cause="$7"; shift 7
89
+ local out rc got _pzsrc
90
+ require_repo "$repo" "$name"
91
+ _pzsrc="${owners:-$WORK/nonexistent-owners-file-for-zone-test}"
92
+ out=$(printf '%s\n' "$@" | ( cd "$repo" && PUSH_ZONE_OWNERS="$_pzsrc" PUSH_ZONE_OK="$pzok" \
93
+ bash templates/.git-hooks/pre-push origin "$url" 2>&1 )); rc=$?
94
+ ALL_OUT="$ALL_OUT
95
+ $out"
96
+
97
+ if printf '%s' "$out" | grep -qiE 'bad substitution|unbound variable|syntax error|command not found'; then
98
+ echo " ❌ $name — RUNTIME FAULT in the hook (aborted, not passed)"
99
+ printf '%s\n' "$out" | grep -iE 'bad substitution|unbound|syntax error|command not found' | sed 's/^/ /' | head -3
100
+ FAILED=$((FAILED+1)); return
101
+ fi
102
+
103
+ if [ "$rc" -ne 0 ]; then
104
+ if printf '%s' "$out" | grep -qE 'FH (Push-Zone Gate|Destructive-Op Gate)'; then
105
+ got=block
106
+ else
107
+ echo " ❌ $name — blocked, but NOT by a named FH gate (harness reason ≠ finding)"
108
+ printf '%s\n' "$out" | sed 's/^/ | /' | head -8
109
+ FAILED=$((FAILED+1)); return
110
+ fi
111
+ else
112
+ got=pass
113
+ fi
114
+
115
+ if [ "$got" != "$expect" ]; then
116
+ echo " ❌ $name — expected $expect, got $got"
117
+ printf '%s\n' "$out" | sed 's/^/ | /' | head -10
118
+ FAILED=$((FAILED+1)); return
119
+ fi
120
+
121
+ if [ -n "$cause" ] && ! printf '%s' "$out" | grep -qE "$cause"; then
122
+ echo " ❌ $name — verdict $got is correct but UNATTRIBUTED (missing: $cause)"
123
+ printf '%s\n' "$out" | sed 's/^/ | /' | head -10
124
+ FAILED=$((FAILED+1)); return
125
+ fi
126
+
127
+ echo " ✅ $name (expected $expect)"
128
+ PASSED=$((PASSED+1))
129
+ }
130
+
131
+ echo "[push-zone] known-pair anchor for the pre-push Push-Zone verdict (hook: $HOOK_SRC)"
132
+ echo ""
133
+
134
+ # Z0 — no owner-account list at all → UNCALIBRATED, plain push allowed.
135
+ R=$(newrepo); require_repo "$R" "Z0"
136
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
137
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
138
+ check_zone "Z0 no owner-account list → PASS " "$R" "" \
139
+ "https://github.com/known-owner/repo.git" 0 pass 'push-zone: UNCALIBRATED' \
140
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
141
+ rm -rf "$R"
142
+
143
+ # Z1 — owner IN the list, https → silent pass; the rest of the hook (confidentiality) still runs.
144
+ R=$(newrepo); require_repo "$R" "Z1"
145
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
146
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
147
+ check_zone "Z1 owner IN list, https → PASS " "$R" "$OWNERS_OK" \
148
+ "https://github.com/known-owner/repo.git" 0 pass 'FH Pre-Publish' \
149
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
150
+ rm -rf "$R"
151
+
152
+ # Z2 — owner OUT of the list, https → BLOCK, attributed, names the URL's (fictitious) owner.
153
+ R=$(newrepo); require_repo "$R" "Z2"
154
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
155
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
156
+ check_zone "Z2 owner OUT of list, https → BLOCK" "$R" "$OWNERS_OK" \
157
+ "https://github.com/someone-else/repo.git" 0 block 'FH Push-Zone Gate' \
158
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
159
+ if printf '%s' "$ALL_OUT" | grep -qF 'github.com/someone-else'; then
160
+ echo " ✅ Z2b block message names the URL's owner"; PASSED=$((PASSED+1))
161
+ else
162
+ echo " ❌ Z2b — block message did not name the URL's owner (someone-else)"; FAILED=$((FAILED+1))
163
+ fi
164
+ rm -rf "$R"
165
+
166
+ # Z3 — owner OUT of the list, scp-like ssh (git@github.com:owner/repo) → BLOCK.
167
+ R=$(newrepo); require_repo "$R" "Z3"
168
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
169
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
170
+ check_zone "Z3 owner OUT of list, ssh scp → BLOCK" "$R" "$OWNERS_OK" \
171
+ "git@github.com:someone-else/repo.git" 0 block 'FH Push-Zone Gate' \
172
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
173
+ rm -rf "$R"
174
+
175
+ # Z4 — remote host is not github.com → out of scope for this axis, pass allowed, says so.
176
+ R=$(newrepo); require_repo "$R" "Z4"
177
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
178
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
179
+ check_zone "Z4 non-github host → PASS " "$R" "$OWNERS_OK" \
180
+ "https://ghe.example.com/x/y" 0 pass 'not github.com' \
181
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
182
+ rm -rf "$R"
183
+
184
+ # Z5 — PUSH_ZONE_OK=1 override on an out-of-list push → allowed, conscious, LOGGED.
185
+ R=$(newrepo); require_repo "$R" "Z5"
186
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
187
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
188
+ check_zone "Z5 PUSH_ZONE_OK=1 override → PASS " "$R" "$OWNERS_OK" \
189
+ "https://github.com/someone-else/repo.git" 1 pass 'PUSH_ZONE_OK=1' \
190
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
191
+ LOG="$R/tracks/_meta/.push_zone_override_log"
192
+ if [ -f "$LOG" ] && grep -qF 'owner:someone-else' "$LOG"; then
193
+ echo " ✅ Z5b override logged to tracks/_meta/.push_zone_override_log"; PASSED=$((PASSED+1))
194
+ else
195
+ echo " ❌ Z5b override NOT logged (wanted a line inside $LOG)"; FAILED=$((FAILED+1))
196
+ fi
197
+ rm -rf "$R"
198
+
199
+ # Z6 — existing destructive verdict (branch DELETE, unique paths → REVIEW → BLOCK) still stands
200
+ # AFTER a silent zone PASS. Same shape as the sibling suite's P2, through an owner-matching
201
+ # github.com URL — proves the zone block, inserted near the TOP of the hook, does not shadow
202
+ # or reorder the destructive checks further down.
203
+ R=$(newrepo); require_repo "$R" "Z6"
204
+ ( cd "$R" && git checkout -q -b unique-work && printf 'only here\n' > unique.txt \
205
+ && git add unique.txt && git commit -qm unique >/dev/null && git checkout -q main )
206
+ TIP=$(cd "$R" && git rev-parse unique-work)
207
+ check_zone "Z6 zone PASS, then BRANCH DELETE → BLOCK" "$R" "$OWNERS_OK" \
208
+ "https://github.com/known-owner/repo.git" 0 block 'REVIEW: .* unique path' \
209
+ "(delete) $ZERO refs/heads/unique-work $TIP"
210
+ rm -rf "$R"
211
+
212
+ # ── Residency: the owner-list's OTHER entries must never appear in any captured output ─────────
213
+ if printf '%s' "$ALL_OUT" | grep -qF 'acme-shared-org'; then
214
+ echo " ❌ RESIDENCY — the fixture owner-list's second entry leaked into hook output"
215
+ FAILED=$((FAILED+1))
216
+ else
217
+ echo " ✅ RESIDENCY — owner-list content never appears in any captured hook output"
218
+ PASSED=$((PASSED+1))
219
+ fi
220
+
221
+ echo ""
222
+ # ── Z7–Z14 (cross-family codex gpt-5.5, 2026-09-05): the first draft accepted three literal URL
223
+ # prefixes and read every other github.com shape as "unrecognized → allow" — four real shapes were
224
+ # failing OPEN; an inline comment in the owner list authorised pushes; CRLF / trailing slash blocked
225
+ # the operator's own remotes. Each lane below is one of those exact inputs.
226
+ OWNERS_CRLF="$WORK/owners-crlf.txt"; printf 'known-owner\r\nacme-shared-org\r\n' > "$OWNERS_CRLF"
227
+ OWNERS_COMMENT="$WORK/owners-comment.txt"; printf 'known-owner # someone-else is NOT allowed\nacme-shared-org\n' > "$OWNERS_COMMENT"
228
+ OWNERS_SLASH="$WORK/owners-slash.txt"; printf 'known-owner/\nacme-shared-org\n' > "$OWNERS_SLASH"
229
+
230
+ _zone_ff_lane() { # $1=id $2=desc $3=owners $4=url $5=expect $6=cause
231
+ local R BASESHA NEWSHA
232
+ R=$(newrepo); require_repo "$R" "$1"
233
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
234
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
235
+ check_zone "$1 $2" "$R" "$3" "$4" 0 "$5" "$6" "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
236
+ rm -rf "$R"
237
+ }
238
+ _zone_ff_lane Z7 "userinfo https (user@github.com), owner OUT → BLOCK" "$OWNERS_OK" "https://user@github.com/someone-else/repo.git" block 'FH Push-Zone Gate'
239
+ _zone_ff_lane Z8 "https with :443 port, owner OUT → BLOCK" "$OWNERS_OK" "https://github.com:443/someone-else/repo.git" block 'FH Push-Zone Gate'
240
+ _zone_ff_lane Z9 "ssh:// with :22 port, owner OUT → BLOCK" "$OWNERS_OK" "ssh://git@github.com:22/someone-else/repo.git" block 'FH Push-Zone Gate'
241
+ _zone_ff_lane Z10 "scp form with leading slash (git@github.com:/o/r), owner OUT → BLOCK" "$OWNERS_OK" "git@github.com:/someone-else/repo.git" block 'FH Push-Zone Gate'
242
+ _zone_ff_lane Z11 "inline comment in owner list must NOT authorise the commented name → BLOCK" "$OWNERS_COMMENT" "https://github.com/someone-else/repo.git" block 'FH Push-Zone Gate'
243
+ _zone_ff_lane Z12 "CRLF owner list, owner IN → pass (no over-block)" "$OWNERS_CRLF" "https://github.com/known-owner/repo.git" pass ''
244
+ _zone_ff_lane Z13 "trailing-slash owner entry, owner IN → pass" "$OWNERS_SLASH" "https://github.com/known-owner/repo.git" pass ''
245
+ _zone_ff_lane Z13b "uppercase host/owner in URL, owner IN → pass (case-insensitive)" "$OWNERS_OK" "https://GitHub.com/Known-Owner/repo.git" pass ''
246
+ # Z14 — $2 is a bare remote NAME (alias/insteadOf edge): the hook must resolve it, not read it as a host.
247
+ R=$(newrepo); require_repo "$R" "Z14"
248
+ ( cd "$R" && git remote add origin https://github.com/someone-else/repo.git ) 2>/dev/null
249
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
250
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
251
+ check_zone "Z14 \$2 is the remote NAME → resolved via git remote get-url → BLOCK" "$R" "$OWNERS_OK" \
252
+ "origin" 0 block 'FH Push-Zone Gate' "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
253
+ rm -rf "$R"
254
+
255
+ echo "── session-close ①-f wiring (scripts/session_close_check.sh) ──"
256
+ if [ ! -f "$SCC_SRC" ]; then
257
+ echo "HARNESS_ERROR — session_close_check.sh not found at $SCC_SRC"; FAILED=$((FAILED+1))
258
+ else
259
+ SR="$(fh_fixture_root "$(mktemp -d)")" || exit 1
260
+ mkdir -p "$SR/tracks/_meta" "$SR/scripts" "$SR/.claude/rules"
261
+ cp "$REPO_ROOT/scripts/push_zone_check.sh" "$SR/scripts/push_zone_check.sh" 2>/dev/null
262
+ chmod +x "$SR/scripts/push_zone_check.sh" 2>/dev/null
263
+ ( cd "$SR" && git init -q && git symbolic-ref HEAD refs/heads/main \
264
+ && git config user.email t@example.com && git config user.name t \
265
+ && git commit -q --allow-empty -m base >/dev/null ) >/dev/null 2>&1
266
+
267
+ if [ -x "$SR/scripts/push_zone_check.sh" ]; then
268
+ SC_OUT=$(PUSH_ZONE_OWNERS="$OWNERS_OK" bash "$SCC_SRC" "$SR" 2>&1)
269
+ if printf '%s' "$SC_OUT" | grep -qE '①-f push-zone: '; then
270
+ echo " ✅ ①-f prints advisory-prefixed push_zone_check.sh output when both files exist"
271
+ PASSED=$((PASSED+1))
272
+ else
273
+ echo " ❌ ①-f did not print the expected advisory prefix"; FAILED=$((FAILED+1))
274
+ fi
275
+ # RESIDENCY (codex #9, 2026-09-05): the enumerator prints owner-account names, and a close's
276
+ # output lands in transcripts and session cards. ①-f therefore prints COUNTS only — the decoy
277
+ # list entry must never appear, and the summary line must.
278
+ if printf '%s' "$SC_OUT" | grep -qF 'acme-shared-org'; then
279
+ echo " ❌ ①-f leaked an owner-account list entry into the close output"; FAILED=$((FAILED+1))
280
+ else
281
+ echo " ✅ ①-f withholds owner-account names (counts only)"; PASSED=$((PASSED+1))
282
+ fi
283
+ if printf '%s' "$SC_OUT" | grep -qE '①-f push-zone: [0-9]+ repo\(s\) enumerated'; then
284
+ echo " ✅ ①-f prints the enumeration summary (repo count · outside-list count)"; PASSED=$((PASSED+1))
285
+ else
286
+ echo " ❌ ①-f summary line missing"; FAILED=$((FAILED+1))
287
+ fi
288
+ else
289
+ echo " ❌ Z-SC setup — fixture push_zone_check.sh not executable"; FAILED=$((FAILED+1))
290
+ fi
291
+
292
+ # Owner-account list absent → UNCALIBRATED, one line, never silent.
293
+ SC_OUT2=$(PUSH_ZONE_OWNERS="$SR/no-such-owners-file" bash "$SCC_SRC" "$SR" 2>&1)
294
+ if printf '%s' "$SC_OUT2" | grep -qE '①-f push-zone UNCALIBRATED'; then
295
+ echo " ✅ ①-f UNCALIBRATED when the owner-account list is absent"; PASSED=$((PASSED+1))
296
+ else
297
+ echo " ❌ ①-f did not report UNCALIBRATED with no owner-account list"; FAILED=$((FAILED+1))
298
+ fi
299
+ rm -rf "$SR"
300
+ fi
301
+
302
+ echo ""
303
+ echo "[push-zone] $PASSED passed, $FAILED failed"
304
+ [ "$FAILED" -eq 0 ]
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # residency_closure_scan.py 레인 테스트 — known-pair 교정.
3
+ #
4
+ # 이 파일이 있는 이유: 초판이 cross-family 적대검증(gpt-5.5, 2026-08-09)에서 **false-CLEAN
5
+ # 다수**로 반증됐다. 아래 L1~L3 은 그때 재현한 실제 우회 경로이고, 되돌리면 이 레인들이
6
+ # 적색이 된다. 「고쳤다」를 주장으로 두지 않고 실행으로 남긴다.
7
+ #
8
+ # 각 레인 = (입력, 기대 rc). rc 계약: 0=CLEAN(스크리닝) · 1=TAINTED · 10=HARNESS(fail-closed)
9
+ set -uo pipefail
10
+
11
+ HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
12
+ SCAN="$HERE/residency_closure_scan.py"
13
+ TMP=$(mktemp -d) || exit 10
14
+ trap 'rm -rf "$TMP"' EXIT
15
+
16
+ PASS=0; FAIL=0
17
+ lane() { # lane <이름> <기대rc> <모듈> [패턴파일]
18
+ local name=$1 want=$2 mod=$3 pat=${4:-$TMP/pat}
19
+ RESIDENCY_PATTERNS="$pat" python3 "$SCAN" --repo "$TMP/repo" "$mod" >/dev/null 2>&1
20
+ local got=$?
21
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ %-42s rc=%s\n' "$name" "$got"
22
+ else FAIL=$((FAIL+1)); printf ' ❌ %-42s rc=%s (기대 %s)\n' "$name" "$got" "$want"; fi
23
+ }
24
+
25
+ R="$TMP/repo"
26
+ mkdir -p "$R"/{src/pkg,myapp,app,clean,deep/sub}
27
+ : > "$R/src/pkg/__init__.py"; : > "$R/app/__init__.py"; : > "$R/deep/__init__.py"
28
+ : > "$R/deep/sub/__init__.py"
29
+
30
+ # L1 상대 임포트 — 초판이 `level == 0` 으로 통째로 버렸다
31
+ printf 'from .secrets import HOST\n' > "$R/src/pkg/main.py"
32
+ printf 'HOST = "api.acme.corp"\n' > "$R/src/pkg/secrets.py"
33
+ # L2 루트 화이트리스트 밖 레이아웃 — 초판은 ("src","scripts",…) 만 따라갔다
34
+ printf 'import myapp.config\n' > "$R/myapp/main.py"
35
+ printf 'HOST = "api.acme.corp"\n' > "$R/myapp/config.py"
36
+ # L3 `from pkg import sub` — 초판은 pkg/__init__.py 만 훑었다
37
+ printf 'from app import secrets\n' > "$R/app/main.py"
38
+ printf 'HOST = "api.acme.corp"\n' > "$R/app/secrets.py"
39
+ # L4 부모 패키지 __init__.py 오염 — payload 에 같이 나간다
40
+ printf 'HOST = "api.acme.corp"\n' > "$R/deep/__init__.py"
41
+ printf 'X = 1\n' > "$R/deep/sub/leaf.py"
42
+ # 과교정 컨트롤 — 진짜 깨끗한 모듈
43
+ printf 'X = 1\n' > "$R/clean/ok.py"
44
+
45
+ printf 'acme\n' > "$TMP/pat"
46
+ : > "$TMP/pat_empty"
47
+ printf '# 주석뿐\n' > "$TMP/pat_comment"
48
+
49
+ echo "== residency 폐포 스캔 레인 =="
50
+ lane "L1 상대 임포트 → TAINTED" 1 src.pkg.main
51
+ lane "L2 루트 화이트리스트 밖 → TAINTED" 1 myapp.main
52
+ lane "L3 from pkg import sub → TAINTED" 1 app.main
53
+ lane "L4 부모 __init__ 오염 → TAINTED" 1 deep.sub.leaf
54
+ lane "★컨트롤: 깨끗한 모듈 → CLEAN" 0 clean.ok
55
+ lane "빈 운영자 패턴 → HARNESS" 10 clean.ok "$TMP/pat_empty"
56
+ lane "주석뿐 운영자 패턴 → HARNESS" 10 clean.ok "$TMP/pat_comment"
57
+ lane "운영자 패턴 부재 → HARNESS" 10 clean.ok "$TMP/nonexistent"
58
+ lane "모듈 부재 → HARNESS" 10 no.such.module
59
+
60
+ # --files 모드: 실제 송신 목록만 잰다
61
+ RESIDENCY_PATTERNS="$TMP/pat" python3 "$SCAN" --repo "$R" --files src/pkg/secrets.py >/dev/null 2>&1
62
+ [ $? = 1 ] && { PASS=$((PASS+1)); echo " ✅ --files 오염 파일 → TAINTED rc=1"; } \
63
+ || { FAIL=$((FAIL+1)); echo " ❌ --files 오염 파일"; }
64
+ RESIDENCY_PATTERNS="$TMP/pat" python3 "$SCAN" --repo "$R" --files clean/ok.py >/dev/null 2>&1
65
+ [ $? = 0 ] && { PASS=$((PASS+1)); echo " ✅ --files 깨끗한 파일 → CLEAN rc=0"; } \
66
+ || { FAIL=$((FAIL+1)); echo " ❌ --files 깨끗한 파일"; }
67
+
68
+ echo "── PASS $PASS · FAIL $FAIL"
69
+ [ "$FAIL" -eq 0 ] || exit 1
70
+ exit 0
@@ -56,6 +56,10 @@ case "${FH_STUB_MODE:-say}" in
56
56
  # 🟥 stdin 을 그대로 받아 적는다 — 실물 `claude -p` 가 하는 짓을 재현한다.
57
57
  # 진짜 CLI 는 stdin 이 TTY 가 아니면 그것을 «읽어 프롬프트 뒤에 붙인다».
58
58
  stdin) echo "stub answer"; cat >> "${FH_STUB_STDIN_LOG:-/dev/null}" ;;
59
+ # L26 (2026-09-05) — a command that runs long past any sane --timeout, default SIGTERM
60
+ # disposition (no trap): dies as soon as the watchdog signals it, so a lane can tell "the
61
+ # deadline was enforced" from "we just waited for it to finish on its own".
62
+ hang) sleep 30 ;;
59
63
  esac
60
64
  exit 0
61
65
  STUB
@@ -278,5 +282,120 @@ else
278
282
  no "L25 meta.tsv not written at all ($META)"
279
283
  fi
280
284
 
285
+ # ── L26 timeout(1) resolution (2026-09-05) — the launchd-PATH incident this exists for ──────────
286
+ # WHY: launchd's PATH ($HOME/.local/bin:/usr/local/bin:/usr/bin:/bin, per this repo's own plist
287
+ # templates prior to this patch) has neither `timeout` nor `gtimeout` on it — both live under
288
+ # Homebrew's prefix. The first real launchd live-eval run (2026-09-05 02:30) hit exactly this:
289
+ # every rep of every arm died at `timeout: command not found` and the run scored 12/12
290
+ # FAILED-TO-RUN with no clue why. This lane shadows whichever CURRENT PATH dir(s) actually hold
291
+ # `timeout`/`gtimeout` (symlinks to everything else in them) — computed, not hardcoded to
292
+ # /opt/homebrew/bin, so the same lane works on Intel, Homebrew or a Linux /usr/bin — and proves
293
+ # two things a prose comment cannot: the bash-native fallback
294
+ # actually answers (not just "doesn't crash"), and it actually enforces the deadline rather than
295
+ # silently never firing.
296
+ _shadow_timeout_dirs() { # prints $PATH with every dir holding timeout/gtimeout replaced by a symlink shadow of it MINUS those two
297
+ # 🟥 Measured 2026-09-05 on CI (ubuntu): `timeout` lives in /usr/bin next to bash and git, so
298
+ # DROPPING that directory (the previous form of this fixture) also dropped bash/git, and the
299
+ # potency check below scored the lane red on every Linux box while macOS (Homebrew prefix,
300
+ # separate dir) stayed green. A shadow directory keeps every other name resolvable and removes
301
+ # only the two — the same fixture on both layouts.
302
+ local IFS=':' d out="" shadow n=0 f b
303
+ for d in $PATH; do
304
+ if [ -x "$d/timeout" ] || [ -x "$d/gtimeout" ]; then
305
+ n=$((n+1)); shadow="$WORKROOT/notimeout_shadow_$n"; rm -rf "$shadow"; mkdir -p "$shadow"
306
+ for f in "$d"/*; do
307
+ [ -x "$f" ] && [ ! -d "$f" ] || continue
308
+ b="${f##*/}"
309
+ case "$b" in timeout|gtimeout) continue ;; esac
310
+ ln -s "$f" "$shadow/$b"
311
+ done
312
+ d="$shadow"
313
+ fi
314
+ out="${out:+$out:}$d"
315
+ done
316
+ printf '%s' "$out"
317
+ }
318
+ NOTIMEOUT_PATH="$(_shadow_timeout_dirs)"
319
+
320
+ echo ""
321
+ echo "── L26 timeout(1) resolution ───────────────────────────────────────"
322
+
323
+ # Fixture-potency check FIRST — a stripped PATH that still resolves timeout/gtimeout proves
324
+ # nothing ([[feedback_fixture_must_use_the_breaking_spelling]]), and a strip that ALSO removed
325
+ # bash/git would confound every assertion below with an unrelated clone failure. Both are named
326
+ # distinctly rather than let a downstream lane fail for the wrong reason. Per this file's own
327
+ # convention (see L24's "검사 못 함(스킵 아님)"), an unusable fixture on this machine is scored
328
+ # as a failure, not silently skipped — it is loud precisely because CI does not gate this file
329
+ # (verified: no .github/workflows/*.yml references it), so a quiet skip would never be noticed.
330
+ if PATH="$NOTIMEOUT_PATH" command -v timeout >/dev/null 2>&1 || PATH="$NOTIMEOUT_PATH" command -v gtimeout >/dev/null 2>&1; then
331
+ no "L26-FIXTURE stripped PATH still resolves timeout/gtimeout — cannot run L26 on this machine"
332
+ elif ! PATH="$NOTIMEOUT_PATH" command -v bash >/dev/null 2>&1 || ! PATH="$NOTIMEOUT_PATH" command -v git >/dev/null 2>&1; then
333
+ no "L26-FIXTURE stripping timeout/gtimeout also removed bash or git — cannot run L26 on this machine"
334
+ else
335
+ ok "L26-FIXTURE stripped PATH lacks timeout+gtimeout; bash/git still resolve"
336
+
337
+ # L26a — no timeout/gtimeout at all: the runner must still answer (via the bash fallback) and
338
+ # must NAME which control it used in its own header, rather than leaving that to be inferred.
339
+ OUTDIR="$WORKROOT/o26a"
340
+ OUT=$( cd "$SRC" && PATH="$STUBBIN:$NOTIMEOUT_PATH" HOME="$FAKEHOME" FH_STUB_MODE=say \
341
+ bash "$SUT" --arm a --reps 1 --prompt p --out "$OUTDIR" 2>&1 )
342
+ if printf '%s' "$OUT" | grep -q "timeout_tool=bash-fallback" && printf '%s' "$OUT" | grep -q "captured"; then
343
+ ok "L26a no timeout/gtimeout on PATH -> bash-fallback answers and names itself in the header"
344
+ else
345
+ no "L26a bash-fallback did not fire/name itself: $(printf '%s' "$OUT" | grep -E 'timeout_tool|captured|UNMEASURED|EMPTY' | head -3 | tr '\n' ';')"
346
+ fi
347
+
348
+ # L26b — the fallback must actually ENFORCE the deadline, not just silently never fire. A stub
349
+ # that would otherwise run 30s under --timeout 2 must come back UNMEASURED well under 30s.
350
+ OUTDIR="$WORKROOT/o26b"
351
+ _t0=$(date +%s)
352
+ OUT=$( cd "$SRC" && PATH="$STUBBIN:$NOTIMEOUT_PATH" HOME="$FAKEHOME" FH_STUB_MODE=hang \
353
+ bash "$SUT" --arm a --reps 1 --prompt p --timeout 2 --out "$OUTDIR" 2>&1 )
354
+ _t1=$(date +%s); _elapsed=$(( _t1 - _t0 ))
355
+ if printf '%s' "$OUT" | grep -q "UNMEASURED" && [ "$_elapsed" -le 10 ]; then
356
+ ok "L26b bash-fallback enforces the deadline (UNMEASURED in ${_elapsed}s of a 30s hang, timeout=2s)"
357
+ else
358
+ no "L26b fallback did not enforce the timeout (elapsed=${_elapsed}s): $(printf '%s' "$OUT" | grep -E 'UNMEASURED|captured' | head -1)"
359
+ fi
360
+
361
+ # L26c control — SAME hang stub, DEFAULT (unstripped) PATH: real timeout/gtimeout must still be
362
+ # preferred over the fallback (header says gnu/gtimeout, not bash-fallback), AND must enforce
363
+ # the same deadline. Without this, L26a/L26b could be passing because the fallback is used
364
+ # unconditionally, not because resolution correctly prefers the real tool when it is present
365
+ # ([[feedback_control_presence_is_not_discrimination]]).
366
+ OUTDIR="$WORKROOT/o26c"
367
+ OUT=$( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=hang \
368
+ bash "$SUT" --arm a --reps 1 --prompt p --timeout 2 --out "$OUTDIR" 2>&1 )
369
+ case "$OUT" in
370
+ *"timeout_tool=bash-fallback"*)
371
+ no "L26c control — default PATH used bash-fallback instead of a real timeout/gtimeout" ;;
372
+ *"timeout_tool=gnu"*|*"timeout_tool=gtimeout"*)
373
+ printf '%s' "$OUT" | grep -q "UNMEASURED" \
374
+ && ok "L26c control — default PATH prefers the real timeout tool and still enforces the deadline" \
375
+ || no "L26c control — real timeout tool resolved but did not enforce the deadline"
376
+ ;;
377
+ *) no "L26c control — no timeout_tool= line in header at all: $(printf '%s' "$OUT" | grep timeout_tool)" ;;
378
+ esac
379
+ fi
380
+
381
+ # ── L27 MCP isolation (2026-09-05) — the QP chamber floor sim saw the operator's user-scope MCP servers ─
382
+ # in a headless arm's tool list. The runner must pass --strict-mcp-config ALWAYS (no --mcp-config ⇒
383
+ # no servers), and pass through --mcp-config only when the caller names a file. Both directions.
384
+ OUTDIR="$WORKROOT/o27"; LOG="$WORKROOT/argv27.log"; : > "$LOG"
385
+ ( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=say FH_STUB_ARGV_LOG="$LOG" \
386
+ bash "$SUT" --arm a --reps 1 --prompt p --out "$OUTDIR" ) >/dev/null 2>&1
387
+ grep -q -- "--strict-mcp-config" "$LOG" \
388
+ && ok "L27a default passes --strict-mcp-config (user-scope MCP cannot reach a blind arm)" \
389
+ || no "L27a default did NOT pass --strict-mcp-config — operator MCP servers leak into headless arms"
390
+ grep -q -- "--mcp-config" "$LOG" \
391
+ && no "L27b default passed --mcp-config without a file (would point at nothing)" \
392
+ || ok "L27b default passes no --mcp-config (strict + none = no servers)"
393
+ OUTDIR="$WORKROOT/o27c"; LOG2="$WORKROOT/argv27c.log"; : > "$LOG2"; printf '{"mcpServers":{}}\n' > "$WORKROOT/mcp27.json"
394
+ ( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=say FH_STUB_ARGV_LOG="$LOG2" \
395
+ bash "$SUT" --arm a --reps 1 --prompt p --mcp-config "$WORKROOT/mcp27.json" --out "$OUTDIR" ) >/dev/null 2>&1
396
+ grep -q -- "--mcp-config $WORKROOT/mcp27.json" "$LOG2" && grep -q -- "--strict-mcp-config" "$LOG2" \
397
+ && ok "L27c --mcp-config <file> reaches the CLI together with --strict-mcp-config (explicit allow, still strict)" \
398
+ || no "L27c --mcp-config not plumbed (or strict dropped when a file is given)"
399
+
281
400
  echo "sim_isolated_run lanes: $pass passed, $fail failed"
282
401
  [ "$fail" -eq 0 ] || exit 1