@chrono-meta/fh-gate 1.4.97 β†’ 1.4.99

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 (44) 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 +229 -42
  6. package/README.ko.md +241 -45
  7. package/README.md +168 -31
  8. package/README.zh.md +219 -40
  9. package/docs/OUTPUT_EVIDENCE.md +21 -12
  10. package/docs/pillars.svg +3 -7
  11. package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
  12. package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
  13. package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
  14. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
  15. package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
  16. package/knowledge/shared/learnings/subagent_invocations_log.yaml +65 -0
  17. package/package.json +5 -1
  18. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  19. package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
  20. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  21. package/plugins/fh-meta/CHANGELOG.md +166 -0
  22. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
  23. package/scripts/consent_registry_check.sh +124 -1
  24. package/scripts/degrade_direction_scan.sh +10 -1
  25. package/scripts/digest_landing_check.sh +20 -4
  26. package/scripts/fh_node_check.sh +128 -1
  27. package/scripts/fh_session_load.sh +22 -2
  28. package/scripts/frontier_digest_autopilot.sh +229 -0
  29. package/scripts/lane_runner_check.sh +294 -26
  30. package/scripts/package_coverage_check.sh +17 -0
  31. package/scripts/postinstall_notice.js +34 -0
  32. package/scripts/selfcheck.sh +183 -5
  33. package/scripts/test_consent_registry.sh +99 -0
  34. package/scripts/test_degrade_scan_shell_probes.sh +75 -0
  35. package/scripts/test_field_canon_lanes.sh +29 -5
  36. package/scripts/test_lane_runner_lanes.sh +295 -0
  37. package/scripts/test_node_check_lanes.sh +217 -0
  38. package/scripts/test_selfcheck_state_lanes.sh +61 -0
  39. package/scripts/test_stale_clone_guard_lanes.sh +21 -7
  40. package/scripts/test_version_lockstep_lanes.sh +62 -0
  41. package/scripts/version_lockstep_check.sh +143 -1
  42. package/templates/.git-hooks/pre-commit +22 -1
  43. package/templates/consent_classes.yaml.example +30 -0
  44. package/templates/degrade_direction_scan.sh +10 -1
