@chrono-meta/fh-gate 1.4.97 → 1.4.98

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 (38) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CATALOG.md +19 -0
  3. package/CHEATSHEET.md +9 -1
  4. package/CLAUDE.md +28 -2
  5. package/README.ja.md +10 -0
  6. package/README.ko.md +10 -0
  7. package/README.md +11 -0
  8. package/README.zh.md +10 -0
  9. package/docs/pillars.svg +3 -7
  10. package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
  11. package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
  12. package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
  13. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
  14. package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
  15. package/knowledge/shared/learnings/subagent_invocations_log.yaml +55 -0
  16. package/package.json +5 -1
  17. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  18. package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
  19. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  20. package/plugins/fh-meta/CHANGELOG.md +65 -0
  21. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
  22. package/scripts/degrade_direction_scan.sh +10 -1
  23. package/scripts/digest_landing_check.sh +20 -4
  24. package/scripts/fh_node_check.sh +60 -1
  25. package/scripts/fh_session_load.sh +22 -2
  26. package/scripts/frontier_digest_autopilot.sh +229 -0
  27. package/scripts/lane_runner_check.sh +237 -25
  28. package/scripts/package_coverage_check.sh +17 -0
  29. package/scripts/postinstall_notice.js +34 -0
  30. package/scripts/selfcheck.sh +111 -5
  31. package/scripts/test_degrade_scan_shell_probes.sh +75 -0
  32. package/scripts/test_field_canon_lanes.sh +29 -5
  33. package/scripts/test_lane_runner_lanes.sh +221 -0
  34. package/scripts/test_stale_clone_guard_lanes.sh +21 -7
  35. package/scripts/test_version_lockstep_lanes.sh +62 -0
  36. package/scripts/version_lockstep_check.sh +143 -1
  37. package/templates/.git-hooks/pre-commit +22 -1
  38. package/templates/degrade_direction_scan.sh +10 -1
@@ -74,6 +74,68 @@ T=$(_fixture he4 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); printf '{"name":"fh-meta"}
74
74
  _expect "HE-4 manifest with no version → exit 2, NOT 0" 2 "$T"
75
75
  _says "HE-4 → says which file carries none" "carries no version string" 1
76
76
 
