@chrono-meta/fh-gate 1.4.85 → 1.4.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +40 -0
- package/package.json +4 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +25 -0
- package/scripts/selfcheck.sh +18 -0
- package/scripts/test_tag_version_lanes.sh +79 -0
- package/templates/.git-hooks/pre-push +56 -0
- package/templates/subagent-tally-hook.json +17 -0
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "fh-meta",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.86",
|
|
15
15
|
"description": "Hub meta-operations toolkit — 35 skills + 7 agents. New in 1.4.53: `fh-codex-doctor` (npm bin) — Codex adapter drift scanner; reads the documented M1/M2/M3 skill tier map + skill/agent source and reports codex-native/adapter-required/claude-native/unclassified per unit, wired into `npm test`/`prepublishOnly` (fail-closed on unclassified Claude-native primitives). New in 1.4.49: steel-quench gains Step 0.6 Verdict-Invariance Probe (groundedness axis — a load-bearing judged gate's verdict must track behavior, not rubric phrasing; measured flip-count over cross-family paraphrases; arXiv:2605.06161 Policy Invariance anchor); multi_model_sidecar_strategy §Vendor-native harness (a model is strongest in its own vendor CLI — Claude/CC, GPT/codex, Gemini/Antigravity; a universal router degrades all of them, so it stays an autocomplete/QA sidecar, never orchestration); predelete_check.sh fail-closed rewrite; memory-hygiene A-TMA 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 command-output axis (route to rtk/proxy for verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce envs). 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 queryable-wiki scaffold (INDEX + session-start read + R/W/C ingest). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment) + video-ingest (capability-routed video ingestion). New in 1.4.x: verify-axis check-class taxonomy (mandatory-pass/measured/judged), no-reinvention Tier-0 inventory, 7-class failure taxonomy, Destructive-Op Gate, Wave-T (Temper), tier-floor governance, Mode D Model Notice, FC consent lane, default-Sonnet guidance. New in 1.3.0: public-surface-audit, field-harvest Mode B auto-trigger, 4-axis gate scope ext. Validated cross-CLI: Claude Code, Codex, Gemini.",
|
|
16
16
|
"source": "./plugins/fh-meta"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fh-commons",
|
|
20
|
-
"version": "1.4.
|
|
20
|
+
"version": "1.4.86",
|
|
21
21
|
"description": "Project-agnostic utility skills — 4 skills (convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate) + 1 agent (quench-challenger). Domain-independent utilities transplantable into any project.",
|
|
22
22
|
"source": "./plugins/fh-commons"
|
|
23
23
|
}
|
|
@@ -828,3 +828,43 @@
|
|
|
828
828
|
B answered the full chain and the order invariant correctly from the gate''s own section (719 chars, ablatable).
|
|
829
829
|
And the headline: three measurements, three KEEPs. The ''CLAUDE.md is bloated'' premise is not surviving contact
|
|
830
830
|
— the earlier 46%-unmeasured framing measured probe coverage, which is not necessity.'
|
|
831
|
+
|
|
832
|
+
- date: 2026-08-03
|
|
833
|
+
agent: general-purpose x5 pinned to sonnet (ablation arms + isolation control)
|
|
834
|
+
task: Re-run the ablation sweep on §Pre-Publish Surface Gate and §FH Improvement 4-Axis Auto-Gate;
|
|
835
|
+
then, when arm B quoted text that grep proved absent from its own arm file, one control agent given
|
|
836
|
+
NO file and forbidden every tool.
|
|
837
|
+
mode: paired arms per section, floor tier, "read only the one arm file" — i.e. the method recorded in
|
|
838
|
+
the 2026-08-02 entry above
|
|
839
|
+
outcome: rejected
|
|
840
|
+
evidence: 'The method does not work. The control agent, given no file and zero tool calls, reproduced
|
|
841
|
+
the ablated section verbatim and named its own source: "project instructions / CLAUDE.md included in
|
|
842
|
+
my system prompt". A Claude Code subagent inherits the project CLAUDE.md, so "all file access denied
|
|
843
|
+
except the one arm file" (the 2026-08-02 entry''s own mode line) never produced a blind arm. Behaviour
|
|
844
|
+
is non-deterministic: in the same run one arm answered from the injected copy while another reported
|
|
845
|
+
the section missing — so the method can return a false KEEP or a false CUT by luck.'
|
|
846
|
+
note: 'Consequence for the entry above: its 3 KEEPs are UNCALIBRATED, not disproved. Its by-product
|
|
847
|
+
claim — the Pre-Publish table row is "719 chars, ablatable" — was re-measured today on a repaired
|
|
848
|
+
instrument and REJECTED: the row is redundant on both retrievability and trigger-firing, but arm A
|
|
849
|
+
stopped for a second confirmation before publishing while arm B proceeded, and that direction is not
|
|
850
|
+
worth 719 bytes on an irreversible surface.'
|
|
851
|
+
|
|
852
|
+
- date: 2026-08-03
|
|
853
|
+
agent: fh-commons:quench-challenger (isolated Agent, opus) + codex gpt-5.5 (cross-family, stdin form)
|
|
854
|
+
task: Axis 2 of the 4-axis gate on the repair itself — scripts/probe_scope_check.sh (ablation procedure
|
|
855
|
+
canon replacement + control C, which gates the script's exit code)
|
|
856
|
+
mode: isolated dispatch with a measured target fingerprint in Step 0; codex leg piped on stdin per the
|
|
857
|
+
recorded sidecar form
|
|
858
|
+
outcome: accepted
|
|
859
|
+
evidence: '19 findings (challenger 4S/4A/4B, codex 2S/4A/1B, overlapping roots). Both families
|
|
860
|
+
independently found the env kill switch (PROBE_SCOPE_FIXTURE ran a 2-row fixture instead of the real
|
|
861
|
+
corpus, silently skipped control C, printed the green line, exit 0 — reproduced). The challenger
|
|
862
|
+
produced a FALSE GREEN in control C using a nonexistent-asset fixture, and measured the second leak
|
|
863
|
+
channel: claude -p retains Bash/Read/Glob, and an arm run from a scratch dir recovered the ablated
|
|
864
|
+
section from the real CLAUDE.md and cited the file. codex reproduced a trap syntax break on a TMPDIR
|
|
865
|
+
containing an apostrophe. It also caught a regression I had just introduced: my new option parser
|
|
866
|
+
rejected --self-test, breaking scripts/selfcheck.sh:317.'
|
|
867
|
+
note: 'The value here is not the count — it is that the findings landed exactly where self-review had
|
|
868
|
+
just declared success. Adversarial review does not substitute for first use; today is the mirror:
|
|
869
|
+
self-repair does not substitute for adversarial review. Axis 2 shipped NOT CONVERGED with the open
|
|
870
|
+
leak channel declared in the header rather than closed by assertion.'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrono-meta/fh-gate",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.86",
|
|
4
4
|
"description": "FH runtime adapters — run FH governance, skills, and agents via Claude or Codex with machine-parseable gates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -146,6 +146,8 @@
|
|
|
146
146
|
"templates/settings.PreToolUse.snippet.json",
|
|
147
147
|
"scripts/halffix_propagation_scan.sh",
|
|
148
148
|
"scripts/test_halffix_lanes.sh",
|
|
149
|
-
"scripts/test_ollama_panel_lanes.sh"
|
|
149
|
+
"scripts/test_ollama_panel_lanes.sh",
|
|
150
|
+
"scripts/test_tag_version_lanes.sh",
|
|
151
|
+
"templates/subagent-tally-hook.json"
|
|
150
152
|
]
|
|
151
153
|
}
|
|
@@ -10,6 +10,31 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
|
10
10
|
|
|
11
11
|
## Plugin Level
|
|
12
12
|
|
|
13
|
+
### [1.4.86] — 2026-08-03
|
|
14
|
+
|
|
15
|
+
**fix: 격리를 자칭하던 어블레이션 절차 + ambig 게이트 앵커 + 밀린 출하 자산 반영**
|
|
16
|
+
|
|
17
|
+
- `scripts/probe_scope_check.sh` — 어블레이션 절차 정본을 교체했다. 서브에이전트에는 프로젝트
|
|
18
|
+
CLAUDE.md 가 **시스템 프롬프트로 주입**되므로 "다른 파일 읽기 금지"는 블라인드 arm 을 만들지
|
|
19
|
+
못한다(파일 0개·툴 0회 컨트롤이 잘린 절을 verbatim 재현하며 출처를 자백). 헤더는 이제
|
|
20
|
+
**"이 절차는 아직 검증되지 않았다"**로 시작한다 — 적대 검증이 두 번째 누출 채널(`claude -p`
|
|
21
|
+
가 Bash/Read/Glob 을 쥐고 있어 arm 이 진짜 CLAUDE.md 를 찾아 인용)을 실측했고, 툴 차단 처방은
|
|
22
|
+
known-pair 가 없다(`--tools ''` 는 양성 컨트롤 실패 = 전 구간 거짓 KEEP). 기존 판정은
|
|
23
|
+
UNCALIBRATED, 오늘 판정 2건은 PROVISIONAL, CUT 후보는 **DO NOT CUT**.
|
|
24
|
+
- 같은 파일 — control B 의 `ambig` 항을 **control C**(픽스처 코퍼스 자기 재호출)로 앵커. 실 코퍼스는
|
|
25
|
+
건드리지 않는다. 게이트 항을 되돌리면 arm 이 빨개지는 것까지 검증.
|
|
26
|
+
- 같은 파일 — 적대 검증이 잡은 자기결함 5건 봉합: env 킬스위치(`PROBE_SCOPE_FIXTURE` export 시
|
|
27
|
+
픽스처를 실 코퍼스 대신 검사하고 control C 를 무음으로 끄고도 초록 + exit 0) → argv 플래그로
|
|
28
|
+
교체 · 타입 없는 exit 3 을 증거로 받던 control C → 자식의 `ambiguous-basename` 카운트 동반 요구 ·
|
|
29
|
+
따옴표 불안전한 RETURN trap · mktemp 실패가 exit 3 으로 대상 코드를 무고하던 것 → exit 1 ·
|
|
30
|
+
새 옵션 파서가 `--self-test` 를 거부해 `selfcheck.sh` 를 깨뜨리던 자초 회귀.
|
|
31
|
+
- `knowledge/shared/learnings/subagent_invocations_log.yaml` — 호출 로그 2건. 그중 하나가
|
|
32
|
+
전날 엔트리를 `rejected` 로 뒤집는다(그 엔트리의 mode 줄 "all file access denied except the one
|
|
33
|
+
arm file" 이 성립하지 않는다).
|
|
34
|
+
- 밀려 있던 출하 자산 반영(#239, 어제): `scripts/selfcheck.sh` · `scripts/package_coverage_check.sh` ·
|
|
35
|
+
`templates/.git-hooks/pre-push` — 태그가 자기 버전을 가리키는지 검사하는 가드.
|
|
36
|
+
|
|
37
|
+
|
|
13
38
|
### [1.4.1] — 2026-06-08
|
|
14
39
|
|
|
15
40
|
**docs: repo-root declutter + pre-commit gate hardening**
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -330,6 +330,24 @@ else
|
|
|
330
330
|
fail=1
|
|
331
331
|
fi
|
|
332
332
|
|
|
333
|
+
# tag/version consistency guard. Wired with the guard it anchors: the guard exists because a wrong
|
|
334
|
+
# tag reached the remote and a publish from the wrong tree was stopped only by npm's own collision
|
|
335
|
+
# check, so an unrun anchor here would be the same luck-as-floor arrangement one layer up.
|
|
336
|
+
if [ ! -f templates/.git-hooks/pre-push ]; then
|
|
337
|
+
echo "SKIP test_tag_version_lanes.sh (subject templates/.git-hooks/pre-push absent)"
|
|
338
|
+
elif [ -f scripts/test_tag_version_lanes.sh ]; then
|
|
339
|
+
if ! bash scripts/test_tag_version_lanes.sh >/dev/null 2>&1; then
|
|
340
|
+
echo "FAIL test_tag_version_lanes.sh: the tag/version guard would mis-route"
|
|
341
|
+
bash scripts/test_tag_version_lanes.sh 2>&1 | tail -12
|
|
342
|
+
fail=1
|
|
343
|
+
else
|
|
344
|
+
echo "PASS test_tag_version_lanes.sh (mismatch blocks · match silent · scope · override)"
|
|
345
|
+
fi
|
|
346
|
+
else
|
|
347
|
+
echo "FAIL test_tag_version_lanes.sh: pre-push present but its anchor is missing"
|
|
348
|
+
fail=1
|
|
349
|
+
fi
|
|
350
|
+
|
|
333
351
|
# ④-e dispatch-log reconciliation + its tally hook. Wired in the same commit that ships them: the
|
|
334
352
|
# obligation they mechanize lost 20/20 in a single session, so leaving the checker itself unrun
|
|
335
353
|
# would be the same defect one layer up.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_tag_version_lanes.sh — known-pair anchor for the pre-push tag/version consistency guard.
|
|
3
|
+
#
|
|
4
|
+
# WHY (measured 2026-08-02): a release tag was pushed onto the WRONG commit. `git pull --ff-only`
|
|
5
|
+
# had failed with a diverging-branches error that went unread, so `main` was still the pre-release
|
|
6
|
+
# tree; the tag landed there and `npm publish` was then attempted from that same tree. The ONLY
|
|
7
|
+
# thing that stopped the publish was npm's own "cannot publish over 1.4.84" collision check.
|
|
8
|
+
# Publishing does not un-happen — being saved by the registry's bookkeeping is luck, not a floor.
|
|
9
|
+
#
|
|
10
|
+
# Built at N=1 on purpose. This repo's escalation rule ("1-2 occurrences -> prose, N>=3 -> mechanize")
|
|
11
|
+
# is scoped to REVERSIBLE surfaces; a wrong tag on a public repo plus a publish from the wrong tree
|
|
12
|
+
# is the irreversible class, where the Surface-Class Degrade Invariant says fail-CLOSED immediately.
|
|
13
|
+
#
|
|
14
|
+
# The lanes drive the REAL hook through its real stdin contract (`<local_ref> <local_sha>
|
|
15
|
+
# <remote_ref> <remote_sha>`), not a re-spelling of its predicate — a hand-copied condition is a
|
|
16
|
+
# divergent normalizer and drifts lenient.
|
|
17
|
+
#
|
|
18
|
+
# Exit 0 = the guard discriminates · 1 = it would let a mismatched tag through, or block a good one.
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
23
|
+
HOOK="$REPO/templates/.git-hooks/pre-push"
|
|
24
|
+
FAILED=0; PASS=0
|
|
25
|
+
chk() { if [ "$1" -eq 0 ]; then PASS=$((PASS+1)); echo " ✅ $2"; else FAILED=1; echo " ❌ $2"; fi; }
|
|
26
|
+
|
|
27
|
+
[ -f "$HOOK" ] || { echo "FAIL tag-version lanes: subject $HOOK missing"; exit 1; }
|
|
28
|
+
ZERO=0000000000000000000000000000000000000000
|
|
29
|
+
|
|
30
|
+
# A throwaway repo so the lanes never depend on this repo's live history.
|
|
31
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
32
|
+
( cd "$T" && git init -q . && git config user.email a@b && git config user.name a
|
|
33
|
+
printf '{\n "name": "x",\n "version": "1.0.0"\n}\n' > package.json
|
|
34
|
+
git add -A && git commit -qm v100
|
|
35
|
+
printf '{\n "name": "x",\n "version": "1.1.0"\n}\n' > package.json
|
|
36
|
+
git add -A && git commit -qm v110
|
|
37
|
+
printf 'no manifest here\n' > README.md && rm -f package.json
|
|
38
|
+
git add -A && git commit -qm nomanifest ) >/dev/null 2>&1
|
|
39
|
+
|
|
40
|
+
C_100=$(cd "$T" && git rev-parse HEAD~2)
|
|
41
|
+
C_110=$(cd "$T" && git rev-parse HEAD~1)
|
|
42
|
+
C_NONE=$(cd "$T" && git rev-parse HEAD)
|
|
43
|
+
|
|
44
|
+
# Drive the hook exactly as git does. Run it INSIDE the scratch repo so its rev-parse/cat-file
|
|
45
|
+
# resolve against that history. Only the tag verdict is under test here, so a non-zero exit from an
|
|
46
|
+
# unrelated guard would be a false red — assert on the MESSAGE, and on rc only for the clean lane.
|
|
47
|
+
run() { # $1=tag $2=sha -> prints hook output
|
|
48
|
+
printf '%s %s %s %s\n' "refs/tags/$1" "$2" "refs/tags/$1" "$ZERO" \
|
|
49
|
+
| ( cd "$T" && bash "$HOOK" origin 2>&1 )
|
|
50
|
+
}
|
|
51
|
+
says_mismatch() { printf '%s' "$1" | grep -q "TAG/VERSION MISMATCH"; }
|
|
52
|
+
blocks() { printf '%s' "$1" | grep -q "FH Tag/Version Consistency"; }
|
|
53
|
+
|
|
54
|
+
echo "── the measured failure, reproduced ──"
|
|
55
|
+
out=$(run v1.1.0 "$C_100")
|
|
56
|
+
says_mismatch "$out" ; chk $? "known-POSITIVE: v1.1.0 on a commit whose package.json says 1.0.0 → MISMATCH"
|
|
57
|
+
blocks "$out" ; chk $? "…and it BLOCKS (detection wired to a blocker, not just printed)"
|
|
58
|
+
|
|
59
|
+
echo "── the good case must not be blocked (over-blocking trains the override) ──"
|
|
60
|
+
out=$(run v1.1.0 "$C_110")
|
|
61
|
+
if says_mismatch "$out"; then chk 1 "known-NEGATIVE: matching tag/version is silent"; else chk 0 "known-NEGATIVE: matching tag/version is silent"; fi
|
|
62
|
+
if blocks "$out"; then chk 1 "…and does not block"; else chk 0 "…and does not block"; fi
|
|
63
|
+
|
|
64
|
+
echo "── scope: not every tag is a release tag ──"
|
|
65
|
+
out=$(run v1.0.0 "$C_NONE")
|
|
66
|
+
if says_mismatch "$out"; then chk 1 "no package.json at that commit → not applicable, not a block"; else chk 0 "no package.json at that commit → not applicable, not a block"; fi
|
|
67
|
+
out=$(run sprint-42 "$C_100")
|
|
68
|
+
if says_mismatch "$out"; then chk 1 "non-version tag name (sprint-42) is out of scope"; else chk 0 "non-version tag name (sprint-42) is out of scope"; fi
|
|
69
|
+
|
|
70
|
+
echo "── the override channel exists and is explicit ──"
|
|
71
|
+
grep -q 'TAG_VERSION_OK' "$HOOK" ; chk $? "TAG_VERSION_OK override present (mirrors MAIN_PUSH_OK / DESTRUCTIVE_OP_OK)"
|
|
72
|
+
out=$(printf '%s %s %s %s\n' "refs/tags/v1.1.0" "$C_100" "refs/tags/v1.1.0" "$ZERO" \
|
|
73
|
+
| ( cd "$T" && TAG_VERSION_OK=1 bash "$HOOK" origin 2>&1 ))
|
|
74
|
+
if blocks "$out"; then chk 1 "override actually lets the push through"; else chk 0 "override actually lets the push through"; fi
|
|
75
|
+
|
|
76
|
+
echo ""
|
|
77
|
+
if [ "$FAILED" -ne 0 ]; then echo "TAG-VERSION LANES: FAIL"; exit 1; fi
|
|
78
|
+
echo "TAG-VERSION LANES: PASS ($PASS/$PASS)"
|
|
79
|
+
exit 0
|
|
@@ -54,6 +54,7 @@ DEL_OTHER="" # refs/tags/* refs/notes/* etc. being deleted
|
|
|
54
54
|
FORCED_REFS=""
|
|
55
55
|
UNCLASSIFIED="" # force-check impossible (remote tip not fetched)
|
|
56
56
|
DIRECT_MAIN="" # non-delete update pushed straight at the integration branch (PR-only policy)
|
|
57
|
+
TAG_MISMATCH="" # vX.Y.Z tag whose commit's package.json disagrees (irreversible-adjacent)
|
|
57
58
|
SEP=$'\n' # ranges are newline-separated and evaluated ONE REF AT A TIME: concatenating
|
|
58
59
|
# them into one arg string let `--not` from ref A flip polarity for ref B, so a
|
|
59
60
|
# multi-ref push could return an empty or wrong commit set (R6 audit 2026-07-26).
|
|
@@ -184,6 +185,40 @@ while read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
184
185
|
fi ;;
|
|
185
186
|
esac
|
|
186
187
|
fi
|
|
188
|
+
# TAG/VERSION CONSISTENCY — a `vX.Y.Z` tag must point at a commit whose package.json says X.Y.Z.
|
|
189
|
+
# WHY (measured 2026-08-02): a release tag was pushed onto the WRONG commit because `git pull`
|
|
190
|
+
# had failed with a diverging-branches error that went unread, so `main` was still the pre-release
|
|
191
|
+
# tree. `npm publish` was then attempted from that same tree — and the ONLY thing that stopped it
|
|
192
|
+
# was npm's own "cannot publish over 1.4.84" collision check. Publishing is irreversible; being
|
|
193
|
+
# saved by the registry's bookkeeping is luck, not a floor.
|
|
194
|
+
# This is N=1 and it is built anyway: the repetition rule ("1-2 occurrences -> prose") is scoped to
|
|
195
|
+
# REVERSIBLE surfaces. A wrong tag on a public repo plus a publish from the wrong tree is the
|
|
196
|
+
# irreversible class, where this repo's own Surface-Class Degrade Invariant says fail-CLOSED now.
|
|
197
|
+
# Scope is deliberately narrow: only a NEW `refs/tags/v<digits>` push, only when package.json
|
|
198
|
+
# exists at that commit. Absent package.json -> not applicable (a tag in a non-npm repo is fine);
|
|
199
|
+
# unreadable package.json at a commit that HAS one -> BLOCK (cannot decide != allowed).
|
|
200
|
+
if [ "$local_sha" != "$ZERO" ] && [ "${remote_sha:-$ZERO}" = "$ZERO" ]; then
|
|
201
|
+
case "$remote_ref" in
|
|
202
|
+
refs/tags/v[0-9]*)
|
|
203
|
+
_tv="${remote_ref#refs/tags/v}"
|
|
204
|
+
_commit=$(git rev-parse "${local_sha}^{commit}" 2>/dev/null)
|
|
205
|
+
if [ -z "$_commit" ]; then
|
|
206
|
+
echo " ⛔ TAG/VERSION: $remote_ref does not resolve to a commit — cannot verify, refusing"
|
|
207
|
+
TAG_MISMATCH="$TAG_MISMATCH $remote_ref"
|
|
208
|
+
elif git cat-file -e "${_commit}:package.json" 2>/dev/null; then
|
|
209
|
+
_pv=$(git show "${_commit}:package.json" 2>/dev/null \
|
|
210
|
+
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
|
211
|
+
if [ -z "$_pv" ]; then
|
|
212
|
+
echo " ⛔ TAG/VERSION: package.json exists at ${_commit} but its version is unreadable — refusing"
|
|
213
|
+
TAG_MISMATCH="$TAG_MISMATCH $remote_ref"
|
|
214
|
+
elif [ "$_pv" != "$_tv" ]; then
|
|
215
|
+
echo " ⛔ TAG/VERSION MISMATCH: $remote_ref points at ${_commit}, whose package.json says $_pv"
|
|
216
|
+
TAG_MISMATCH="$TAG_MISMATCH $remote_ref"
|
|
217
|
+
fi
|
|
218
|
+
fi ;;
|
|
219
|
+
esac
|
|
220
|
+
fi
|
|
221
|
+
|
|
187
222
|
# PR-only policy: a non-delete update aimed straight at the integration branch.
|
|
188
223
|
# Detected INSIDE this loop because this is the only place git's ref list is readable.
|
|
189
224
|
if [ "$local_sha" != "$ZERO" ]; then
|
|
@@ -403,6 +438,27 @@ fi
|
|
|
403
438
|
# instead of a bare server rejection, and it keeps working if the server setting is ever relaxed.
|
|
404
439
|
# It is deliberately NOT the hard floor — a client-side hook is bypassable by `--no-verify`.
|
|
405
440
|
# Scope: blocks the PUSH, not the merge — `gh pr merge` operates server-side and is unaffected.
|
|
441
|
+
if [ -n "$TAG_MISMATCH" ] && [ "${TAG_VERSION_OK:-0}" != "1" ]; then
|
|
442
|
+
echo ""
|
|
443
|
+
echo "══════════════════════════════════════════════"
|
|
444
|
+
echo " ⛔ FH Tag/Version Consistency (pre-push)"
|
|
445
|
+
echo "══════════════════════════════════════════════"
|
|
446
|
+
echo " Tag(s) whose commit disagrees with package.json:$TAG_MISMATCH"
|
|
447
|
+
echo ""
|
|
448
|
+
echo " A release tag is the thing a human and a script both trust to answer \"what shipped\"."
|
|
449
|
+
echo " Measured 2026-08-02: a tag went onto the pre-release commit because a failed \`git pull\`"
|
|
450
|
+
echo " went unread, and \`npm publish\` was then attempted from that same tree. Only npm's own"
|
|
451
|
+
echo " version-collision check stopped it. Publishing does not un-happen; that was luck."
|
|
452
|
+
echo ""
|
|
453
|
+
echo " Fix: git fetch origin && git switch main && git pull --ff-only"
|
|
454
|
+
echo " git tag -d <tag> && git push origin :refs/tags/<tag> # if already pushed"
|
|
455
|
+
echo " # then re-tag on the commit whose package.json really carries that version"
|
|
456
|
+
echo ""
|
|
457
|
+
echo " Deliberate exception (explicit, logged — mirrors MAIN_PUSH_OK / DESTRUCTIVE_OP_OK):"
|
|
458
|
+
echo " TAG_VERSION_OK=1 git push …"
|
|
459
|
+
exit 1
|
|
460
|
+
fi
|
|
461
|
+
|
|
406
462
|
if [ -n "$DIRECT_MAIN" ] && [ "${MAIN_PUSH_OK:-0}" != "1" ]; then
|
|
407
463
|
echo ""
|
|
408
464
|
echo "══════════════════════════════════════════════"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_readme": "FH dispatch-tally hook. Merge the `hooks.SubagentStop` block below into your .claude/settings.json (that file is gitignored, which is why this template exists). It feeds session_close_check.sh ④-e, which blocks a close push when sub-agents were dispatched and the day's invocation-log entries are zero. The hook TALLIES ONLY — it never writes a log entry, because a fabricated outcome/evidence field would poison the 60/40 promotion gate worse than a missing one.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SubagentStop": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "bash -c 'HUB=\"${CLAUDE_PROJECT_DIR:-$HOME/projects/forge-harness}\"; T=\"$HUB/tracks/_meta/.subagent_dispatch_tally\"; mkdir -p \"$(dirname \"$T\")\" 2>/dev/null; printf \"%s\\n\" \"$(date +%Y-%m-%d)\" >> \"$T\" 2>/dev/null; exit 0'",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|