@@ -121,30 +121,37 @@ EXEMPT=(
121
121
  # 11 not 12), and test_marker_floor_lanes.sh calibrates pre-commit's live validate_marker_floor().
122
122
  # Both are wired now.
123
123
  #
124
- # πŸŸ₯ THE TWO BELOW WERE WIRED, MEASURED RED IN CI, AND DELIBERATELY MOVED BACK. Making them green
125
- # was available and is the wrong move: their failures are TRUE β€” the suites really do depend on
126
- # things a CI runner does not have β€” so forcing a pass would be routing a real failure to green in
127
- # the same delta that spent nine repairs closing exactly that. A suite whose preconditions are
128
- # undeclared belongs in the todo list, not in the mandatory path.
129
- DEBT=(
130
- # PASS 19 Β· FAIL 0 locally; PASS 12 Β· FAIL 7 in CI. Reproduced locally by pointing HOME at an
131
- # empty directory β€” 12/7, the same split β€” so the dependency is exact and not a guess:
132
- # field_canon_preload.sh resolves `${HOME}/projects` and seven lanes assume the operator's
133
- # mapped-project layout is there. Nothing in the suite declares that precondition, so on a
134
- # machine without it the suite reports a REGRESSION rather than "not exercised here".
135
- # Fix before re-wiring: the suite must detect the missing layout and exit NOT-EXERCISED (the
136
- # shape test_sessionstart_multihook_lanes.sh already uses for its CLI dependency), or build its
137
- # own fixture HOME for those seven the way it already does for the no-HOME lane.
138
- "test_field_canon_lanes.sh"
139
- # 17/17 locally; 2/17 in CI. And this one was PREDICTED: the adversarial review of this very
140
- # delta flagged `elapsed < 10` as a wall-clock assertion that would go falsely red on a loaded CI
141
- # runner, naming this file and that line. It was filed as a low-severity residual and not acted
142
- # on; CI then produced exactly it. β˜… The finding named a MECHANISM (a wall-clock assertion is now
143
- # on a mandatory path), and a mechanism does not become less true for being labelled R.
144
- # Fix before re-wiring: give the timing assertions headroom or gate them behind an explicit
145
- # opt-in, so the default run measures logic and not the runner's load.
146
- "test_stale_clone_guard_lanes.sh"
147
- )
124
+ # 🟒 THE TWO THAT WERE HERE ARE RE-WIRED (2026-08-14) β€” DEBT 2 β†’ 0. Both entries below are
125
+ # HISTORICAL: they record why the suites were pulled out and what the fix had to be, kept because
126
+ # the reasoning (and the exact CI-reproduction numbers) are load-bearing evidence for the next
127
+ # person who touches either suite, not because either is still debt.
128
+ #
129
+ # test_field_canon_lanes.sh β€” was: PASS 19 Β· FAIL 0 locally; PASS 12 Β· FAIL 7 in CI. Reproduced
130
+ # locally by pointing HOME at an empty directory β€” 12/7, the same split β€” so the dependency was
131
+ # exact and not a guess: field_canon_preload.sh resolves `${HOME}/projects` and seven lanes assumed
132
+ # the operator's mapped-project layout was there. Nothing in the suite declared that precondition,
133
+ # so on a machine without it the suite reported a REGRESSION rather than "not exercised here". Fix
134
+ # (built the second option this entry named, not the first): the suite now carries its own fixture
135
+ # hub+project-root (mirroring the shape lane β‘­ already used) instead of depending on the operator's
136
+ # real filesystem β€” `run()`/`runD()` inject `CLAUDE_PROJECT_DIR`/`FIELD_CANON_PROJECT_ROOT` at a
137
+ # fixed fixture path. Verified both directions: PASS 19/19 under an empty HOME (the exact CI
138
+ # reproduction) AND a known-negative check β€” breaking the `-dev`-suffix repo-resolution fallback in
139
+ # field_canon_preload.sh reproduces the identical PASS 12 / FAIL 7 split, so the fixture measures
140
+ # real behavior, not a vacuously-green rewrite.
141
+ #
142
+ # test_stale_clone_guard_lanes.sh β€” was: 17/17 locally; 2/17 in CI. Predicted by an adversarial
143
+ # review of the delta that introduced `elapsed < 10` as a wall-clock assertion on a mandatory path;
144
+ # CI then produced exactly it. Fix (the first option this entry named): the wedge sleep widened
145
+ # 30s→60s and the assertion changed from a fixed `elapsed < 10` to a relative `elapsed <
146
+ # (wedge/2)=30s` β€” proving the guard's internal 0.5s budget bounded it (60x headroom) rather than an
147
+ # absolute constant tuned to one machine's speed. Known-negative check: neutering the budget's kill
148
+ # condition in stale_clone_guard.sh (so it waits out the full wedge instead of self-bounding)
149
+ # correctly fails exactly this one lane, confirming the widened threshold still discriminates.
150
+ #
151
+ # Re-wired into scripts/selfcheck.sh's pair-loop alongside the other ten (found→extend, same shape:
152
+ # "scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" and
153
+ # "scripts/stale_clone_guard.sh|scripts/test_stale_clone_guard_lanes.sh").
154
+ DEBT=()
148
155
 
149
156
  if [ "${1:-}" = "--list-debt" ]; then
150
157
  # Same bash-3.2 empty-array guard as the scan call below. This site was MISSED when that one was
@@ -169,6 +176,96 @@ if [ -f scripts/selfcheck.sh ] && ! grep -qE '^[[:space:]]*(if !? ?)?bash script
169
176
  exit 1
170
177
  fi
171
178
 
179
+ # ── ORG SEAM β€” a downstream org declares ITS OWN suites in ITS OWN file ───────────────────────
180
+ # Why this exists (requested 2026-08-13 by a downstream org fork of this harness, with evidence):
181
+ # EXEMPT and DEBT above are hardcoded arrays in a SHARED-LAYER file. A downstream fork carries
182
+ # suites this repo has never heard of (`test_rest_push_lanes.sh`, `test_company_delta_lanes.sh` …),
183
+ # and all three remedies this check offers land in files that fork does not own:
184
+ # wire into selfcheck.sh β†’ shared layer Β· EXEMPT array β†’ this file, shared layer
185
+ # DEBT array β†’ this file, shared layer
186
+ # So the fork's only choices were "violate the sync boundary" or "live at rc=1 forever". Measured:
187
+ # 5 such suites on their first run of this check. That is not a preference β€” a permanently red
188
+ # check stops being read, and one of those five suites had just caught a real regression when it
189
+ # was run by hand.
190
+ #
191
+ # Shape: OPTIONAL file, absent by default. This repo ships none, so the no-op arm is the arm that
192
+ # runs here β€” verified by the known-pair in scripts/test_lane_runner_lanes.sh.
193
+ #
194
+ # πŸŸ₯ A MALFORMED FILE IS FAIL-CLOSED, a stale entry is advisory. Those are different failures:
195
+ # Β· cannot parse β†’ the instrument did not look. Reporting "no declarations" would render
196
+ # UNMEASURED as ZERO, which is the exact family this repo keeps closing.
197
+ # Β· names a suite that no longer exists β†’ hygiene, same as a stale DEBT entry above, so it warns
198
+ # with the same voice. Making the downstream rule STRICTER than the upstream
199
+ # one it mirrors would just train people to delete the file.
200
+ ORG_DECL="company/lane_declarations.yaml"
201
+ ORG_EXEMPT=(); ORG_DEBT=()
202
+ if [ -f "$ORG_DECL" ]; then
203
+ _org_out=$(python3 - "$ORG_DECL" <<'ORGPY'
204
+ import re, sys
205
+ path = sys.argv[1]
206
+ try:
207
+ txt = open(path, encoding='utf-8').read()
208
+ except OSError as e:
209
+ print(f"ERR\tcannot read {path}: {e}"); sys.exit(0)
210
+ sect = None; seen = {}
211
+ for i, ln in enumerate(txt.splitlines(), 1):
212
+ s = ln.split('#', 1)[0].rstrip()
213
+ if not s.strip():
214
+ continue
215
+ m = re.match(r'^([A-Za-z_]+):\s*$', s)
216
+ if m:
217
+ sect = m.group(1)
218
+ if sect not in ('exempt', 'debt'):
219
+ print(f"ERR\t{path}:{i}: unknown section '{sect}' (expected 'exempt' or 'debt')")
220
+ sys.exit(0)
221
+ continue
222
+ m = re.match(r'^\s+-\s+(\S+)\s*$', s)
223
+ if m:
224
+ if sect is None:
225
+ print(f"ERR\t{path}:{i}: entry before any section header"); sys.exit(0)
226
+ name = m.group(1)
227
+ if name in seen and seen[name] != sect:
228
+ print(f"ERR\t{path}:{i}: '{name}' declared both exempt and debt β€” ambiguous")
229
+ sys.exit(0)
230
+ seen[name] = sect
231
+ print(f"{sect}\t{name}")
232
+ continue
233
+ print(f"ERR\t{path}:{i}: unparseable line: {s.strip()[:60]}")
234
+ sys.exit(0)
235
+ ORGPY
236
+ )
237
+ # `grep -q '^ERR'` β€” NOT `'^ERR\t'`. In a POSIX BRE, GNU grep reads `\t` as a literal `t`
238
+ # (so the pattern becomes `^ERRt` and matches nothing) while BSD grep on macOS takes it as a tab.
239
+ # Measured 2026-08-13: this exact line passed all 11 lanes locally and failed L6/L7/L8 in CI β€”
240
+ # the fail-closed arm was silently dead on Linux, which is the platform that gates merges.
241
+ # Same family as the `"$DDSCAN" "` anchor divergence a sibling harness measured the same day:
242
+ # a regex that means different things on the two greps is an instrument, and an instrument that
243
+ # answers differently per platform has not measured anything.
244
+ if printf '%s' "$_org_out" | grep -q '^ERR'; then
245
+ echo "FAIL lane-runner: $ORG_DECL exists but could not be read as declarations β€”"
246
+ printf '%s' "$_org_out" | sed 's/^ERR[[:space:]]*/ /'
247
+ echo " An unreadable declaration file is UNMEASURED, not empty. Fix the file or delete it."
248
+ exit 2
249
+ fi
250
+ while IFS=$'\t' read -r _k _v; do
251
+ [ -z "${_v:-}" ] && continue
252
+ case "$_k" in
253
+ exempt) ORG_EXEMPT+=("$_v") ;;
254
+ debt) ORG_DEBT+=("$_v") ;;
255
+ esac
256
+ done <<< "$_org_out"
257
+ # Stale-entry warning, same voice as the DEBT hygiene warnings below.
258
+ for _n in "${ORG_EXEMPT[@]+"${ORG_EXEMPT[@]}"}" "${ORG_DEBT[@]+"${ORG_DEBT[@]}"}"; do
259
+ [ -f "scripts/$_n" ] || echo "⚠️ lane-runner: $ORG_DECL declares '$_n' but scripts/$_n does not exist (renamed/deleted?)"
260
+ done
261
+ # Visibility: an org-declared suite must never be indistinguishable from an upstream one. A
262
+ # reader who sees "0 debt" has to be able to tell whether that is this repo's zero or a fork's
263
+ # declaration absorbing its own list.
264
+ echo " lane-runner: $ORG_DECL β€” ${#ORG_EXEMPT[@]} exempt Β· ${#ORG_DEBT[@]} debt (org-declared)"
265
+ EXEMPT+=("${ORG_EXEMPT[@]+"${ORG_EXEMPT[@]}"}")
266
+ DEBT+=("${ORG_DEBT[@]+"${ORG_DEBT[@]}"}")
267
+ fi
268
+
172
269
  # `"${ARR[@]+"${ARR[@]}"}"` and not the plain `"${ARR[@]}"`: on bash 3.2 (stock macOS) `set -u`
