@chrono-meta/fh-gate 1.4.95 → 1.4.96

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 (63) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +18 -0
  3. package/CHEATSHEET.md +1 -1
  4. package/knowledge/shared/harness-core/fh_detail_protocols.md +12 -0
  5. package/knowledge/shared/harness-core/ship_readiness_gate.md +7 -4
  6. package/knowledge/shared/learnings/subagent_invocations_log.yaml +43 -1
  7. package/package.json +6 -1
  8. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  9. package/plugins/fh-commons/agents/quench-challenger.md +49 -23
  10. package/plugins/fh-commons/skills/convergence-loop/SKILL.md +14 -0
  11. package/plugins/fh-commons/skills/deliberation/SKILL.md +14 -0
  12. package/plugins/fh-commons/skills/mcp-circuit-breaker/SKILL.md +10 -1
  13. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  14. package/plugins/fh-meta/CHANGELOG.md +36 -0
  15. package/plugins/fh-meta/agents/beginner.md +4 -1
  16. package/plugins/fh-meta/agents/challenger.md +7 -1
  17. package/plugins/fh-meta/agents/expert.md +1 -1
  18. package/plugins/fh-meta/agents/fact-checker.md +7 -1
  19. package/plugins/fh-meta/agents/hub-persona-auditor.md +2 -1
  20. package/plugins/fh-meta/agents/main-player.md +4 -1
  21. package/plugins/fh-meta/agents/persona-innovator.md +10 -2
  22. package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
  23. package/plugins/fh-meta/skills/apex-review/SKILL.md +5 -0
  24. package/plugins/fh-meta/skills/asset-placement-gate/SKILL.md +38 -8
  25. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +16 -2
  26. package/plugins/fh-meta/skills/context-doctor/SKILL_detail.md +45 -10
  27. package/plugins/fh-meta/skills/corpus-grounding-expander/SKILL.md +14 -5
  28. package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +93 -30
  29. package/plugins/fh-meta/skills/deep-clarify/SKILL.md +28 -9
  30. package/plugins/fh-meta/skills/fh/SKILL.md +4 -0
  31. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +64 -8
  32. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +20 -7
  33. package/plugins/fh-meta/skills/goal-quench/SKILL.md +48 -15
  34. package/plugins/fh-meta/skills/goal-quench/SKILL_detail.md +58 -11
  35. package/plugins/fh-meta/skills/harness-doctor/SKILL_detail.md +109 -33
  36. package/plugins/fh-meta/skills/harvest-loop/SKILL.md +6 -1
  37. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +126 -17
  38. package/plugins/fh-meta/skills/install-doctor/SKILL.md +50 -14
  39. package/plugins/fh-meta/skills/install-wizard/SKILL.md +26 -7
  40. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +68 -21
  41. package/plugins/fh-meta/skills/memory-hygiene/SKILL.md +64 -17
  42. package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +38 -4
  43. package/plugins/fh-meta/skills/persona-roster-expander/SKILL.md +15 -7
  44. package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +39 -11
  45. package/plugins/fh-meta/skills/plugin-recommender/SKILL_detail.md +24 -7
  46. package/plugins/fh-meta/skills/prompt-regression/SKILL.md +54 -11
  47. package/plugins/fh-meta/skills/salience-splitter/SKILL.md +120 -7
  48. package/plugins/fh-meta/skills/salience-splitter/SKILL_detail.md +46 -13
  49. package/plugins/fh-meta/skills/sim-conductor/SKILL_detail.md +28 -3
  50. package/plugins/fh-meta/skills/steel-quench/SKILL.md +3 -1
  51. package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +72 -14
  52. package/scripts/count_check.sh +47 -1
  53. package/scripts/degrade_direction_scan.sh +276 -6
  54. package/scripts/degrade_probe_capability.sh +105 -0
  55. package/scripts/package_coverage_check.sh +8 -0
  56. package/scripts/psa_probe_capability.sh +78 -0
  57. package/scripts/public_surface_scan_files.sh +8 -0
  58. package/scripts/selfcheck.sh +15 -0
  59. package/scripts/test_capability_entrypoint_shipping.sh +132 -0
  60. package/scripts/test_count_check_readme_format_lanes.sh +75 -0
  61. package/scripts/test_degrade_scan_shell_probes.sh +415 -0
  62. package/scripts/validate_yaml.sh +146 -0
  63. package/templates/degrade_direction_scan.sh +276 -6
@@ -198,6 +198,118 @@ n=$(p_hits "$TMP/kn.py")
198
198
  [ "$n" -eq 0 ] && ok "python known-negative: still silent" \
199
199
  || bad "python known-negative: $n hit(s) — Python probes became noisy"
200
200
 
