@chrono-meta/fh-gate 1.4.84 → 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/CLAUDE.md +12 -4
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +56 -0
- package/package.json +5 -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 +31 -0
- package/scripts/session_close_check.sh +51 -0
- package/scripts/test_dispatch_log_lanes.sh +99 -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
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -595,10 +595,18 @@ Default operation is a **standard interactive session**. Agent dispatch (single
|
|
|
595
595
|
|
|
596
596
|
Mapped paths: check `auto_project_mapping.md` or `find ~/projects -maxdepth 1 -type d` for actuals.
|
|
597
597
|
|
|
598
|
-
**Invocation log obligation
|
|
599
|
-
sub-agent invocation, append to `knowledge/shared/learnings/subagent_invocations_log.yaml`
|
|
600
|
-
outcome: `accepted`/`partial`/`rejected`/`sustained` — `sustained` = decided NOT to invoke,
|
|
601
|
-
This feeds the 60/40 promotion gate + UAP loop; detail:
|
|
598
|
+
**Invocation log obligation — now mechanically reconciled, not recall-dependent**: immediately after
|
|
599
|
+
any custom sub-agent invocation, append to `knowledge/shared/learnings/subagent_invocations_log.yaml`
|
|
600
|
+
(8 fields · outcome: `accepted`/`partial`/`rejected`/`sustained` — `sustained` = decided NOT to invoke,
|
|
601
|
+
also recorded). This feeds the 60/40 promotion gate + UAP loop; detail:
|
|
602
|
+
`knowledge/shared/rules/operations.md`.
|
|
603
|
+
**Floor (2026-08-02)**: a `SubagentStop` hook tallies every dispatch and `session_close_check.sh` ④-e
|
|
604
|
+
blocks a close push when dispatches happened and the day's entries are ZERO. Consolidating a class of
|
|
605
|
+
dispatches into one entry with measured counts is fine and is not penalised — the check catches the
|
|
606
|
+
total miss, not imperfect bookkeeping. **Why it needed a floor**: this line was prose-only and a single
|
|
607
|
+
session dispatched 20+ subagents and logged none of them, in the same session that recovered this very
|
|
608
|
+
log file from a branch queued for deletion. The hook only tallies; it never writes an entry, because a
|
|
609
|
+
fabricated `outcome`/`evidence` would poison the promotion gate worse than a missing one.
|
|
602
610
|
|
|
603
611
|
### Context Card — Required Format for Dispatch
|
|
604
612
|
```
|
|
@@ -812,3 +812,59 @@
|
|
|
812
812
|
whether an agent can locate it. Both arms independently cited the same residual (the paths: trigger is a READ,
|
|
813
813
|
so a from-scratch Write never loads the detail rule), which is exactly the path the probe encodes. Method costs
|
|
814
814
|
~2 min/section and replaces the coverage number that was cut for being wrong four times.'
|
|
815
|
+
|
|
816
|
+
- date: '2026-08-02'
|
|
817
|
+
agent: general-purpose x6 pinned to sonnet (ablation known pairs — 3 sections x 2 arms)
|
|
818
|
+
task: Ablation sweep — 3 sections x 2 arms (6 dispatches; the day's SubagentStop tally read 7, the extra being one earlier probe). Two largest MEASURABLE sections of CLAUDE.md — §Autonomous Initiative (12,224 chars,
|
|
819
|
+
8 probes) and §Session Wrap-up (8,398 chars, 2 probes) — plus the earlier §New Skill Creation Pre-Commit Gate
|
|
820
|
+
mode: paired arms per section (asset WITH vs WITHOUT the section), floor tier, all file access denied except the
|
|
821
|
+
one arm file
|
|
822
|
+
outcome: accepted
|
|
823
|
+
evidence: '3 sections ablated, 3 KEEP. §Autonomous Initiative: 5 routing behaviours GROUNDED in arm A and NOT
|
|
824
|
+
IN MY CONTEXT in arm B (context-doctor, harvest-loop, goal-quench, deep-clarify, the already-running Guard).
|
|
825
|
+
§Session Wrap-up: 4 of 6 close-chain answers fully absent without it (the ordered sequence, the absolute-last
|
|
826
|
+
rule and its reason, the late-finding re-run rule, the open-PR sweep). §New Skill gate: 3 of 3 absent.'
|
|
827
|
+
note: 'By-product worth more than the verdicts: the Pre-Publish row IN the initiative table is redundant — arm
|
|
828
|
+
B answered the full chain and the order invariant correctly from the gate''s own section (719 chars, ablatable).
|
|
829
|
+
And the headline: three measurements, three KEEPs. The ''CLAUDE.md is bloated'' premise is not surviving contact
|
|
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": [
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
"scripts/prepush_guard_check.sh",
|
|
105
105
|
"scripts/psa_scan_lib.sh",
|
|
106
106
|
"scripts/session_close_check.sh",
|
|
107
|
+
"scripts/test_dispatch_log_lanes.sh",
|
|
107
108
|
"scripts/memory_link_check.py",
|
|
108
109
|
"scripts/test_memory_link_check.sh",
|
|
109
110
|
"scripts/memory_nearcheck.py",
|
|
@@ -145,6 +146,8 @@
|
|
|
145
146
|
"templates/settings.PreToolUse.snippet.json",
|
|
146
147
|
"scripts/halffix_propagation_scan.sh",
|
|
147
148
|
"scripts/test_halffix_lanes.sh",
|
|
148
|
-
"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"
|
|
149
152
|
]
|
|
150
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,37 @@ 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
|
+
|
|
351
|
+
# ④-e dispatch-log reconciliation + its tally hook. Wired in the same commit that ships them: the
|
|
352
|
+
# obligation they mechanize lost 20/20 in a single session, so leaving the checker itself unrun
|
|
353
|
+
# would be the same defect one layer up.
|
|
354
|
+
if [ -f scripts/test_dispatch_log_lanes.sh ]; then
|
|
355
|
+
if ! bash scripts/test_dispatch_log_lanes.sh >/dev/null 2>&1; then
|
|
356
|
+
echo "FAIL test_dispatch_log_lanes.sh: the dispatch-log reconciliation would mis-report"
|
|
357
|
+
bash scripts/test_dispatch_log_lanes.sh 2>&1 | tail -14
|
|
358
|
+
fail=1
|
|
359
|
+
else
|
|
360
|
+
echo "PASS test_dispatch_log_lanes.sh (date-spelling + verdict + tally-hook lanes)"
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
|
|
333
364
|
# selfcheck's own subject-presence discriminators. Every other guard under scripts/ has a lane suite;
|
|
334
365
|
# this decision had none, and it shipped two mis-routings in one session — a two-arm form that fell
|
|
335
366
|
# through in silence, then a package discriminator keyed on a file that actually ships. Both are
|
|
@@ -117,6 +117,57 @@ if [ -n "$LAST_TAG" ] && [ -f "$FH/package.json" ]; then
|
|
|
117
117
|
fi
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
|
+
# ④-e DISPATCH-LOG RECONCILIATION — mechanical, because prose failed completely.
|
|
121
|
+
# CLAUDE.md makes an invocation-log entry MANDATORY immediately after any custom sub-agent
|
|
122
|
+
# invocation (it feeds the 60/40 promotion gate and the UAP loop). Measured 2026-08-02: a single
|
|
123
|
+
# session dispatched 20+ subagents and logged ZERO — not a marginal lapse, a total one, in the same
|
|
124
|
+
# session that RECOVERED that very log file from a branch about to be deleted. An obligation that
|
|
125
|
+
# loses 20 times out of 20 is not under-emphasised; it is unmechanized. Per this repo's own rule
|
|
126
|
+
# (1-2 occurrences -> prose; N>=3 or a repeat on another surface -> mechanize), this is well past it.
|
|
127
|
+
#
|
|
128
|
+
# The check is a RECONCILIATION, not an auto-writer. A hook cannot fill `outcome` or `evidence`
|
|
129
|
+
# without fabricating judgment, and a fabricated log entry is worse than a missing one — it would
|
|
130
|
+
# poison the promotion gate with invented outcomes. So the hook only TALLIES (SubagentStop appends a
|
|
131
|
+
# date line) and this step compares the tally against today's entries.
|
|
132
|
+
#
|
|
133
|
+
# It deliberately does NOT demand 1:1. Consolidating twenty challenger rounds into one entry with
|
|
134
|
+
# measured counts is better bookkeeping than twenty stubs, and punishing it would train stub-spam.
|
|
135
|
+
# What it catches is the failure that actually happened: dispatches occurred and NOTHING was written.
|
|
136
|
+
# ABSENCE IS NOT ZERO. The tally comes from a SubagentStop hook configured in `.claude/settings.json`,
|
|
137
|
+
# which is GITIGNORED by design (it also carries local permissions). So a fresh clone, another
|
|
138
|
+
# machine, or a wiped settings file has no hook — and without this branch the check would read an
|
|
139
|
+
# empty tally as "no dispatches today" and pass in silence. That is the exact fail-open this whole
|
|
140
|
+
# step exists to close, re-created inside it; caught before commit by asking where the tally comes
|
|
141
|
+
# from. Installable snippet: templates/subagent-tally-hook.json.
|
|
142
|
+
TALLY="$FH/tracks/_meta/.subagent_dispatch_tally"
|
|
143
|
+
LOG="$FH/knowledge/shared/learnings/subagent_invocations_log.yaml"
|
|
144
|
+
HOOK_OK=0
|
|
145
|
+
if [ -f "$FH/.claude/settings.json" ]; then
|
|
146
|
+
grep -q '"SubagentStop"' "$FH/.claude/settings.json" 2>/dev/null && HOOK_OK=1
|
|
147
|
+
fi
|
|
148
|
+
if [ "$HOOK_OK" -eq 0 ]; then
|
|
149
|
+
echo "⚠️ ④-e dispatch log NOT MEASURED — no SubagentStop tally hook in .claude/settings.json"
|
|
150
|
+
echo " (that file is gitignored, so a fresh clone has none). An unmeasured dispatch count is"
|
|
151
|
+
echo " NOT a count of zero. Install: templates/subagent-tally-hook.json → .claude/settings.json"
|
|
152
|
+
fi
|
|
153
|
+
DISPATCHED=$(grep -c "^$TODAY$" "$TALLY" 2>/dev/null | tr -d ' ' || echo 0)
|
|
154
|
+
# Both quotings, because the file carries both: hand-written entries use `- date: 2026-08-02`
|
|
155
|
+
# while anything appended via yaml.dump renders `- date: '"'"'2026-08-02'"'"'`. Matching one form counted
|
|
156
|
+
# half the entries as absent — a divergent-normalizer miss inside the check that exists to catch
|
|
157
|
+
# missing records. Known-pair calibrated below in test_dispatch_log_lanes.sh.
|
|
158
|
+
LOGGED=$(grep -cE "^- date: *'?$TODAY'?" "$LOG" 2>/dev/null | tr -d ' ' || echo 0)
|
|
159
|
+
if [ "${DISPATCHED:-0}" -gt 0 ] && [ "${LOGGED:-0}" -eq 0 ]; then
|
|
160
|
+
echo "❌ ④-e $DISPATCHED sub-agent dispatch(es) today and ZERO invocation-log entries — the 60/40"
|
|
161
|
+
echo " promotion gate and the UAP loop both read that file; an unlogged session is invisible to"
|
|
162
|
+
echo " them. Append to knowledge/shared/learnings/subagent_invocations_log.yaml (consolidated"
|
|
163
|
+
echo " per class is fine — record counts and outcomes, not one stub per dispatch)."
|
|
164
|
+
FAIL=1
|
|
165
|
+
elif [ "${DISPATCHED:-0}" -gt 0 ]; then
|
|
166
|
+
echo "✅ ④-e dispatch log: $DISPATCHED dispatch(es) today, $LOGGED log entr(ies) recorded"
|
|
167
|
+
elif [ "$HOOK_OK" -eq 1 ]; then
|
|
168
|
+
echo "✅ ④-e dispatch log: no sub-agent dispatches tallied today"
|
|
169
|
+
fi
|
|
170
|
+
|
|
120
171
|
# ⑤ CARD-LAST invariant — the card must be the NEWEST close artifact. A card older than
|
|
121
172
|
# fh_completed / signal files written this session = ⑤ ran before ①–④ finished (the bug class).
|
|
122
173
|
if [ -f "$CARD" ]; then
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_dispatch_log_lanes.sh — known-pair anchor for session_close_check.sh ④-e (dispatch-log
|
|
3
|
+
# reconciliation) and for the SubagentStop tally hook that feeds it.
|
|
4
|
+
#
|
|
5
|
+
# WHY (2026-08-02): CLAUDE.md makes an invocation-log entry mandatory immediately after any custom
|
|
6
|
+
# sub-agent invocation — it feeds the 60/40 promotion gate and the UAP loop. Measured: one session
|
|
7
|
+
# dispatched 20+ subagents and logged ZERO, in the same session that recovered that very log file
|
|
8
|
+
# from a branch queued for deletion. An obligation that loses 20/20 is not under-emphasised, it is
|
|
9
|
+
# unmechanized, so ④-e now reconciles a hook-written tally against the day's entries.
|
|
10
|
+
#
|
|
11
|
+
# The reconciliation itself then shipped a defect this suite exists to prevent from returning: the
|
|
12
|
+
# log carries TWO date spellings — `- date: 2026-08-02` for hand-written entries and
|
|
13
|
+
# `- date: '2026-08-02'` for anything appended through yaml.dump — and the first matcher saw only
|
|
14
|
+
# one, counting half the entries as absent. A missing-records check that itself miscounts records is
|
|
15
|
+
# the worst possible shape, so both spellings are lanes below.
|
|
16
|
+
#
|
|
17
|
+
# Exit 0 = the reconciliation discriminates · 1 = it would mis-report.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
CHECK="$SCRIPT_DIR/session_close_check.sh"
|
|
22
|
+
SETTINGS="$SCRIPT_DIR/../.claude/settings.json"
|
|
23
|
+
FAILED=0; PASS=0
|
|
24
|
+
chk() { if [ "$1" -eq 0 ]; then PASS=$((PASS+1)); echo " ✅ $2"; else FAILED=1; echo " ❌ $2"; fi; }
|
|
25
|
+
|
|
26
|
+
[ -f "$CHECK" ] || { echo "FAIL dispatch-log lanes: subject $CHECK missing"; exit 1; }
|
|
27
|
+
|
|
28
|
+
# The matcher under test, lifted from the subject rather than re-spelled (a hand-copied predicate is
|
|
29
|
+
# a divergent normalizer — which is exactly the bug being anchored). If it is gone, fail loudly.
|
|
30
|
+
MATCHER=$(grep -oE "grep -cE \"\^- date: \*'\?\\\$TODAY'\?\"" "$CHECK" | head -1)
|
|
31
|
+
if [ -z "$MATCHER" ]; then
|
|
32
|
+
echo "FAIL the ④-e date matcher is no longer in session_close_check.sh in the expected form —"
|
|
33
|
+
echo " this lane cannot verify what it claims to. Update the lane WITH the subject."
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
echo "── matcher located in the subject"
|
|
37
|
+
|
|
38
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
39
|
+
D=2026-08-02
|
|
40
|
+
|
|
41
|
+
echo "── date-spelling lanes (the shipped miscount) ──"
|
|
42
|
+
printf -- "- date: %s\n" "$D" > "$T/plain.yaml"
|
|
43
|
+
printf -- "- date: '%s'\n" "$D" > "$T/quoted.yaml"
|
|
44
|
+
printf -- "- date: %s\n- date: '%s'\n- date: 2020-01-01\n" "$D" "$D" > "$T/both.yaml"
|
|
45
|
+
count() { grep -cE "^- date: *'?$D'?" "$1" 2>/dev/null | tr -d ' '; }
|
|
46
|
+
[ "$(count "$T/plain.yaml")" = 1 ] ; chk $? "unquoted date counted"
|
|
47
|
+
[ "$(count "$T/quoted.yaml")" = 1 ] ; chk $? "yaml.dump-quoted date counted (the half the first matcher missed)"
|
|
48
|
+
[ "$(count "$T/both.yaml")" = 2 ] ; chk $? "mixed file counts both, and ignores another date"
|
|
49
|
+
# known-POSITIVE for the bug itself: the reverted single-form matcher DOES undercount
|
|
50
|
+
[ "$(grep -c "^- date: *$D" "$T/both.yaml")" = 1 ] ; chk $? "known-POSITIVE: the reverted matcher undercounts (1 of 2) — the defect is real"
|
|
51
|
+
|
|
52
|
+
echo "── reconciliation verdict lanes ──"
|
|
53
|
+
verdict() { # $1=dispatched $2=logged -> BLOCK | OK | NONE
|
|
54
|
+
if [ "$1" -gt 0 ] && [ "$2" -eq 0 ]; then echo BLOCK
|
|
55
|
+
elif [ "$1" -gt 0 ]; then echo OK
|
|
56
|
+
else echo NONE; fi
|
|
57
|
+
}
|
|
58
|
+
[ "$(verdict 20 0)" = BLOCK ] ; chk $? "dispatches with ZERO entries → BLOCK (this is the 2026-08-02 failure)"
|
|
59
|
+
[ "$(verdict 20 4)" = OK ] ; chk $? "consolidated logging (20 dispatches, 4 entries) → OK, not punished"
|
|
60
|
+
[ "$(verdict 1 1)" = OK ] ; chk $? "1:1 logging → OK"
|
|
61
|
+
[ "$(verdict 0 0)" = NONE ] ; chk $? "no dispatches → silent, not a failure"
|
|
62
|
+
|
|
63
|
+
echo "── the tally hook that feeds it ──"
|
|
64
|
+
if [ -f "$SETTINGS" ]; then
|
|
65
|
+
python3 - "$SETTINGS" <<'PY' >"$T/hookcmd" 2>/dev/null || true
|
|
66
|
+
import json,sys
|
|
67
|
+
d=json.load(open(sys.argv[1]))
|
|
68
|
+
g=d.get("hooks",{}).get("SubagentStop",[])
|
|
69
|
+
print(g[0]["hooks"][0]["command"] if g and g[0].get("hooks") else "")
|
|
70
|
+
PY
|
|
71
|
+
cmd=$(cat "$T/hookcmd")
|
|
72
|
+
[ -n "$cmd" ] ; chk $? "SubagentStop hook is configured (an untallied dispatch is invisible to ④-e)"
|
|
73
|
+
if [ -n "$cmd" ]; then
|
|
74
|
+
case "$cmd" in *"exit 0"*) ok=0 ;; *) ok=1 ;; esac
|
|
75
|
+
[ "$ok" -eq 0 ] ; chk $? "hook ends in exit 0 — a non-zero hook exit discards its stdout SILENTLY"
|
|
76
|
+
# run it against a scratch HUB and confirm it appends today's date
|
|
77
|
+
CLAUDE_PROJECT_DIR="$T/hub" bash -c "$cmd" >/dev/null 2>&1
|
|
78
|
+
[ "$(grep -c "$(date +%Y-%m-%d)" "$T/hub/tracks/_meta/.subagent_dispatch_tally" 2>/dev/null || echo 0)" -ge 1 ]
|
|
79
|
+
chk $? "hook actually appends a dated line when run (not merely present)"
|
|
80
|
+
fi
|
|
81
|
+
else
|
|
82
|
+
echo " ⏭️ .claude/settings.json absent — hook lanes unchecked (not a pass)"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo "── absence-is-not-zero (the fail-open this fix nearly shipped) ──"
|
|
86
|
+
# .claude/settings.json is GITIGNORED, so a fresh clone has no hook and an empty tally. Without an
|
|
87
|
+
# explicit branch the reconciliation would read that as "no dispatches" and pass in silence.
|
|
88
|
+
hookless_verdict() { # $1=hook_configured -> MEASURED | NOT-MEASURED
|
|
89
|
+
[ "$1" -eq 1 ] && echo MEASURED || echo NOT-MEASURED
|
|
90
|
+
}
|
|
91
|
+
[ "$(hookless_verdict 0)" = NOT-MEASURED ] ; chk $? "no hook → NOT MEASURED (an unmeasured count is not zero)"
|
|
92
|
+
[ "$(hookless_verdict 1)" = MEASURED ] ; chk $? "hook present → measured"
|
|
93
|
+
grep -q "NOT MEASURED" "$CHECK" ; chk $? "the subject actually carries that branch (not just this lane)"
|
|
94
|
+
[ -f "$SCRIPT_DIR/../templates/subagent-tally-hook.json" ] ; chk $? "installable snippet exists for a clone that has no settings.json"
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
if [ "$FAILED" -ne 0 ]; then echo "DISPATCH-LOG LANES: FAIL"; exit 1; fi
|
|
98
|
+
echo "DISPATCH-LOG LANES: PASS ($PASS/$PASS)"
|
|
99
|
+
exit 0
|
|
@@ -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
|
+
}
|