173
270
  # treats an EMPTY array's expansion as an unbound variable and aborts. DEBT is now empty by design,
174
271
  # which is exactly the state the plain form cannot survive β€” measured here 2026-08-13, and it
@@ -256,11 +353,150 @@ def runner_dispatches(suite, txt):
256
353
 
257
354
  wired = {s for s in suites if has_runner(s)}
258
355
 
356
+ # ── Embedded --self-test dispatchers β€” a class the name-pattern `suites` glob cannot see ──────
357
+ # Measured 2026-08-13, the header of this file, Β§WHAT DEBT:0 DOES NOT MEAN: a lane suite that
358
+ # lives INSIDE its subject as a `--self-test` flag β€” not a separate `test_*.sh`/`*_lanes.sh` file
359
+ # β€” is structurally invisible to the glob above. When this block was written, 4 such scripts were
360
+ # known and none showed up as WIRED or UNWIRED anywhere in this report.
361
+ # ⚠️ Do not read that 4 as a current figure, and do not re-list the names here. The set is
362
+ # DISCOVERED at run time by `_st_candidates` below and the live figure is printed in this file's
363
+ # own SELFTEST_COUNTS line, so a hand-maintained example list beside a self-discovering scan is a
364
+ # second source of truth that goes stale in silence. That is not hypothetical: the previous
365
+ # version of this comment named a 4th example the code no longer agreed with, and the session that
366
+ # noticed had to trace the discrepancy to ground by hand.
367
+ # The wiring line for the subjects that ARE wired,
368
+ # the `for _subj in ...` loop in scripts/selfcheck.sh, could be deleted and nothing here would
369
+ # go red. found→extend, not a new file: same idiom as `suites`/`has_runner` above — discover
370
+ # subjects, detect dispatch, report undeclared β€” new predicates for the shape this pattern uses.
371
+ #
372
+ # πŸŸ₯ A PARENTHESIS TRAP LIVES IN THIS SPECIFIC HEREDOC, READ BEFORE ADDING A LINE HERE.
373
+ # Measured 2026-08-14: a single heredoc-body line whose own paren count was unbalanced β€” one more
374
+ # close-paren than open-paren, from a regex needing a literal close-paren character β€” broke bash's
375
+ # parse of every line after it, on THIS file only. The reason is that the heredoc below sits inside
376
+ # a command-substitution wrapper, and that wrapper's own close-paren-matching scan turned out not
377
+ # to be fully heredoc-blind in the bash build this repo has tested. `bash -n` then failed dozens of
378
+ # lines later with an unrelated-looking error, because by then the parser believed the heredoc had
379
+ # already closed. Reproduced in isolation: a bare regex assignment needing a literal close-paren,
380
+ # spliced into this file at this exact position, alone, with nothing else added. The fix is
381
+ # structural, not "be careful" β€” every line added inside this heredoc must carry a matched
382
+ # open-paren and close-paren count on that same line, and a matched pair spanning two lines is
383
+ # exactly the shape that tripped this the first time. Collapse it back to one line, or spell the
384
+ # literal paren out as an escape sequence instead of a bare character, rather than splitting it
385
+ # across lines.
386
+ # A bare substring match on --self-test would also catch prose that only DISCUSSES the flag
387
+ # (measured: scripts/selfcheck.sh's `_subj` loop carries a comment naming it as what NOT to grep
388
+ # for, which is exactly the false positive this narrower check exists to avoid). Require one of
389
+ # the two real dispatcher shapes instead: `"--self-test"` in a quoted comparison, or `--self-test)`
390
+ # as a bare case-pattern. The close-paren is built via chr β€” see the paren-trap note above; a
391
+ # literal `)` character on this line, however it is spelled, throws this file's parser off.
392
+ _CP = chr(41)
393
+ SELFTEST_DISPATCH_FORMS = ('"--self-test"', '--self-test' + _CP)
394
+
395
+ def _read(path):
396
+ try:
397
+ return open(path, encoding='utf-8', errors='replace').read()
398
+ except OSError:
399
+ return ''
400
+
401
+ # πŸŸ₯ NAMED RESIDUALS in the self-test branch β€” two asymmetries the 2026-08-15 repair did NOT close,
402
+ # written here because this file names its other residuals and silence would read as coverage.
403
+ # 1. Comment-stripping is LINE-PREFIX only. A dispatch line living inside a heredoc body or
404
+ # inside an echoed string is not a comment and still counts as a live caller. Symmetric with
405
+ # runner_dispatches, which has the same limit β€” so "symmetry restored" is true, and the shared
406
+ # blind spot survives in both. In-tree example of the shape: the heredoc fixtures in
407
+ # scripts/test_lane_runner_lanes.sh. Harmless today only because those name a fixture subject.
408
+ # 2. Subject DISCOVERY below still reads comments. `_st_names` matches the raw dispatcher forms
409
+ # anywhere in the file, so a comment that merely mentions the flag can enrol a script as a
410
+ # self-test subject. Real case: chamber_witness.sh carries the flag inside a usage comment.
411
+ # Harmless today because that file also has a real dispatcher β€” but it means "a mention is not
412
+ # a declaration" is enforced on the RUNNER side and not on the SUBJECT side.
413
+ # Neither is mechanized: closing them means parsing shell rather than scanning lines, which is the
414
+ # Grep-Collision Treadmill this repo has already logged. Fix on the first case that actually bites.
415
+ #
416
+ # One line on purpose β€” see the paren-trap note above the SELFTEST_PAT definition.
417
+ _st_candidates = [f for f in glob.glob('scripts/*.sh') if os.path.basename(f) not in suites and os.path.basename(f) != 'lane_runner_check.sh']
418
+ _st_names = [os.path.basename(f)[:-3] for f in _st_candidates if any(_form in _read(f) for _form in SELFTEST_DISPATCH_FORMS)]
419
+ selftest_subjects = sorted(set(_st_names))
420
+
421
+ def selftest_dispatched(bare_name, txt):
422
+ """Two shapes, both real in this repo. Cross-family review (2026-08-14) caught the first draft
423
+ shipping only the second β€” it read scripts/selfcheck.sh's `_subj` for-loop but missed that
424
+ file's direct `bash scripts/probe_scope_check.sh --self-test` / `bash scripts/
425
+ utterance_landing_check.sh --self-test`, so those two subjects were reported UNDECLARED while
426
+ selfcheck.sh runs them every time. This is the exact failure the header above names by cite β€”
427
+ a reader trusting the count over the source would have been told a false thing with confidence.
428
+
429
+ Shape 1 (direct): `bash scripts/<name>.sh ... --self-test` on one line β€” mirrors
430
+ runner_dispatches' direct branch, structurally simpler than the indirect case below.
431
+ Shape 2 (indirect): bare_name sits in a `for VAR in ... bare_name ...; do` loop whose body
432
+ dispatches $VAR with --self-test β€” mirrors the indirect-branch reasoning of runner_dispatches:
433
+ the literal name is in a list construct, the invocation runs through the loop variable, so a
434
+ direct-dispatch grep alone structurally cannot see it (selfcheck.sh's `for _subj in ...` loop).
435
+
436
+ Comment lines are dropped first, exactly as runner_dispatches does for ordinary suites: "a
437
+ mention is not an invocation" applies identically here, and until 2026-08-15 it was enforced in
438
+ only one of the two predicates. Measured: a `# bash scripts/x.sh --self-test` usage line
439
+ matched the direct regex and certified a subject nothing ran."""
440
+ _body = '\n'.join(_l for _l in txt.split('\n') if not _l.strip().startswith('#'))
441
+ if re.search(rf'\bbash\s+scripts/{re.escape(bare_name)}\.sh\b[^\n]*--self-test', _body):
442
+ return True
443
+ in_loop = False; loop_var = None; has_name = False
444
+ for ln in _body.split('\n'):
445
+ s = ln.strip()
446
+ m = re.match(r'for\s+(\w+)\s+in\b(.*)', s)
447
+ if m:
448
+ loop_var = m.group(1)
449
+ has_name = bool(re.search(rf'\b{re.escape(bare_name)}\b', m.group(2)))
450
+ in_loop = True
451
+ continue
452
+ if in_loop:
453
+ if has_name and '--self-test' in ln and re.search(rf'\${{?{re.escape(loop_var)}\b', ln):
454
+ return True
455
+ if s == 'done':
456
+ in_loop = False; loop_var = None; has_name = False
457
+ return False
458
+
459
+ def has_selftest_runner(bare_name):
460
+ # A subject does not wire itself. has_runner() has skipped a suite's own file since this file
461
+ # was written; this branch did not, and the asymmetry was invisible because nothing exercised
462
+ # it β€” the only in-file control drove selftest_dispatched with synthetic text, never the
463
+ # runner set.
464
+ # ⚠️ Attribution, kept precise because a cross-family round caught it being loose: the 2026-08-15
465
+ # incident β€” directional_diff_gate.sh dispatched by nothing while the report said 10/10 β€” was
466
+ # caused by the COMMENT half, and comment-stripping alone closes it. Every self-reference in
467
+ # this tree at that date was a `#` line; a non-comment one existed nowhere. So this guard is
468
+ # SYMMETRY, not a repair of an observed case: it covers the shape where a subject names its own
469
+ # dispatch in live code, such as a usage helper. That shape is pinned by lane L16, which was
470
+ # added at the same time and for the same reason β€” without it this line is a repair no control
471
+ # drives, which is the failure mode this file exists to name.
472
+ _own = bare_name + '.sh'
473
+ return any(selftest_dispatched(bare_name, _read(r)) for r in runners if os.path.basename(r) != _own)
474
+
475
+ selftest_wired = {s for s in selftest_subjects if has_selftest_runner(s)}
476
+ selftest_undeclared = sorted(s for s in selftest_subjects if s not in selftest_wired)
477
+
478
+ # Minimal known-pair β€” proportionate to the size of this addition, not the full CTL apparatus
479
+ # below, but a dead predicate must still be caught rather than trusted on read-through alone.
480
+ _ST_POS_FIXTURE = 'for _subj in alpha beta; do\n bash "scripts/$_subj.sh" --self-test\ndone\n'
481
+ _ST_NEG_FIXTURE = 'for _subj in alpha beta; do\n bash "scripts/$_subj.sh" --normal-run\ndone\n'
482
+ if not selftest_dispatched('alpha', _ST_POS_FIXTURE):
483
+ print("CONTROL_FAILED\tself-test known-positive fixture read as undispatched β€” detector is blind")
484
+ raise SystemExit(2)
485
+ if selftest_dispatched('alpha', _ST_NEG_FIXTURE):
486
+ print("CONTROL_FAILED\tself-test known-negative fixture with no --self-test flag read as dispatched")
487
+ raise SystemExit(2)
488
+ # Second known-negative, added 2026-08-15 with the comment-skip repair. Without it the repair is a
489
+ # line of code no control drives β€” the failure mode this whole file exists to name.
490
+ _ST_CMT_FIXTURE = '# bash scripts/alpha.sh --self-test # usage example, not a caller\n'
491
+ if selftest_dispatched('alpha', _ST_CMT_FIXTURE):
492
+ print("CONTROL_FAILED\tself-test commented usage line read as a live dispatcher")
493
+ raise SystemExit(2)
494
+
259
495
  # ── CONTROL: the instrument must be able to see a suite known to be wired, and must NOT see one