201
+ # ── Markdown-fence lanes (added 2026-08-12) ───────────────────────────────────────────────────
202
+ # WHY THESE EXIST: the fence-extraction feature shipped with ZERO anchors, and a revert probe run
203
+ # by the pre-publish security pass proved it — neutralizing `_md_shadow()` entirely left this suite
204
+ # at "14 passed, 0 failed". A feature you can delete without reddening a lane is not covered, and
205
+ # that gap is exactly why the python3-absence hole below reached a release candidate.
206
+ MDT="$TMP/mdlanes"; mkdir -p "$MDT"
207
+ printf '# t\n\n```bash\nverify() { run || return 0; }\n```\n' > "$MDT/pos.md"
208
+ printf '# t\n\nprose only, no fence\n' > "$MDT/neg.md"
209
+
210
+ # L-MD1 known-positive: a defect inside a ```bash fence is FOUND (the whole point of the feature)
211
+ out=$(bash "$SCAN" "$MDT/pos.md" 2>&1); rc=$?
212
+ if printf '%s' "$out" | grep -q 'bash fence'; then
213
+ ok "MD1 a defect inside a bash fence is detected and reported at the ORIGIN path"
214
+ else
215
+ bad "MD1 fence extraction found nothing in a known-positive — feature is inert (rc=$rc)"
216
+ fi
217
+
218
+ # L-MD2 known-negative: a markdown file with NO fence must stay UNSCANNABLE, never 'clean'
219
+ out=$(bash "$SCAN" "$MDT/neg.md" 2>&1); rc=$?
220
+ if printf '%s' "$out" | grep -qi 'unscannable' && [ "$rc" -ne 0 ]; then
221
+ ok "MD2 fence-less markdown reports UNSCANNABLE (not measured != clean)"
222
+ else
223
+ bad "MD2 fence-less markdown did not report UNSCANNABLE (rc=$rc) — absence rendered as pass"
224
+ fi
225
+
226
+ # L-MD3 THE REGRESSION THIS SUITE WAS MISSING: with python3 unreachable, extraction is impossible.
227
+ # The file must land in UNSCANNABLE and the run must NOT exit 0. Before the fix it fell into
228
+ # neither set and vanished from the summary, so the scan reported "no smells ... exit 0".
229
+ MDBIN="$TMP/mdbin"; mkdir -p "$MDBIN"
230
+ for c in bash grep sed awk find cksum cut tr mktemp rm cat sort head wc; do
231
+ src=$(command -v "$c" 2>/dev/null) && ln -sf "$src" "$MDBIN/$c"
232
+ done
233
+ # The stub must be CONTROLLED — a stub missing `find` makes the scan report "no scannable target
234
+ # files" and this lane would then pass for a reason unrelated to python3.
235
+ _mdbin_ok=$(env PATH="$MDBIN" bash -c 'command -v find >/dev/null 2>&1 && echo 1 || echo 0')
236
+ out=$(env PATH="$MDBIN" bash "$SCAN" "$MDT/pos.md" 2>&1); rc=$?
237
+ if [ "$_mdbin_ok" != "1" ]; then
238
+ bad "MD3 stub PATH lacks find — lane NOT RUN (not a pass)"
239
+ elif [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
240
+ # NOTE the wording: this asserts UNMEASURED, not UNSCANNABLE. The two were one bucket in the
241
+ # first fix and a re-verification round showed why that mattered — "no fence here" (a normal
242
+ # state) and "extraction impossible" (a blind spot) must not share an exit path.
243
+ ok "MD3 python3 unreachable -> markdown reports COULD-NOT-MEASURE and exit != 0 (never a silent clean)"
244
+ else
245
+ bad "MD3 python3 unreachable produced rc=$rc without a could-not-measure signal — the not-found==0 hole is open"
246
+ fi
247
+
248
+ # L-MD4 shadow-name collision: `a/b.md` and `a_b.md` must not map to the same shadow file, or one
249
+ # of them is silently overwritten while the scanned COUNT still says 2 (coverage counted, detection
250
+ # impossible). Measured before the fix: the defective file disappeared and the run exited 0.
251
+ mkdir -p "$MDT/col/a"
252
+ printf '# c\n\n```bash\nverify() { run || return 0; }\n```\n' > "$MDT/col/a/b.md"
253
+ printf '# c\n\n```bash\nverify() { run || return 1; }\n```\n' > "$MDT/col/a_b.md"
254
+ out=$(bash "$SCAN" "$MDT/col/a/b.md" "$MDT/col/a_b.md" 2>&1); rc=$?
255
+ if printf '%s' "$out" | grep -q 'a/b.md'; then
256
+ ok "MD4 colliding basenames keep separate shadows (the defective file is still reported)"
257
+ else
258
+ bad "MD4 a/b.md vanished under collision with a_b.md — shadow name is not unique"
259
+ fi
260
+
261
+ # L-MD5/MD6 — THE DIRECTORY ARM. MD3 only exercises the single-file path, and a re-verification
262
+ # round proved that mattered: the first fix promoted "could not measure" to exit 2 ONLY when FILES
263
+ # was empty, so a directory containing one scannable .sh demoted the failure to a note line and the
264
+ # run exited 0 = CLEAN. That is the dominant path (typed capability scans directories), i.e. the
265
+ # defect this release claims to fix was still live where it actually runs.
266
+ # The stub PATH is CONTROLLED first: `command -v` under zsh can return a bare name, which produces
267
+ # a self-referential symlink and a stub with no `find` — then the scan reports "no scannable target
268
+ # files" and the lane would pass for a reason that has nothing to do with python3.
269
+ MDD="$TMP/mddir"; mkdir -p "$MDD/repo" "$MDD/bin"
270
+ printf '# t\n\n```bash\nverify() { run || return 0; }\n```\n' > "$MDD/repo/SKILL.md"
271
+ printf '#!/usr/bin/env bash\necho ok\n' > "$MDD/repo/helper.sh"
272
+ for c in bash sh grep sed awk find cut tr mktemp rm cat sort head wc printf cksum dirname basename; do
273
+ p=$(command -v "$c" 2>/dev/null); case "$p" in /*) ln -sf "$p" "$MDD/bin/$c" ;; esac
274
+ done
275
+ _stub_md=$(env PATH="$MDD/bin" find "$MDD/repo" -type f -name '*.md' 2>/dev/null | grep -c .)
276
+ if [ "$_stub_md" != "1" ]; then
277
+ bad "MD5/MD6 stub PATH is unusable (find missing) — lanes NOT RUN, which is not a pass"
278
+ else
279
+ out=$(env PATH="$MDD/bin" bash "$SCAN" "$MDD/repo" 2>&1); rc=$?
280
+ if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
281
+ ok "MD5 directory scan + python3 unreachable -> non-clean exit (not demoted to a note)"
282
+ else
283
+ bad "MD5 directory scan with python3 unreachable exited $rc — 'could not measure' rendered as clean"
284
+ fi
285
+ # cksum is the OTHER undeclared dependency: without it the uniqueness token is empty and the
286
+ # shadow-name collision (MD4) silently returns, so it must gate the same way python3 does.
287
+ cp -R "$MDD/bin" "$MDD/bin2"
288
+ p=$(command -v python3 2>/dev/null); case "$p" in /*) ln -sf "$p" "$MDD/bin2/python3" ;; esac
289
+ rm -f "$MDD/bin2/cksum"
290
+ out=$(env PATH="$MDD/bin2" bash "$SCAN" "$MDD/repo" 2>&1); rc=$?
291
+ if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
292
+ ok "MD6 cksum unreachable -> non-clean exit (collision guard cannot silently degrade)"
293
+ else
294
+ bad "MD6 cksum unreachable exited $rc — the A-1 collision returns without any signal"
295
+ fi
296
+ # MD8 — `cut` is the THIRD undeclared dependency of the same pipeline (`cksum | cut`). Guarding
297
+ # cksum alone left it half-guarded: without `cut` the uniqueness token is empty and the MD4
298
+ # collision returns. Cross-family finding (gpt-5.5, 2026-08-12); the guard shipped without an
299
+ # anchor, so deleting it left every lane green — this is that missing half.
300
+ cp -R "$MDD/bin" "$MDD/bin3"
301
+ for c in python3 cksum; do
302
+ p=$(command -v "$c" 2>/dev/null); case "$p" in /*) ln -sf "$p" "$MDD/bin3/$c" ;; esac
303
+ done
304
+ rm -f "$MDD/bin3/cut"
305
+ out=$(env PATH="$MDD/bin3" bash "$SCAN" "$MDD/repo" 2>&1); rc=$?
306
+ if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
307
+ ok "MD8 cut unreachable -> non-clean exit (the cksum|cut pipeline is guarded as a whole)"
308
+ else
309
+ bad "MD8 cut unreachable exited $rc — the uniqueness token silently empties and MD4's collision returns"
310
+ fi
311
+ fi
312
+
201
313
  # The field-propagated copy must not drift from the canonical one. Two copies of the same
202
314
  # normalizer diverge, and the lenient half silently drops what the strict half catches — measured
203
315
  # 2026-07-28: `templates/` was 2 lines behind BEFORE this session's fix and then a full 8 KB behind
@@ -215,6 +327,309 @@ else
215
327
  bad "templates/degrade_direction_scan.sh has DRIFTED from scripts/ — the field copy is what qasp/pmh run; sync it (cp scripts/degrade_direction_scan.sh templates/)"
216
328
  fi
217
329
 
330
+ # MD7 — "extraction FAILED" must not be spelled the same way as "this file has no fence".
331
+ # Cross-family finding (gpt-5.5, 2026-08-12) against the first language-split draft: an unreadable
332
+ # SKILL.md fell into UNSCANNABLE, and one clean .sh in the same directory then carried the run to
333
+ # exit 0 = clean. The scanner would have reported a corpus it could not open as verified.
334
+ MDE="$TMP/mderr"; mkdir -p "$MDE"
335
+ printf '# t\n\n```bash\nverify() { run || return 0; }\n```\n' > "$MDE/SKILL.md"
336
+ printf '#!/usr/bin/env bash\necho ok\n' > "$MDE/clean.sh"
337
+ chmod 000 "$MDE/SKILL.md" 2>/dev/null
338
+ # CONTROL: root (and some filesystems) ignore mode 000, and then this lane would pass or fail for a
339
+ # reason unrelated to the code. Prove unreadability before asserting on it.
340
+ if head -c1 "$MDE/SKILL.md" >/dev/null 2>&1; then
341
+ printf ' – MD7 lane NOT RUN (file still readable at mode 000 — running as root?) — this is not a pass\n'
342
+ else
343
+ out=$(bash "$SCAN" "$MDE" 2>&1); rc=$?
344
+ if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
345
+ ok "MD7 unreadable markdown reports COULD-NOT-MEASURE even with a clean .sh beside it"
346
+ else
347
+ bad "MD7 unreadable markdown exited $rc without a could-not-measure signal — extraction failure is being read as 'no fence'"
348
+ fi
349
+ fi
350
+ chmod 644 "$MDE/SKILL.md" 2>/dev/null
351
+
352
+ # ── S6 word-split lanes (added 2026-08-12) ────────────────────────────────────────────────────
353
+ # The probe claims a SHELL-BEHAVIOUR difference, so the fixtures are not enough on their own: a
354
+ # regex can look right while the premise is wrong. S6c measures the premise itself in both shells.
355
+ # The fixtures carry a ZSH shebang on purpose. S6 is scoped to surfaces whose execution shell is not
356
+ # pinned to a splitting shell (markdown fences, zsh scripts) — see S6d for the measurement that
357
+ # forced that scoping and for the bash-shebang arm that must stay silent.
358
+ cat > "$TMP/s6_positive.sh" <<'EOF'
359
+ #!/usr/bin/env zsh
360
+ for f in $recent_sessions; do echo "$f"; done
361
+ git add -- $FILES
362
+ EOF
363
+ cat > "$TMP/s6_negative.sh" <<'EOF'
364
+ #!/usr/bin/env zsh
365
+ # Command substitution DOES split in zsh — flagging it would be noise, not a finding.
366
+ for f in $(find . -name '*.md'); do echo "$f"; done
367
+ # Arrays split by element in both shells.
368
+ for f in "${FILES[@]}"; do echo "$f"; done
369
+ git add -- "${FILES[@]}"
370
+ # The portable form this probe prescribes.
371
+ printf '%s\n' "$recent_sessions" | while IFS= read -r f; do echo "$f"; done
372
+ EOF
373
+ n=$(bash "$SCAN" "$TMP/s6_positive.sh" 2>&1 | grep -cE '\[S6:')
374
+ [ "$n" -eq 2 ] && ok "S6 known-positive: 2/2 (\`for x in \$VAR\` and \`-- \$LIST\`)" \
375
+ || bad "S6 known-positive: expected 2 hits, got $n — the zsh word-split class is invisible"
376
+
377
+ n=$(bash "$SCAN" "$TMP/s6_negative.sh" 2>&1 | grep -cE '\[S6:')
378
+ [ "$n" -eq 0 ] && ok "S6 known-negative: \$(cmd), \"\${arr[@]}\" and printf|while stay silent" \
379
+ || bad "S6 known-negative: $n hit(s) — S6 fires on forms that behave identically in both shells"
380
+
381
+ # S6c — the PREMISE, measured rather than asserted. If zsh ever word-split a parameter expansion,
382
+ # this probe would be a style rule wearing a defect's clothes, and every finding it produced would
383
+ # be a false positive. Pinning the premise means a future zsh/bash change reddens a lane here
384
+ # instead of silently invalidating the probe.
385
+ if command -v zsh >/dev/null 2>&1; then
386
+ _b=$(bash -c 'v="a b"; n=0; for x in $v; do n=$((n+1)); done; echo $n')
387
+ _z=$(zsh -c 'v="a b"; n=0; for x in $v; do n=$((n+1)); done; echo $n')
388
+ _zc=$(zsh -c 'n=0; for x in $(echo a b); do n=$((n+1)); done; echo $n')
389
+ if [ "$_b" = "2" ] && [ "$_z" = "1" ] && [ "$_zc" = "2" ]; then
390
+ ok "S6c premise holds: bash splits \$VAR (2), zsh does not (1), zsh splits \$(cmd) (2)"
391
+ else
392
+ bad "S6c premise BROKEN: bash=\$VAR:$_b zsh=\$VAR:$_z zsh=\$(cmd):$_zc — S6's scope is no longer justified"
393
+ fi
394
+ else
395
+ printf ' – S6c premise lane NOT RUN (zsh unavailable) — this is not a pass\n'
396
+ fi
397
+
398
+ # S6d — APPLICABILITY. Measured 2026-08-12 across 28 hits: 7/7 real inside markdown fences,
399
+ # 21/21 false in `.sh` files with a bash shebang (bash runs those, so splitting is intended there).
400
+ # A 75%-noise probe trains dismissal — this file already paid that at 9/9 FP on S5. Both arms are
401
+ # pinned, because scoping a probe is one edit away from blinding it.
402
+ cat > "$TMP/s6_pinned_bash.sh" <<'EOF'
403
+ #!/usr/bin/env bash
404
+ for f in $recent_sessions; do echo "$f"; done
405
+ git add -- $FILES
406
+ EOF
407
+ n=$(bash "$SCAN" "$TMP/s6_pinned_bash.sh" 2>&1 | grep -cE '\[S6:')
408
+ [ "$n" -eq 0 ] && ok "S6d bash-shebang file: silent (execution shell is pinned — splitting is intended)" \
409
+ || bad "S6d bash-shebang file fired $n time(s) — S6 is back to 21/28 noise"
410
+
411
+ # S6d-2 — a `.sh` with NO shebang is not pinned either, so the probe MUST apply. The first version
412
+ # of the scoping asked "is this a markdown shadow or a zsh script?" and therefore went silent here;
413
+ # the question that decides it is "does a shebang pin a splitting shell?" (cross-family, round 4).
414
+ cat > "$TMP/s6_no_shebang.sh" <<'EOF'
415
+ for f in $recent_sessions; do echo "$f"; done
416
+ git add -- $FILES
417
+ EOF
418
+ n=$(bash "$SCAN" "$TMP/s6_no_shebang.sh" 2>&1 | grep -cE '\[S6:')
419
+ [ "$n" -eq 2 ] && ok "S6d-2 shebang-less .sh: fires (unknown shell is not a pinned shell)" \
420
+ || bad "S6d-2 shebang-less .sh produced $n S6 hit(s), expected 2 — the scoping excludes an unpinned surface"
421
+
422
+ # S6d-3 — /bin/sh pins a splitting shell too, so it must stay silent (the exclusion is about the
423
+ # SHELL, not about the presence of a shebang line).
424
+ cat > "$TMP/s6_posix.sh" <<'EOF'
425
+ #!/bin/sh
426
+ for f in $recent_sessions; do echo "$f"; done
427
+ EOF
428
+ n=$(bash "$SCAN" "$TMP/s6_posix.sh" 2>&1 | grep -cE '\[S6:')
429
+ [ "$n" -eq 0 ] && ok "S6d-3 /bin/sh shebang: silent (POSIX sh splits — intended semantics)" \
430
+ || bad "S6d-3 /bin/sh file fired $n time(s) — the pinned-shell test does not recognize sh"
431
+
432
+ # SYM — a target that is a SYMLINK to a directory must still be traversed. `[ -d ]` follows the
433
+ # link but `find <link> -type f` does not, so the scan enumerated nothing and exited 0 = clean on a
434
+ # directory full of known-positives (cross-family repro, round 4). `-H` follows the argument only.
435
+ SYMREAL="$TMP/symreal"; mkdir -p "$SYMREAL"
436
+ printf '#!/usr/bin/env bash\nscan=$(run) || exit 0\n' > "$SYMREAL/bad.sh"
437
+ ln -sfn "$SYMREAL" "$TMP/symlink"
438
+ n=$(s_hits "$TMP/symlink")
439
+ [ "$n" -ge 1 ] && ok "SYM symlinked directory target is traversed (known-positive inside is found)" \
440
+ || bad "SYM symlinked directory target produced $n hits — an entire target renders as 'no scannable files, exit 0'"
441
+
442
+ # ...and the same content in a markdown fence, where no shebang pins the shell, MUST still fire.
443
+ printf '# t\n\n```bash\nfor f in $recent_sessions; do echo "$f"; done\n```\n' > "$MDT/s6.md"
444
+ n=$(bash "$SCAN" "$MDT/s6.md" 2>&1 | grep -cE '\[S6:')
445
+ [ "$n" -eq 1 ] && ok "S6d markdown fence: still fires (the surface a human pastes into zsh)" \
446
+ || bad "S6d markdown fence produced $n S6 hit(s), expected 1 — the scoping blinded the real class"
447
+
448
+ # S6e — a trailing COMMENT must not mask a real hit. Cross-family repro (gpt-5.5, 2026-08-12):
449
+ # `grep -vE '\[[@*]\]'` ran against the raw line, so a real defect whose comment mentioned the safe
450
+ # array form was suppressed entirely. The mirror arm is pinned too: a line matching ONLY inside its
451
+ # comment must stay silent.
452
+ cat > "$TMP/s6_comment.sh" <<'EOF'
453
+ #!/usr/bin/env zsh
454
+ git add -- $FILES # use "${arr[@]}" later
455
+ for x in $VAR; do :; done # "${arr[@]}" would be safe
456
+ echo hi # for y in $OTHER <- match lives only in this comment
457
+ EOF
458
+ n=$(bash "$SCAN" "$TMP/s6_comment.sh" 2>&1 | grep -cE '\[S6:')
459
+ # 3, not 2: the comment-only line IS reported. That is the accepted trade after both comment-aware
460
+ # filters were removed — the array exclusion was dead code that suppressed real hits, and the sed
461
+ # that replaced it could not tell a comment from a `#` inside a string. See the scanner's comment.
462
+ [ "$n" -eq 3 ] && ok "S6e trailing comments never mask a real hit (3/3, comment-only match reported by design)" \
463
+ || bad "S6e expected 3 S6 hits, got $n — 2 means a comment-aware filter is masking a real defect again"
464
+
465
+ # S6f — a `#` inside a QUOTED STRING must not swallow the real hit that follows it. This is the
466
+ # defect the comment-stripping sed introduced and its removal closes (cross-family repro, round 4).
467
+ cat > "$TMP/s6_quoted_hash.sh" <<'EOF'
468
+ #!/usr/bin/env zsh
469
+ printf "#"; git add -- $FILES
470
+ printf "#"; for f in $recent_sessions; do :; done
471
+ EOF
472
+ n=$(bash "$SCAN" "$TMP/s6_quoted_hash.sh" 2>&1 | grep -cE '\[S6:')
473
+ [ "$n" -eq 2 ] && ok "S6f a quoted '#' before the defect does not hide it (2/2)" \
474
+ || bad "S6f expected 2 S6 hits, got $n — comment stripping is back and it cannot see quotes"
475
+
476
+ # S6g — expansion OPERATORS. `for f in ${FILES:-}` is the same defect one operator away and was a
477
+ # false negative until round 5. The negative arm matters just as much: widening the pattern is what
478
+ # makes `${arr[@]}` reachable, and an array expansion splits by element in zsh too.
479
+ cat > "$TMP/s6_ops.sh" <<'EOF'
480
+ #!/usr/bin/env zsh
481
+ for f in ${FILES:-}; do :; done
482
+ for f in ${FILES:=x}; do :; done
483
+ for f in ${FILES#p}; do :; done
484
+ git add -- ${FILES:-}
485
+ EOF
486
+ n=$(bash "$SCAN" "$TMP/s6_ops.sh" 2>&1 | grep -cE '\[S6:')
487
+ [ "$n" -eq 4 ] && ok "S6g braced expansions with operators are detected (4/4)" \
488
+ || bad "S6g expected 4 S6 hits, got $n — \${VAR:-} class is invisible again"
489
+
490
+ cat > "$TMP/s6_ops_neg.sh" <<'EOF'
491
+ #!/usr/bin/env zsh
492
+ for f in ${FILES[@]}; do :; done
493
+ for f in "${FILES[@]}"; do :; done
494
+ git add -- ${FILES[*]}
495
+ EOF
496
+ n=$(bash "$SCAN" "$TMP/s6_ops_neg.sh" 2>&1 | grep -cE '\[S6:')
497
+ [ "$n" -eq 0 ] && ok "S6g-neg array expansions stay silent even under the widened pattern" \
498
+ || bad "S6g-neg fired $n time(s) on array expansions — the widening reintroduced the class it must exclude"
499
+
500
+ # S6i — the `/` (substitution) and `@` operators. `${FILES//old/new}` splits in bash and not in zsh
501
+ # exactly like a plain `$FILES`, and it was invisible until round 6 because `/` was missing from the
502
+ # operator class. Adding operators one incident at a time is why this lane enumerates them.
503
+ cat > "$TMP/s6_subst.sh" <<'EOF'
504
+ #!/usr/bin/env zsh
505
+ for f in ${FILES//old/new}; do :; done
506
+ for f in ${FILES/#p/q}; do :; done
507
+ git add -- ${FILES//,/ }
508
+ EOF
509
+ n=$(bash "$SCAN" "$TMP/s6_subst.sh" 2>&1 | grep -cE '\[S6:')
510
+ [ "$n" -eq 3 ] && ok "S6i substitution operators (\${V//a/b}) are detected (3/3)" \
511
+ || bad "S6i expected 3 S6 hits, got $n — the substitution class is invisible again"
512
+
513
+ # HYPH — a target FILE whose name starts with `-`. Normalizing only the directory branch left the
514
+ # file branch feeding `-bad.sh` straight into `grep`, which parsed it as an option cluster and
515
+ # reported "no smells in 1 scanned file" (round 6). Both branches now normalize at the loop head.
516
+ HYPHD="$TMP/hyph"; mkdir -p "$HYPHD"
517
+ printf '#!/usr/bin/env bash\nscan=$(run) || exit 0\n' > "$HYPHD/-bad.sh"
518
+ ( cd "$HYPHD" && bash "$SCAN" "-bad.sh" >/dev/null 2>&1 ); rc=$?
519
+ [ "$rc" -eq 2 ] && ok "HYPH a file target named -bad.sh is scanned, not swallowed as options (rc=2)" \
520
+ || bad "HYPH file target starting with '-' returned rc=$rc — a known-positive renders as clean"
521
+ n=$(s_hits "$HYPHD")
522
+ [ "$n" -ge 1 ] && ok "HYPH-dir the same file is found through a directory walk" \
523
+ || bad "HYPH-dir directory walk missed the hyphen-named file ($n hits)"
524
+
525
+ # S6h — `#!/usr/bin/env ksh93`: a digit is a word character, so `\bksh\b` could not match it and the
526
+ # file was treated as unpinned. ksh93 splits `$VAR`, so it is pinned and must stay silent.
527
+ cat > "$TMP/s6_ksh93.sh" <<'EOF'
528
+ #!/usr/bin/env ksh93
529
+ for f in $recent_sessions; do :; done
530
+ EOF
531
+ n=$(bash "$SCAN" "$TMP/s6_ksh93.sh" 2>&1 | grep -cE '\[S6:')
532
+ [ "$n" -eq 0 ] && ok "S6h ksh93 shebang recognized as a pinned splitting shell" \
533
+ || bad "S6h ksh93 fired $n time(s) — versioned shell names escape the pinned-shell test"
534
+
535
+ # DISC — a directory that cannot be traversed must not render as "no scannable target files, exit 0".
536
+ # The `find` preflight proves the BINARY exists; it says nothing about THIS traversal. Cross-family
537
+ # repro used a mode-000 directory holding a known-positive.
538
+ DISCD="$TMP/discblind"; mkdir -p "$DISCD"
539
+ printf '#!/usr/bin/env bash\nscan=$(run) || exit 0\n' > "$DISCD/bad.sh"
540
+ chmod 000 "$DISCD" 2>/dev/null
541
+ if find "$DISCD" -type f >/dev/null 2>&1; then
542
+ printf ' – DISC lane NOT RUN (directory still traversable at mode 000 — running as root?) — not a pass\n'
543
+ else
544
+ out=$(bash "$SCAN" "$DISCD" 2>&1); rc=$?
545
+ if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -qi 'COULD NOT BE MEASURED'; then
546
+ ok "DISC unreadable directory reports COULD-NOT-MEASURE and exits non-zero"
547
+ else
548
+ bad "DISC unreadable directory exited $rc without a could-not-measure signal — an unenumerated surface renders clean"
549
+ fi
550
+ fi
551
+ chmod 755 "$DISCD" 2>/dev/null
552
+
553
+ # CAP — the typed capability entry point must reach the scanner for a markdown-only target, and must
554
+ # not collapse "could not measure" into "found findings". Both were live defects on 2026-08-12.
555
+ CAPSH="$REPO_ROOT/scripts/degrade_probe_capability.sh"
556
+ if [ ! -f "$CAPSH" ]; then
557
+ printf ' – CAP lanes NOT RUN (degrade_probe_capability.sh absent) — not a pass\n'
558
+ else
559
+ CAPD="$TMP/capmd"; mkdir -p "$CAPD"
560
+ printf '# t\n\n```bash\nverify() { run || return 0; }\n```\n' > "$CAPD/SKILL.md"
561
+ bash "$CAPSH" --target "$CAPD" >/dev/null 2>&1; rc=$?
562
+ [ "$rc" -eq 2 ] && ok "CAP1 markdown-only target reaches the scanner through the typed path (rc=2)" \
563
+ || bad "CAP1 markdown-only target returned rc=$rc — the headline feature is unreachable on the typed path (3 = the pre-fix NO_TARGET)"
564
+
565
+ CAPD2="$TMP/capprose"; mkdir -p "$CAPD2"; printf '# prose only\n' > "$CAPD2/doc.md"
566
+ bash "$CAPSH" --target "$CAPD2" >/dev/null 2>&1; rc=$?
567
+ [ "$rc" -eq 3 ] && ok "CAP2 fence-less markdown-only target is NO_TARGET, not FINDINGS (rc=3)" \
568
+ || bad "CAP2 fence-less markdown returned rc=$rc, expected 3"
569
+
570
+ # CAP3 — MIXED: one measurable finding + one unmeasurable file. "Could not measure" must win.
571
+ CAPD3="$TMP/capmixed"; mkdir -p "$CAPD3"
572
+ printf '# t\n\n```bash\nverify() { run || return 0; }\n```\n' > "$CAPD3/good.md"
573
+ printf '# t\n\n```bash\nx=1\n```\n' > "$CAPD3/blind.md"
574
+ chmod 000 "$CAPD3/blind.md" 2>/dev/null
575
+ if head -c1 "$CAPD3/blind.md" >/dev/null 2>&1; then
576
+ printf ' – CAP3 lane NOT RUN (file readable at mode 000 — running as root?) — not a pass\n'
577
+ else
578
+ bash "$CAPSH" --target "$CAPD3" >/dev/null 2>&1; rc=$?
579
+ [ "$rc" -eq 10 ] && ok "CAP3 findings + unmeasurable → HARNESS_ERROR (could-not-measure outranks findings)" \
580
+ || bad "CAP3 returned rc=$rc, expected 10 — a partly-unmeasured scan is being reported as a plain FINDINGS verdict"
581
+ fi
582
+ chmod 644 "$CAPD3/blind.md" 2>/dev/null
583
+
584
+ # CAP4 — instrument absence must not be spelled as target absence. The wrapper's own pre-count
585
+ # runs `find`; without it the count is 0 and the run reported NO_TARGET, making the HARNESS_ERROR
586
+ # verdict unreachable for that failure while the scanner itself reports it correctly
587
+ # (cross-family, round 4, A). The stub PATH is CONTROLLED first, as everywhere else in this file.
588
+ CAPBIN="$TMP/capbin"; mkdir -p "$CAPBIN"
589
+ for c in bash sh grep sed awk cut tr mktemp rm cat sort head wc printf cksum python3 dirname basename; do
590
+ p=$(command -v "$c" 2>/dev/null); case "$p" in /*) ln -sf "$p" "$CAPBIN/$c" ;; esac
591
+ done
592
+ if env PATH="$CAPBIN" sh -c 'command -v bash >/dev/null 2>&1' && ! env PATH="$CAPBIN" sh -c 'command -v find >/dev/null 2>&1'; then
593
+ env PATH="$CAPBIN" bash "$CAPSH" --target "$CAPD" >/dev/null 2>&1; rc=$?
594
+ [ "$rc" -eq 10 ] && ok "CAP4 find unavailable → HARNESS_ERROR (instrument absence is not target absence)" \
595
+ || bad "CAP4 returned rc=$rc, expected 10 — a missing instrument is reported as 'no targets' (3)"
596
+ else
597
+ printf ' – CAP4 lane NOT RUN (stub PATH unusable: bash missing or find still resolvable) — not a pass\n'
598
+ fi
599
+ fi
600
+
601
+ # ── Python-fence lanes (added 2026-08-12) ─────────────────────────────────────────────────────
602
+ # ```python fences were outside the extractor, so `except: pass` — the exact shape probes A/B exist
603
+ # for — was unmeasured wherever it actually lives in this corpus (SKILL.md, not .py).
604
+ printf '# t\n\n```python\ndef f(x):\n try:\n return g(x)\n except Exception:\n return True\n```\n' > "$MDT/py.md"
605
+ out=$(bash "$SCAN" "$MDT/py.md" 2>&1)
606
+ if printf '%s' "$out" | grep -q 'python fence'; then
607
+ ok "PY1 a defect inside a \`\`\`python fence is detected and LABELLED as a python fence"
608
+ else
609
+ bad "PY1 python fence not scanned (or mislabelled as bash) — the surface where except/pass lives is unmeasured"
610
+ fi
611
+
612
+ # PY2 — the reason the shadow carries a .py extension instead of reusing .sh. `is_sh` keys on the
613
+ # extension, so a python fence written into a .sh shadow would be probed by the SHELL rules. The
614
+ # fixture below is python whose COMMENT contains a shell shape; a .sh shadow scores it S1.
615
+ printf '# t\n\n```python\n# scan=$(run) || exit 0\ndef f():\n return None\n```\n' > "$MDT/py_shellish.md"
616
+ n=$(bash "$SCAN" "$MDT/py_shellish.md" 2>&1 | grep -cE '\[S[0-9]:')
617
+ [ "$n" -eq 0 ] && ok "PY2 python fences are NOT probed by the shell rules (language-split shadows hold)" \
618
+ || bad "PY2 shell probes fired $n time(s) on python source — the shadow extension collapsed back to .sh"
619
+
620
+ # PY3 — a markdown file carrying BOTH fence languages must yield BOTH, at their own origin lines.
621
+ # NOTE the python half returns True, not `pass`: probe A keys on a permissive RETURN. The first
622
+ # draft of this lane used `pass` and went red against a correct implementation — the fixture was
623
+ # measuring nothing. Verified by running the extractor directly on both languages before editing
624
+ # any code (the shadows were fine; the assertion was not).
625
+ printf '# t\n\n```bash\nverify() { run || return 0; }\n```\n\n```python\ndef f(x):\n try:\n return g(x)\n except Exception:\n return True\n```\n' > "$MDT/both.md"
626
+ out=$(bash "$SCAN" "$MDT/both.md" 2>&1)
627
+ if printf '%s' "$out" | grep -q 'bash fence' && printf '%s' "$out" | grep -q 'python fence'; then
628
+ ok "PY3 one markdown file yields both a bash and a python shadow (neither overwrites the other)"
629
+ else
630
+ bad "PY3 mixed-language markdown lost one language — shadows collide by name or the loop stops at the first hit"
631
+ fi
632
+
218
633
  echo "----"
219
634
  echo "degrade-scan shell probes: $pass passed, $fail failed"
220
635
  [ "$fail" -eq 0 ] || exit 1
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ # Validate YAML frontmatter in all SKILL.md files AND all agent definitions.
3
+ # Catches frontmatter that does not parse — the failure mode that breaks Codex/Gemini plugin parsers.
4
+ # Usage: ./scripts/validate_yaml.sh [--fix]
5
+ #
6
+ # ── 2026-07-26 rewrite: the checker was the defect, not the skills it flagged ──────────────────
7
+ # The previous version grepped the description VALUE for ': ' and reported an error on a hit. It never
8
+ # asked whether the value was QUOTED, so a correctly quoted description (which parses fine) was
9
+ # reported as broken — a false positive that cannot be cleared by fixing the file. Worse, `--fix`
10
+ # would then wrap the already-quoted value in a `>-` block scalar, folding the quote characters INTO
11
+ # the string: an auto-fix that corrupts a correct file. Its sed also ran over the whole file rather
12
+ # than the frontmatter, so any body line beginning with 'description: ' was a target too.
13
+ #
14
+ # Ground truth here is now the parser: does the frontmatter load? A heuristic about punctuation is
15
+ # not a substitute for parsing the thing whose parseability is the question. When no YAML parser is
16
+ # reachable the check does not silently degrade to the old guess — it reports UNCALIBRATED and exits
17
+ # non-zero, because "I could not measure" is not "it is fine" (CLAUDE.md §Instrument Calibration).
18
+ #
19
+ # ── 2026-08-11: agents added to the scanned surface ───────────────────────────────────────────
20
+ # The scan covered only `plugins/*/skills/*/SKILL.md`, so agent definitions were structurally
21
+ # invisible — and one of them was broken the whole time. `quench-challenger.md` carried an unquoted
22
+ # multi-line `description:` whose `user:`/`assistant:` lines parsed as new keys, which invalidated
23
+ # the `tools: Read, Grep, Glob` and the `model: opus` HARD FLOOR declared BELOW it. Measured in a
24
+ # live session agent list: it showed `(Tools: All tools)` and a fallback description while its
25
+ # siblings showed exactly what they declared — i.e. a read-only adversary was running with write
26
+ # and execute tools, and a hard tier floor was pinning nothing.
27
+ #
28
+ # Note what the check measures: **YAML validity**, not CC-tolerance. Claude Code's own loader is
29
+ # more lenient — `expert.md` had an unquoted description containing ": " and still loaded correctly
30
+ # there. That leniency is not portable, and this file's whole reason for existing is the parsers
31
+ # that are not lenient (Codex/Gemini plugin loaders). So a file CC accepts can still fail here, on
32
+ # purpose. Calibrated at wiring time on a known pair: `quench-challenger.md`/`expert.md` failed
33
+ # before their fixes, the other six passed — a checker that flagged everything or nothing would
34
+ # have proven nothing.
35
+ set -uo pipefail
36
+
37
+ FIX=${1:-}
38
+ ERRORS=0
39
+ UNCALIBRATED=0
40
+
41
+ # Parser availability is resolved ONCE, loudly, before any file is judged.
42
+ PARSER=""
43
+ if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' >/dev/null 2>&1; then
44
+ PARSER=python3
45
+ fi
46
+
47
+ check_skill() {
48
+ local file="$1"
49
+ local skill; skill="${2:-$(basename "$(dirname "$file")")}"
50
+
51
+ if [ -z "$PARSER" ]; then
52
+ echo " ⚠️ $skill: UNCALIBRATED — no YAML parser available, frontmatter NOT verified"
53
+ UNCALIBRATED=$((UNCALIBRATED + 1))
54
+ return 0
55
+ fi
56
+
57
+ local out rc
58
+ out=$(python3 - "$file" <<'PY' 2>&1
59
+ import sys, yaml
60
+ path = sys.argv[1]
61
+ text = open(path, encoding='utf-8').read()
62
+ # Frontmatter = content between the first two '---' fence lines.
63
+ lines = text.split('\n')
64
+ if not lines or lines[0].strip() != '---':
65
+ print('NOFRONTMATTER'); sys.exit(3)
66
+ end = next((i for i, l in enumerate(lines[1:], start=1) if l.strip() == '---'), None)
67
+ if end is None:
68
+ print('UNTERMINATED'); sys.exit(3)
69
+ fm = '\n'.join(lines[1:end])
70
+ try:
71
+ data = yaml.safe_load(fm)
72
+ except yaml.YAMLError as e:
73
+ print('PARSE ' + str(e).replace('\n', ' ')[:160]); sys.exit(1)
74
+ if not isinstance(data, dict):
75
+ print('NOTMAPPING'); sys.exit(2)
76
+ missing = [k for k in ('name', 'description') if not data.get(k)]
77
+ if missing:
78
+ print('MISSING ' + ','.join(missing)); sys.exit(2)
79
+ print('OK')
80
+ PY
81
+ ); rc=$?
82
+
83
+ case "$rc" in
84
+ 0) return 0 ;;
85
+ *)
86
+ echo " ❌ $skill: frontmatter does not validate — $out"
87
+ ERRORS=$((ERRORS + 1))
88
+ if [ "$FIX" = "--fix" ]; then
89
+ # Only ONE repair is attempted, and only for the case it actually addresses: an unquoted
90
+ # description scalar that fails to parse. Quoting is preferred over a `>-` block scalar
91
+ # because it is a single-line, reversible edit that leaves the string bytes unchanged.
92
+ # A value already containing a single quote is left alone — escaping it correctly is a
93
+ # judgment call, and a wrong auto-fix on a parse error is worse than a reported one.
94
+ local dline val
95
+ dline=$(awk '/^---/{n++; if(n==2) exit} n==1' "$file" | grep -m1 '^description: ' || true)
96
+ val="${dline#description: }"
97
+ if [ -n "$dline" ] \
98
+ && ! printf '%s' "$val" | grep -q "'" \
99
+ && ! printf '%s' "$val" | grep -qE "^['\">|]"; then
100
+ python3 - "$file" "$dline" "$val" <<'PY'
101
+ import sys
102
+ path, dline, val = sys.argv[1], sys.argv[2], sys.argv[3]
103
+ s = open(path, encoding='utf-8').read()
104
+ open(path, 'w', encoding='utf-8').write(s.replace(dline, "description: '" + val + "'", 1))
105
+ PY
106
+ echo " → quoted the description scalar; re-run to confirm"
107
+ else
108
+ echo " → NOT auto-fixed (already quoted/blocked, or contains a single quote) — fix by hand"
109
+ fi
110
+ fi
111
+ ;;
112
+ esac
113
+ }
114
+
115
+ echo "=== SKILL.md + agent YAML validation ==="
116
+ [ -z "$PARSER" ] && echo " ⚠️ no YAML parser (python3 + pyyaml) — this run cannot verify anything"
117
+ _skills=0
118
+ for dir in plugins/*/skills/*/; do
119
+ file="${dir}SKILL.md"
120
+ [ -f "$file" ] && { check_skill "$file"; _skills=$((_skills + 1)); }
121
+ done
122
+ _agents=0
123
+ for file in plugins/*/agents/*.md .claude/agents/*.md; do
124
+ [ -f "$file" ] || continue # unmatched glob is not a file — no nullglob assumption
125
+ check_skill "$file" "agent:$(basename "$file" .md)"
126
+ _agents=$((_agents + 1))
127
+ done
128
+ # A surface that scans zero files is an instrument error, not a pass — the exact shape this
129
+ # addition exists to close (agents were an empty set for the whole life of the checker).
130
+ if [ "$_skills" -eq 0 ] || [ "$_agents" -eq 0 ]; then
131
+ echo " ❌ INSTRUMENT ERROR — scanned skills=$_skills agents=$_agents; a zero surface cannot pass"
132
+ exit 3
133
+ fi
134
+ echo " (scanned: $_skills skill(s), $_agents agent(s))"
135
+
136
+ echo ""
137
+ if [ "$UNCALIBRATED" -gt 0 ]; then
138
+ echo " ⚠️ UNCALIBRATED — $UNCALIBRATED skill(s) unverified (no parser). Not a pass."
139
+ exit 2
140
+ fi
141
+ if [ "$ERRORS" -eq 0 ]; then
142
+ echo " ✅ All skills + agents: frontmatter parses"
143
+ exit 0
144
+ fi
145
+ echo " $ERRORS error(s). Run with --fix for the unquoted-description case; others are by hand."
146
+ exit 1