77
+ # ── Self-restatement — found→extend into this lens (2026-08-14) ──────────────────────────────
78
+ # RS-KN pins the exact false positive the naive first draft threw against its OWN calibration
79
+ # target (scripts/lane_runner_check.sh): a loose \D{0,12} gap matched across an unrelated
80
+ # exit-code legend entry ("declared DEBT · 1 =") and across a before→after transition narration
81
+ # ("DEBT 2 → 0"). Both shapes are known-negatives — they describe history/enumeration, not a
82
+ # live contradiction — and must stay silent.
83
+
84
+ T=$(_fixture rs1 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
85
+ mkdir -p "$T/scripts"
86
+ cat > "$T/scripts/some_lane.sh" <<'EOF'
87
+ #!/usr/bin/env bash
88
+ # Exit: 0 = every suite is WIRED, EXEMPT, or declared DEBT · 1 = an undeclared suite has no runner
89
+ # THE TWO THAT WERE HERE ARE RE-WIRED (2026-08-14) — DEBT 2 → 0.
90
+ # WHAT `DEBT: 0` DOES NOT MEAN — read before quoting the number anywhere.
91
+ DEBT=()
92
+ EOF
93
+ _expect "RS-KN historical DEBT narration (2→0, exit-code legend) → exit 0" 0 "$T"
94
+ _says "RS-KN → does not fire on the transition/legend shapes" "self-restatement drift" 0
95
+
96
+ T=$(_fixture rs2 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
97
+ mkdir -p "$T/scripts"
98
+ cat > "$T/scripts/broken_lane.sh" <<'EOF'
99
+ #!/usr/bin/env bash
100
+ # DEBT: 12 unwired suites remain, tracked below.
101
+ # ... (later, uncorrected after a partial fix) ...
102
+ # still DEBT 9 by last count.
103
+ DEBT=()
104
+ EOF
105
+ _expect "RS-KP shell comment restates DEBT with two different numbers → exit 0 (advisory)" 0 "$T"
106
+ _says "RS-KP → names the file and the disagreeing values" \
107
+ 'broken_lane.sh :: comments state DEBT as \[9, 12\]' 1
108
+
109
+ T=$(_fixture rs3 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
110
+ mkdir -p "$T/plugins/fh-commons/skills/fake-skill"
111
+ cat > "$T/plugins/fh-commons/skills/fake-skill/SKILL.md" <<'EOF'
112
+ ---
113
+ name: fake-skill
114
+ description: uses v1.4.95 of the reference build
115
+ ---
116
+ # fake-skill
117
+
118
+ Body text never restates a version number, so there is nothing to cross-check against.
119
+ EOF
120
+ _expect "RS-KN2 SKILL.md frontmatter version, body silent → exit 0, no drift" 0 "$T"
121
+ _says "RS-KN2 → does not fire when body has zero version mentions" "self-restatement drift" 0
122
+
123
+ T=$(_fixture rs4 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
124
+ mkdir -p "$T/plugins/fh-commons/skills/fake-skill2"
125
+ cat > "$T/plugins/fh-commons/skills/fake-skill2/SKILL.md" <<'EOF'
126
+ ---
127
+ name: fake-skill2
128
+ description: ships as part of v1.4.95
129
+ ---
130
+ # fake-skill2
131
+
132
+ Elsewhere in this doc we note the shipped build is v1.4.92, which is the value users should
133
+ actually expect to see when they check their installed version.
134
+ EOF
135
+ _expect "RS-KP2 SKILL.md frontmatter v1.4.95 vs body v1.4.92 → exit 0 (advisory)" 0 "$T"
136
+ _says "RS-KP2 → names the file and the drifted version" \
137
+ 'fake-skill2/SKILL.md :: frontmatter states v{1.4.95}' 1
138
+
77
139
  echo
78
140
  echo "──────────────────────────────────────────────"
79
141
  if [ "$FAIL" -gt 0 ]; then
@@ -22,7 +22,7 @@ ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
22
22
  command -v python3 >/dev/null 2>&1 || { echo "LOCKSTEP: HARNESS-ERROR — python3 unavailable"; exit 2; }
23
23
 
24
24
  python3 - "$ROOT" <<'PY'
25
- import json, sys, glob, os
25
+ import json, sys, glob, os, re
26
26
  root = sys.argv[1]
27
27
  pkg_path = os.path.join(root, 'package.json')
28
28
  try:
@@ -69,6 +69,39 @@ for p in targets:
69
69
  if got != want:
70
70
  drift.append(f" {rel} :: {label} = {got} (package.json = {want})")
71
71
 
72
+ # ── CHANGELOG is a shipped version surface too, and it was outside this check ─────────────────
73
+ # Measured 2026-08-13, by a peer session, AFTER 1.4.97 was already published: the four JSON
74
+ # manifests all read 1.4.97 and plugins/fh-meta/CHANGELOG.md's newest entry was still [1.4.96].
75
+ # So the tarball a consumer downloads carries a changelog with no entry for the version they just
76
+ # installed — six merged PRs invisible on the record surface. This check reported PASS while that
77
+ # was true, because its target list was "manifests carrying a JSON version field" rather than
78
+ # "surfaces that state which version this is".
79
+ # ★ That is the half-fix propagation boundary in its purest form: the version was updated
80
+ # everywhere the CHECK looked, which is not the same set as everywhere it MATTERS.
81
+ # Non-blocking is deliberate and narrow: a missing entry is a documentation gap, not a broken
82
+ # package, and turning a publish red on prose would train the override this repo has already
83
+ # measured people reaching for. It is LOUD, it names the file, and it is impossible to miss in the
84
+ # publish output — which is what the JSON drift arms could not have been, since those genuinely
85
+ # break installs.
86
+ CHANGELOGS = sorted(glob.glob(os.path.join(root, 'plugins', '*', 'CHANGELOG.md')))
87
+ for p in CHANGELOGS:
88
+ rel = os.path.relpath(p, root)
89
+ try:
90
+ txt = open(p, encoding='utf-8', errors='replace').read()
91
+ except OSError as e:
92
+ print(f"LOCKSTEP: HARNESS-ERROR — cannot read {rel}: {e}")
93
+ sys.exit(2)
94
+ # The heading form this repo uses: `### [x.y.z] — DATE`. Absence of ANY such heading means the
95
+ # extractor stopped matching the file's real shape — report that rather than a clean run.
96
+ heads = re.findall(r'^#+\s*\[(\d+\.\d+\.\d+)\]', txt, re.M)
97
+ if not heads:
98
+ print(f"LOCKSTEP: HARNESS-ERROR — {rel} has no `[x.y.z]` heading; the extractor is blind, "
99
+ f"which is not the same as the changelog being current")
100
+ sys.exit(2)
101
+ if want not in heads:
102
+ print(f"LOCKSTEP: ⚠️ {rel} has no entry for {want} (newest is {heads[0]}) — the published "
103
+ f"tarball would carry a changelog that does not mention the version it ships")
104
+
72
105
  if drift:
73
106
  print(f"LOCKSTEP: DRIFT — {len(drift)} of {checked} shipped version string(s) do not match package.json")
74
107
  print("\n".join(drift))
@@ -76,5 +109,114 @@ if drift:
76
109
  print("so a stale entry ships as 'already installed' on the runtime that cannot report it.")
77
110
  sys.exit(1)
78
111
 
112
+ # ── Self-restatement — a file that states its own count/version twice can drift from itself ──
113
+ # Measured 2026-08-13 (peer session, reship axis): N=2 on two DIFFERENT surfaces, both real —
114
+ # (a) a memory file's YAML frontmatter `description:` stated an npm version differently from
115
+ # its own body (1.4.95 vs 1.4.92 — a lockstep drift, just in prose instead of JSON)
116
+ # (b) lane_runner_check.sh's own header comment stated "11/8" while a later comment said the
117
+ # DEBT count was 12 — caught by cross-family review, not by re-reading (see that file's own
118
+ # header for the incident write-up; it has since self-corrected into a historical note).
119
+ # «타표면 재발 = 기계화 의무» (recurrence on a second, different surface obligates mechanizing) —
120
+ # found→extend into THIS lens rather than a new scanner: same idiom (extract candidate values,
121
+ # compare, collect drift, report), new target surfaces.
122
+ #
123
+ # 🟥 (a) IS NOT CLOSED BY THIS ARM, AND THAT IS DELIBERATE — cross-family review (2026-08-14)
124
+ # caught an earlier draft of this comment reading as though citing incident (a) meant this scan
125
+ # reaches it. It does not: the memory store lives under the operator's home directory
126
+ # (~/.claude/projects/.../memory/), outside this repo's `root` entirely, so the candidate glob
127
+ # below structurally cannot see it — this script runs at `npm publish` time (package.json
128
+ # prepublishOnly), where only the git-tracked repo exists (CI has no access to a contributor's
129
+ # local home directory, and different operators have different paths). Incident (a) is the
130
+ # MOTIVATING PRECEDENT for the general pattern, not a case this specific scope closes. Closing it
131
+ # for real needs a separate, local-only check over the memory store — a different surface with
132
+ # different constraints, not a widening of this one.
133
+ #
134
+ # Advisory (like the CHANGELOG check above), same reasoning: a stale self-count is a documentation
135
+ # defect, not a broken package, and turning publish red on prose trains the override this repo has
136
+ # already measured people reaching for.
137
+ #
138
+ # Scope, deliberately narrow — the two false-positive traps that would have made this noise instead
139
+ # of signal, both avoided on purpose:
140
+ # 1. tracks/**/*.md and this repo's session cards are EXCLUDED. Those are running logs that
141
+ # legitimately cite dozens of historical version numbers across dated sessions — scanning them
142
+ # for "two differing v-numbers in one file" would fire on nearly every one. The mission here is
143
+ # "does a single-snapshot doc contradict itself", not "every version ever mentioned".
144
+ # 2. The DEBT counter only fires on 2+ DISTINCT values for the SAME literal label ("DEBT") within
145
+ # comment lines of one file — not on any two numbers that happen to appear near each other. A
146
+ # one-off historical narration ("this said 11/8 until it was fixed") does not by itself carry
147
+ # two live DEBT-labeled claims, so it does not false-positive as an active contradiction.
148
+ def _self_restate_md():
149
+ hits = []
150
+ candidates = [os.path.join(root, 'CLAUDE.md'), os.path.join(root, 'AGENTS.md')]
151
+ candidates += sorted(glob.glob(os.path.join(root, 'plugins', '*', 'skills', '*', 'SKILL.md')))
152
+ # v-prefix optional — cross-family review found the real incident that motivated this arm
153
+ # (a memory file's frontmatter `latest v1.4.97` vs body `latest **1.4.97**`) does not carry the
154
+ # prefix on the body side, so the mandatory-v form never fires on the actual shape it exists
155
+ # to catch. Bare N.N.N is noisier (could match an unrelated dependency version), but the
156
+ # candidate list above is narrow enough (CLAUDE.md/AGENTS.md/SKILL.md only) that this stays
157
+ # advisory-tolerable rather than explosive.
158
+ VERPAT = re.compile(r'\bv?(\d+\.\d+\.\d+)\b')
159
+ for p in candidates:
160
+ if not os.path.exists(p):
161
+ continue
162
+ try:
163
+ txt = open(p, encoding='utf-8', errors='replace').read()
164
+ except OSError:
165
+ continue
166
+ m = re.match(r'^---\n(.*?)\n---\n(.*)$', txt, re.S)
167
+ if not m:
168
+ continue
169
+ fm, body = m.group(1), m.group(2)
170
+ fm_vers = set(VERPAT.findall(fm))
171
+ body_vers = set(VERPAT.findall(body))
172
+ drifted = fm_vers - body_vers
173
+ if fm_vers and body_vers and drifted:
174
+ rel = os.path.relpath(p, root)
175
+ hits.append(f" {rel} :: frontmatter states v{{{','.join(sorted(drifted))}}} not "
176
+ f"found anywhere in body (body has v{{{','.join(sorted(body_vers))}}})")
177
+ return hits
178
+
179
+ def _self_restate_sh():
180
+ hits = []
181
+ # Tight: DEBT immediately followed by (optional :/=, whitespace) a number — not "any digit
182
+ # within 12 chars" (that matched an unrelated exit-code legend entry across a bullet, «DEBT ·
183
+ # 1»). A number immediately followed by an arrow (→ / ->) is a before→after transition
184
+ # narration ("DEBT 2 → 0"), not a live claim about the current count — excluded on purpose,
185
+ # calibrated against this repo's own lane_runner_check.sh (known-negative: 3 raw matches with
186
+ # the loose form, 1 real match with this form — verified by hand, 2026-08-14).
187
+ DEBTPAT = re.compile(r'\bDEBT\b\s*[:=]?\s*(\d+)\b(?!\s*(?:→|->))')
188
+ # test_*.sh / *_lanes.sh are excluded — measured 2026-08-14 on this check's own test file:
189
+ # a known-pair fixture legitimately embeds two differing DEBT numbers as heredoc TEST DATA
190
+ # (proving the detector can tell them apart), and that heredoc text is also literal source in
191
+ # the test file itself. Those are fixtures, not a claim about the test file's own state — same
192
+ # distinction the `suites` glob elsewhere in this repo already draws between subject scripts
193
+ # and the test scripts that exercise them.
194
+ for p in (sorted(f for f in glob.glob(os.path.join(root, 'scripts', '*.sh'))
195
+ if not re.match(r'^(test_.*|.*_lanes)\.sh$', os.path.basename(f)))
196
+ + sorted(f for f in glob.glob(os.path.join(root, 'templates', '*.sh'))
197
+ if not re.match(r'^(test_.*|.*_lanes)\.sh$', os.path.basename(f)))):
198
+ try:
199
+ txt = open(p, encoding='utf-8', errors='replace').read()
200
+ except OSError:
201
+ continue
202
+ # Comment lines only — a live `DEBT=()` array literal is code computing the fact, not a
203
+ # restated claim about it, and must not be treated as a second (possibly stale) copy.
204
+ vals = set()
205
+ for ln in txt.splitlines():
206
+ if not re.match(r'^\s*#', ln):
207
+ continue
208
+ vals |= {int(m) for m in DEBTPAT.findall(ln)}
209
+ if len(vals) > 1:
210
+ rel = os.path.relpath(p, root)
211
+ hits.append(f" {rel} :: comments state DEBT as {sorted(vals)} — pick one number, "
212
+ f"or point the stale comment at the live count instead of a literal")
213
+ return hits
214
+
215
+ restate_hits = _self_restate_md() + _self_restate_sh()
216
+ if restate_hits:
217
+ print(f"LOCKSTEP: ⚠️ self-restatement drift — {len(restate_hits)} file(s) state their own "
218
+ f"version/count more than once and the copies disagree:")
219
+ print("\n".join(restate_hits))
220
+
79
221
  print(f"LOCKSTEP: PASS — {checked} shipped version string(s) all at {want}")
80
222
  PY
@@ -1072,7 +1072,28 @@ if [ -n "$LOADBEARING" ]; then
1072
1072
  echo "[Gate] load-bearing change — degrade lint (advisory) + cross-family acknowledgment..."
1073
1073
  DDSCAN="$REPO_ROOT/scripts/degrade_direction_scan.sh"
1074
1074
  if [ -f "$DDSCAN" ]; then
1075
- _dd=$(bash "$DDSCAN" 2>/dev/null \
1075
+ # SCOPE THE SCAN TO THE STAGED FILES. This used to call `bash "$DDSCAN"` with NO ARGUMENTS,
1076
+ # which makes the scanner walk the WHOLE REPOSITORY recursively — and then the very next pipe
1077
+ # threw almost all of it away, keeping only the lines naming files we already had in hand.
1078
+ # Measured on this repo 2026-08-13: whole-repo 42s / 260 lines · one staged file 0s / 2 lines.
1079
+ # A sibling harness measured the same call under load at 2m07s inside a commit that took
1080
+ # 7m54s–9m40s, one of which hit a 10-minute tool ceiling and could not complete at all.
1081
+ # ★ The structure was inverted: it held the target list, scanned everything, then filtered.
1082
+ # WHY THIS IS WORSE THAN A SLOW CHECK: this lane declares itself ADVISORY (see the doctrine line
1083
+ # above — FP-tolerant, never a solo block). So a lane that cannot block was consuming most of
1084
+ # every load-bearing commit. That trains `--no-verify`, and the SAME hook carries the
1085
+ # Destructive-Op gate — one learned bypass disarms an irreversible-surface gate too. CLAUDE.md
1086
+ # warns repeatedly that over-blocking trains the override; over-DELAYING gets there just as well.
1087
+ # Read into an array rather than relying on word-splitting: $LOADBEARING is newline-separated and
1088
+ # an unquoted expansion would also split on spaces. `while read` and not `mapfile`, because
1089
+ # mapfile does not exist on bash 3.2 (stock macOS), which this hook still has to run on.
1090
+ _dd_targets=()
1091
+ while IFS= read -r _p; do
1092
+ [ -n "$_p" ] && _dd_targets+=("$_p")
1093
+ done <<< "$LOADBEARING"
1094
+ # The grep filter is KEPT even though the scan is now scoped: the scanner emits summary lines
1095
+ # ("degrade-scan: N smell(s) …") that name no file, and those must not read as findings.
1096
+ _dd=$(bash "$DDSCAN" "${_dd_targets[@]}" 2>/dev/null \
1076
1097
  | grep -Ff <(printf '%s\n' "$LOADBEARING") 2>/dev/null | head -5 || true)
1077
1098
  if [ -n "$_dd" ]; then
1078
1099
  echo " ⚠️ degrade-direction smell(s) in the staged load-bearing files (advisory):"
@@ -500,11 +500,20 @@ for f in "${FILES[@]}"; do
500
500
  # `tok in text` (paid⊂prepaid) when the variables aren't named verdict/state (M#4, steel-quench).
501
501
  # C2 closes that: simple var-in-var (not a collection literal / range / for) = a likely
502
502
  # containment check that should be exact/word-boundary if it grounds a verdict. Higher noise; advisory.
503
+ # 2026-08-14: pinned an ALL-CAPS-constant exclusion here that Probe C already carries a few lines
504
+ # up (`in [A-Z_]+\b`) — C2 had drifted out of sync with it. Measured on this repo's own scripts/:
505
+ # 6 raw C2 hits, all `if VAR in {seen,by_name,accepted,packed,PLACEHOLDERS}:` — genuine collection
506
+ # membership, not the substring-containment smell this probe exists to catch. Do NOT also add
507
+ # lowercase collection-name heuristics (`seen`/`by_name`/...) here — that class still needs a
508
+ # human to read the assignment and confirm it is a set/dict/list, and a naming guess would just
509
+ # move the false-negative risk instead of removing it. This narrows one already-established class,
510
+ # it does not invent a new one.
503
511
  while IFS= read -r m; do
504
512
  emit "$f" "${m%%:*}" "C2:substring-boolean" "bare 'X in Y' in if/return/assert — if this grounds a presence/verdict check, use exact/word-boundary match, not containment"
505
513
  done < <(grep -nE '^[[:space:]]*(if|elif|return|assert|while)[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]+in[[:space:]]+[A-Za-z_][A-Za-z0-9_.]*[[:space:]]*[:)]?[[:space:]]*$' "$f" 2>/dev/null \
506
514
  | grep -vE '\bfor\b|in range|in enumerate|not in' \
507
- | grep -vE '\b(verdict|present|ground|state|match|expected)\w*\b')
515
+ | grep -vE '\b(verdict|present|ground|state|match|expected)\w*\b' \
516
+ | grep -vE 'in [A-Z_]+[[:space:]]*[:)]?[[:space:]]*$')
508
517
 
509
518
  # Probe E — negated-falsy guard returning permissive (dominance-benchmark round-2 f2 class): an error
510
519
  # SENTINEL (None / {} / "" / []) is falsy, so `if not X: return <PASS>` treats "the check errored / never