260
496
  # known to be dead. Without both arms a broken detector reports "all clean" or "all broken" and
261
497
  # either reads as a verdict. [[feedback_absence_measurement_needs_control]]
262
498
  CTL_POS = 'test_selfcheck_state_lanes.sh' # selfcheck.sh invokes this DIRECTLY
263
- CTL_POS2 = 'test_session_close_lanes.sh' # selfcheck.sh:613 for-list + `bash "$_anchor"` β€” the
499
+ CTL_POS2 = 'test_session_close_lanes.sh' # selfcheck.sh's `_anchor` for-list + `bash "$_anchor"` β€” the
264
500
  # INDIRECT arm. Pins the second detection branch: the
265
501
  # strict detector called this UNWIRED and a hand-check
266
502
  # showed it runs. Without this control that branch
@@ -360,6 +596,9 @@ for s in resolved:
360
596
  print(f"RESOLVED\t{s}")
361
597
  for s in gone:
362
598
  print(f"GONE\t{s}")
599
+ print(f"SELFTEST_COUNTS\t{len(selftest_subjects)}\t{len(selftest_wired)}")
600
+ for s in selftest_undeclared:
601
+ print(f"SELFTEST_UNDECLARED\t{s}")
363
602
  PY
364
603
  )
365
604
  rc=$?
@@ -421,5 +660,34 @@ if [ "$N_DEBT" -gt 0 ]; then
421
660
  echo " only go down; a new one fails the check rather than joining the list silently."
422
661
  fi
423
662
 
424
- echo "PASS lane-runner: ${TOTAL} suites β€” ${WIRED} wired Β· ${N_EXEMPT} exempt Β· ${N_DEBT} declared debt"
663
+ SELFTEST_COUNTS=$(printf '%s\n' "$out" | awk -F'\t' '$1=="SELFTEST_COUNTS"{print $2" "$3}')
664
+ set -- $SELFTEST_COUNTS
665
+ ST_TOTAL="${1:-0}"; ST_WIRED="${2:-0}"
666
+ SELFTEST_UNDECLARED=$(printf '%s\n' "$out" | awk -F'\t' '$1=="SELFTEST_UNDECLARED"{print $2}')
667
+ ST_UNDECLARED_N=0
668
+ if [ -n "$SELFTEST_UNDECLARED" ]; then
669
+ ST_UNDECLARED_N=$(printf '%s\n' "$SELFTEST_UNDECLARED" | wc -l | tr -d ' ')
670
+ # Advisory, not blocking β€” these are pre-existing (measured 2026-08-13, before this check saw
671
+ # them at all), not something introduced by whatever change is running this check right now.
672
+ # Same DEBT philosophy as above: loudly counted, never silent, must only go down from here.
673
+ echo "⚠️ lane-runner: ${ST_UNDECLARED_N} embedded --self-test subject(s) with no --self-test"
674
+ echo " dispatcher anywhere (self-test code exists, nothing calls it β€” see this file's own"
675
+ echo " Β§Embedded --self-test comment for why the suites glob above cannot see this class):"
676
+ printf '%s\n' "$SELFTEST_UNDECLARED" | sed 's/^/ /'
677
+ echo " Fix by wiring \`bash scripts/<name>.sh --self-test\` into scripts/selfcheck.sh, by"
678
+ echo " ONE of its two shapes β€” and the choice is decided by the subject's own output, not"
679
+ echo " by which is shorter. The \`for _subj in ...\` loop gates on the substring μΊ˜λ¦¬λΈŒλ ˆμ΄μ…˜;"
680
+ echo " a subject whose terminal verdict is worded any other way goes red there for a reason"
681
+ echo " that has nothing to do with its lanes β€” capability_registry_check was added to that"
682
+ echo " loop and reverted for exactly this. Such a subject takes the second shape: its own"
683
+ echo " direct-dispatch block, as capability_registry_check.sh and relay_channel.sh do there."
684
+ echo " πŸŸ₯ In that second shape do NOT gate on the exit code alone. Measured 2026-08-15 on"
685
+ echo " directional_diff_gate: a suite whose lanes were all deleted still printed a PASS"
686
+ echo " verdict and exited 0, and a usage banner carrying the same words satisfied a"
687
+ echo " substring match. Require the terminal verdict AS A WHOLE LINE, with a non-zero count"
688
+ echo " in it, or exit 0 will certify a suite that ran nothing."
689
+ fi
690
+
691
+ echo "PASS lane-runner: ${TOTAL} suites β€” ${WIRED} wired Β· ${N_EXEMPT} exempt Β· ${N_DEBT} declared debt" \
692
+ "Β· self-test: ${ST_WIRED}/${ST_TOTAL} wired"
425
693
  exit 0
@@ -36,6 +36,8 @@ cd "$REPO_ROOT" || exit 1
36
36
  # a file of that name to exercise the LOW allowlist β€” not a pointer to this file at all.)
37
37
  # scripts/sync_guard_check.sh β€” anchor for that same operator-private mirror sync;
38
38
  # no shipped hook invokes it.
39
+ # scripts/sync_to_be_lanes.sh β€” forward-path lane suite for sync-to-be.sh, itself
40
+ # ACCEPTED_ABSENT above; added 2026-08-14, pmh-dev#69.
39
41
  ACCEPTED_ABSENT=(
40
42
  ".claude/registry/LOCAL_SKILL_REGISTRY.md"
41
43
  # An INSTALL DESTINATION the user creates (`cp templates/local_fh_context.md
@@ -57,6 +59,21 @@ ACCEPTED_ABSENT=(
57
59
  # anchor but guards on the subject's presence, so package mode SKIPs rather than falling through.
58
60
  "scripts/sync-from-be.sh"
59
61
  "scripts/sync_from_be_lanes.sh"
62
+ # Forward path's own lane suite (added 2026-08-14, pmh-dev#69). Same reason as its return-path
63
+ # sibling directly above: it exercises scripts/sync-to-be.sh, itself ACCEPTED_ABSENT β€” a lane
64
+ # suite for a script that never ships has nothing to verify on a consumer's machine either.
65
+ "scripts/sync_to_be_lanes.sh"
66
+ # Shared identity resolver for the two transports directly above (added to files[] by #368,
67
+ # REMOVED from it 2026-08-15). It was the odd one out: its own callers β€” sync-to-be.sh and
68
+ # sync-from-be.sh β€” are ACCEPTED_ABSENT here for a reason that applies to it verbatim (the
69
+ # namespacing it computes only means anything on a machine that HAS the operator's companion
70
+ # store), yet it alone was listed for shipping. The pre-publish confidentiality scan is what
71
+ # surfaced the misclassification: it carries the companion store's own directory vocabulary,
72
+ # which would have reached the registry for the first time in 1.4.98. The one shipped caller,
73
+ # fh_session_load.sh, sources it behind `[ -f ]` and documents its own degrade ("an unresolved
74
+ # identity degrades to the historical unsuffixed tracks-meta rather than erroring β€” that is the
75
+ # pre-fix behavior, not a new failure mode"), so a consumer loses nothing that consumer ever had.
76
+ "scripts/fh_hub_identity.sh"
60
77
  # ── The three lane suites selfcheck.sh's DEBT-12 pair-loop names but does not ship ────────────
61
78
  # Added 2026-08-13, and the way they got here is the point: this check CAUGHT them. Before that
62
79
  # loop existed, these names lived in lane_runner_check.sh's DEBT array as bare basenames
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // postinstall_notice.js β€” one-line, stderr-only, CI-silent install notice.
4
+ //
5
+ // WHY: 16k+ npm downloads (measured live, 2026-08-15) vs 7 GitHub stars on the same day β€”
6
+ // people are using fh-gate and have no reason to ever see the GitHub repo, since `npx
7
+ // @chrono-meta/fh-gate` never surfaces it. This closes that gap at the one moment every
8
+ // installer passes through, without touching fh-gate.js's own stdout (that IS the CI
9
+ // contract per its own header comment β€” this notice must never risk polluting it).
10
+ //
11
+ // WHY stderr, not stdout: npm's own install-time output already goes to stderr by
12
+ // convention (npm's progress/warnings), and more importantly this keeps it structurally
13
+ // impossible for the notice to ever be mistaken for gate output β€” a script piping
14
+ // `npx @chrono-meta/fh-gate`'s stdout for FH_GATE_VERDICT: parsing never sees this line,
15
+ // even if this file were somehow invoked in the same process (it isn't β€” separate
16
+ // lifecycle script, separate process).
17
+ //
18
+ // WHY CI-silent: postinstall banners are a known source of npm-ecosystem noise complaints,
19
+ // and this package explicitly markets itself for CI use (CHEATSHEET.md Β§9.5). A line that
20
+ // reprints on every CI run install is exactly the annoyance that gets a package reported,
21
+ // not starred. Skip whenever a CI environment is plausible OR the operator opts out.
22
+ if (process.env.CI || process.env.FH_NO_BANNER || process.env.CONTINUOUS_INTEGRATION) {
23
+ process.exit(0);
24
+ }
25
+ try {
26
+ process.stderr.write(
27
+ '\n⭐ forge-harness (fh-gate) β€” if this is useful, a star helps others find it:\n' +
28
+ ' https://github.com/chrono-meta/forge-harness\n' +
29
+ ' (set FH_NO_BANNER=1 to silence this message)\n\n'
30
+ );
31
+ } catch (_) {
32
+ // Never fail an install over a courtesy message.
33
+ }
34
+ process.exit(0);