@chrono-meta/fh-gate 1.4.73 → 1.4.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +14 -1
- package/docs/ETHOS.md +106 -0
- package/docs/OUTPUT_EVIDENCE.md +118 -0
- package/docs/WHY.md +42 -0
- package/knowledge/patterns/ensemble_union_detection_task_pattern.md +125 -0
- package/knowledge/shared/GLOSSARY.md +77 -0
- package/knowledge/shared/harness-core/harness_frontier_diagnosis_2026-06-02.md +1 -1
- package/knowledge/shared/harness-core/meta_harness_engineering_definition.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +9 -0
- package/knowledge/shared/patterns/multi-persona-review.md +88 -0
- package/knowledge/shared/plugin-catalog/recommended_plugins.md +117 -0
- package/package.json +28 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +617 -0
- package/scripts/below_floor_scan.sh +91 -0
- package/scripts/chamber_run.sh +184 -0
- package/scripts/degrade_direction_scan.sh +17 -2
- package/scripts/fh_env_delta_scan.sh +108 -0
- package/scripts/memory_link_check.py +237 -0
- package/scripts/memory_nearcheck.py +131 -0
- package/scripts/package_coverage_check.sh +140 -0
- package/scripts/selfcheck.sh +47 -0
- package/scripts/session_close_check.sh +31 -1
- package/scripts/sidecar_wait.sh +76 -0
- package/scripts/substrate_jump_detector.sh +60 -0
- package/scripts/test_card_drift_probe.sh +77 -0
- package/scripts/test_degrade_scan_shell_probes.sh +26 -0
- package/scripts/test_marker_floor_lanes.sh +45 -0
- package/scripts/test_memory_link_check.sh +134 -0
- package/scripts/test_session_close_lanes.sh +99 -0
- package/scripts/tier_census_grep.sh +54 -0
- package/templates/.claude/rules/session.md +153 -0
- package/templates/contrib_session.md +34 -0
- package/templates/degrade_direction_scan.sh +17 -2
- package/templates/goal-quench-hook-setup.md +152 -0
- package/templates/starter_profile.md +83 -0
- package/templates/temper_check.sh +46 -0
- package/plugins/fh-meta/skills/context-bridge-dispatch/SKILL.md +0 -32
- package/plugins/fh-meta/skills/self-marketing-lint/SKILL.md +0 -30
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""nearcheck.py — read-only near-duplicate probe over a markdown memory store.
|
|
3
|
+
|
|
4
|
+
Design absorbed from obsidian-mind's `memory-similarity.ts` (MIT, breferrari) — the PROPOSITIONS,
|
|
5
|
+
not the code:
|
|
6
|
+
* lexical, not semantic. An embedding call on the write hot path costs latency and money, and
|
|
7
|
+
the failure being prevented is the LITERAL re-record (a session restating a lesson in nearly
|
|
8
|
+
the same words), not two genuinely different phrasings. Semantics is what RECALL is for.
|
|
9
|
+
* facet-gated: two notes are only comparable when their declared reach overlaps.
|
|
10
|
+
* pure: no IO in the scoring core, so it can be calibrated on fixtures.
|
|
11
|
+
|
|
12
|
+
DEVIATION, and why: obsidian-mind tokenizes on word boundaries. This corpus is majority KOREAN,
|
|
13
|
+
where whitespace tokens are morphologically inflected and word-level overlap under-reports badly —
|
|
14
|
+
the same failure class this repo already measured (an ASCII-token scanner over a Korean corpus
|
|
15
|
+
produced ~96% false positives). So the shingles here are CHARACTER 3-grams over NFC-normalized,
|
|
16
|
+
markup-stripped text, which is script-agnostic.
|
|
17
|
+
|
|
18
|
+
READ-ONLY. Writes nothing. Exit 0 always — this is a measurement, not a gate.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
import re, sys, unicodedata
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from itertools import combinations
|
|
24
|
+
|
|
25
|
+
N = 3 # character shingle size
|
|
26
|
+
TOP = 40 # how many pairs to print
|
|
27
|
+
|
|
28
|
+
def strip_markup(t: str) -> str:
|
|
29
|
+
t = re.sub(r"```.*?```", " ", t, flags=re.S) # fenced code
|
|
30
|
+
t = re.sub(r"`[^`]*`", " ", t) # inline code
|
|
31
|
+
t = re.sub(r"\[\[([^\]]*)\]\]", r"\1", t) # wikilinks -> text
|
|
32
|
+
t = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", t) # md links -> text
|
|
33
|
+
t = re.sub(r"https?://\S+", " ", t)
|
|
34
|
+
t = re.sub(r"[#*_>|\-–—·:;,.!?()\[\]{}\"'`~^=+/\\]", " ", t)
|
|
35
|
+
return re.sub(r"\s+", " ", t).strip()
|
|
36
|
+
|
|
37
|
+
def parse(p: Path) -> dict:
|
|
38
|
+
raw = p.read_text(encoding="utf-8", errors="ignore")
|
|
39
|
+
fm, body = {}, raw
|
|
40
|
+
if raw.startswith("---"):
|
|
41
|
+
end = raw.find("\n---", 3)
|
|
42
|
+
if end > 0:
|
|
43
|
+
for line in raw[3:end].splitlines():
|
|
44
|
+
if ":" in line and not line.startswith(" "):
|
|
45
|
+
k, v = line.split(":", 1)
|
|
46
|
+
fm[k.strip()] = v.strip().strip('"')
|
|
47
|
+
body = raw[end + 4 :]
|
|
48
|
+
return {
|
|
49
|
+
"path": p,
|
|
50
|
+
"name": fm.get("name", p.stem),
|
|
51
|
+
"desc": fm.get("description", ""),
|
|
52
|
+
"type": fm.get("type", ""),
|
|
53
|
+
"body": unicodedata.normalize("NFC", strip_markup(body)),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def shingles(t: str) -> set[str]:
|
|
57
|
+
t = t.replace(" ", "")
|
|
58
|
+
return {t[i : i + N] for i in range(max(0, len(t) - N + 1))}
|
|
59
|
+
|
|
60
|
+
def jaccard(a: set, b: set) -> float:
|
|
61
|
+
if not a or not b:
|
|
62
|
+
return 0.0
|
|
63
|
+
inter = len(a & b)
|
|
64
|
+
return inter / (len(a) + len(b) - inter)
|
|
65
|
+
|
|
66
|
+
def containment(a: set, b: set) -> float:
|
|
67
|
+
"""Asymmetric: how much of the SMALLER note is inside the larger.
|
|
68
|
+
|
|
69
|
+
Jaccard alone under-reports the case that matters most here — a short lesson later restated
|
|
70
|
+
inside a longer note.
|
|
71
|
+
|
|
72
|
+
SIZE-GATED (repaired 2026-07-28 after the probe's first run): without the gate this metric
|
|
73
|
+
SATURATES on size-asymmetric pairs. Korean character 3-grams have a very high base rate
|
|
74
|
+
(inflectional endings and particles recur everywhere), so a 22 KB note "contains" almost every
|
|
75
|
+
short note's shingles and the top of the ranking became one large file paired with everything.
|
|
76
|
+
Hand-check that killed it: user_role.md (658 B, "user is a QA engineer") scored 0.80 against a
|
|
77
|
+
22 KB provenance note. Not a near-duplicate by any reading — an instrument artifact.
|
|
78
|
+
A restatement worth flagging is of COMPARABLE size; a short note swallowed by a long one is
|
|
79
|
+
the containment metric measuring the alphabet, not the content.
|
|
80
|
+
"""
|
|
81
|
+
if not a or not b:
|
|
82
|
+
return 0.0
|
|
83
|
+
small, large = (a, b) if len(a) <= len(b) else (b, a)
|
|
84
|
+
if len(small) < 0.34 * len(large):
|
|
85
|
+
return 0.0
|
|
86
|
+
return len(small & large) / len(small)
|
|
87
|
+
|
|
88
|
+
def main(argv):
|
|
89
|
+
root = Path(argv[1])
|
|
90
|
+
docs = [parse(p) for p in sorted(root.glob("*.md"))
|
|
91
|
+
if p.name not in {"MEMORY.md", "MEMORY_archive.md"}]
|
|
92
|
+
if not docs:
|
|
93
|
+
print("EXTRACTOR_BROKE: 0 documents parsed — the probe did not run", file=sys.stderr)
|
|
94
|
+
return 2
|
|
95
|
+
# Corpus-frequency filter — the second half of the same repair. A shingle present in most
|
|
96
|
+
# notes carries no evidence of duplication; it is this corpus's alphabet. Dropping the common
|
|
97
|
+
# band is the lexical equivalent of a stopword list, derived rather than hand-listed so it
|
|
98
|
+
# transfers to a corpus in any script.
|
|
99
|
+
from collections import Counter
|
|
100
|
+
df = Counter()
|
|
101
|
+
for d in docs:
|
|
102
|
+
d["sh_raw"] = shingles(d["body"])
|
|
103
|
+
df.update(d["sh_raw"])
|
|
104
|
+
cutoff = max(2, int(0.15 * len(docs)))
|
|
105
|
+
common = {g for g, n in df.items() if n > cutoff}
|
|
106
|
+
for d in docs:
|
|
107
|
+
d["sh"] = d["sh_raw"] - common
|
|
108
|
+
d["shd"] = shingles(d["desc"]) - common
|
|
109
|
+
|
|
110
|
+
empty = [d["path"].name for d in docs if len(d["sh"]) < 20]
|
|
111
|
+
pairs = []
|
|
112
|
+
for a, b in combinations(docs, 2):
|
|
113
|
+
j = jaccard(a["sh"], b["sh"])
|
|
114
|
+
c = containment(a["sh"], b["sh"])
|
|
115
|
+
jd = jaccard(a["shd"], b["shd"])
|
|
116
|
+
if j >= 0.25 or c >= 0.55 or jd >= 0.45:
|
|
117
|
+
pairs.append((max(j, c), j, c, jd, a, b))
|
|
118
|
+
pairs.sort(key=lambda t: -t[0])
|
|
119
|
+
|
|
120
|
+
print(f"scanned: {len(docs)} notes (skipped index files)")
|
|
121
|
+
print(f"corpus-common shingles dropped: {len(common)} (present in >{cutoff} notes)")
|
|
122
|
+
print(f"too-short-to-score (<20 shingles): {len(empty)}" + (f" → {empty}" if empty else ""))
|
|
123
|
+
print(f"candidate pairs above threshold: {len(pairs)}\n")
|
|
124
|
+
for score, j, c, jd, a, b in pairs[:TOP]:
|
|
125
|
+
print(f"[{score:.2f}] jac={j:.2f} cont={c:.2f} desc={jd:.2f}")
|
|
126
|
+
print(f" A {a['path'].name}")
|
|
127
|
+
print(f" B {b['path'].name}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# package_coverage_check.sh — a shipped document must not point at a file the package omits.
|
|
3
|
+
#
|
|
4
|
+
# WHY (measured 2026-07-28): the npm tarball shipped CLAUDE.md, README, CATALOG and the knowledge/
|
|
5
|
+
# base while omitting much of what they instruct the reader to open — 35 distinct paths existed in
|
|
6
|
+
# the repo, were named by a shipped document, and were absent from the tarball. `CLAUDE.md` told
|
|
7
|
+
# consumers to run `templates/predelete_check.sh` before a destructive op; that file did not ship.
|
|
8
|
+
# A gate you are told to run and cannot run is worse than one you were never told about.
|
|
9
|
+
#
|
|
10
|
+
# This is the ANTI-REGROWTH instrument for that class. Closing the 35 once is worth little: the set
|
|
11
|
+
# regrows every time a doc gains a reference or files[] gains an entry. So the check is mechanical
|
|
12
|
+
# and the exceptions are ENUMERATED, never implicit.
|
|
13
|
+
#
|
|
14
|
+
# SOURCE-TREE ONLY. Inside an installed package the un-shipped files are legitimately absent and
|
|
15
|
+
# package.json's files[] may not even be present in a comparable form, so the check self-skips.
|
|
16
|
+
#
|
|
17
|
+
# Usage: bash scripts/package_coverage_check.sh
|
|
18
|
+
# Exit: 0 = every referenced path is either shipped or explicitly accepted; 1 = a new phantom.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
cd "$REPO_ROOT" || exit 1
|
|
23
|
+
|
|
24
|
+
if [ ! -d .git ] || [ ! -f package.json ]; then
|
|
25
|
+
echo "SKIP package-coverage (not a source checkout)"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ── Accepted-absent, with the reason each one is NOT a defect ────────────────────────────────
|
|
30
|
+
# Adding a line here is a decision, not a silencer: each entry states why shipping it would be
|
|
31
|
+
# wrong. If you cannot write that sentence, the file probably belongs in files[].
|
|
32
|
+
#
|
|
33
|
+
# .claude/registry/LOCAL_SKILL_REGISTRY.md — per-environment, generated by the registry scan.
|
|
34
|
+
# Its consumers probe it with `ls … 2>/dev/null` and handle absence; shipping one machine's
|
|
35
|
+
# registry would hand every consumer a false map of skills they do not have.
|
|
36
|
+
# .claude/regression/probes.md — per-environment prompt-regression baselines. The
|
|
37
|
+
# skill explicitly prints NO_CUSTOM_PROBES when absent. Shipping FH's baselines would make
|
|
38
|
+
# a consumer's regression run compare against someone else's harness.
|
|
39
|
+
# scripts/sync-to-be.sh — operator-private companion-store sync. Never ships.
|
|
40
|
+
# (The reference that flags it is a TEST FIXTURE in prepush_guard_check.sh which *creates*
|
|
41
|
+
# a file of that name to exercise the LOW allowlist — not a pointer to this file at all.)
|
|
42
|
+
# scripts/sync_guard_check.sh — anchor for that same operator-private mirror sync;
|
|
43
|
+
# no shipped hook invokes it.
|
|
44
|
+
ACCEPTED_ABSENT=(
|
|
45
|
+
".claude/registry/LOCAL_SKILL_REGISTRY.md"
|
|
46
|
+
".claude/regression/probes.md"
|
|
47
|
+
"scripts/sync-to-be.sh"
|
|
48
|
+
"scripts/sync_guard_check.sh"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
out=$(python3 - "${ACCEPTED_ABSENT[@]}" <<'PY'
|
|
52
|
+
import re, os, json, sys
|
|
53
|
+
accepted = set(sys.argv[1:])
|
|
54
|
+
files = json.load(open('package.json'))['files']
|
|
55
|
+
|
|
56
|
+
def covered(p):
|
|
57
|
+
return any(p == f or p.startswith(f.rstrip('/') + '/') for f in files)
|
|
58
|
+
|
|
59
|
+
shipped = []
|
|
60
|
+
for f in files:
|
|
61
|
+
if os.path.isfile(f):
|
|
62
|
+
shipped.append(f)
|
|
63
|
+
elif os.path.isdir(f):
|
|
64
|
+
for root, _, names in os.walk(f):
|
|
65
|
+
shipped.extend(os.path.join(root, n) for n in names)
|
|
66
|
+
|
|
67
|
+
# Only text surfaces can carry a reference a human or agent would follow.
|
|
68
|
+
shipped = [s for s in shipped if s.endswith(('.md', '.sh', '.js', '.json', '.yaml', '.yml'))]
|
|
69
|
+
|
|
70
|
+
pat = re.compile(
|
|
71
|
+
r'(?<![\w/.-])((?:scripts|templates|bin|docs|knowledge|plugins|\.claude)'
|
|
72
|
+
r'/[A-Za-z0-9_./-]+\.(?:sh|py|js|md|yaml|yml|json|defaults))'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
phantom = {}
|
|
76
|
+
exercised = set() # accepted entries a shipped doc ACTUALLY still points at
|
|
77
|
+
for s in shipped:
|
|
78
|
+
try:
|
|
79
|
+
text = open(s, encoding='utf-8', errors='ignore').read()
|
|
80
|
+
except OSError:
|
|
81
|
+
continue
|
|
82
|
+
for m in set(pat.findall(text)):
|
|
83
|
+
# Only a path that REALLY EXISTS here but is left out of the tarball is this defect.
|
|
84
|
+
# A path that exists nowhere is the ordinary phantom-reference class the ref-path
|
|
85
|
+
# check above already owns; a path outside files[] that is also absent is nothing.
|
|
86
|
+
if os.path.exists(m) and not covered(m):
|
|
87
|
+
if m in accepted:
|
|
88
|
+
exercised.add(m)
|
|
89
|
+
else:
|
|
90
|
+
phantom.setdefault(m, set()).add(s)
|
|
91
|
+
|
|
92
|
+
# Impossible-zero guard: this repo always has shipped docs. Zero scanned means the extractor
|
|
93
|
+
# broke — report that as a failure rather than letting a dead check print a pass
|
|
94
|
+
# (same rule as count_check.sh and the ref-path extractor).
|
|
95
|
+
if not shipped:
|
|
96
|
+
print("EXTRACTOR_BROKE")
|
|
97
|
+
raise SystemExit(2)
|
|
98
|
+
|
|
99
|
+
for p, srcs in sorted(phantom.items(), key=lambda kv: (-len(kv[1]), kv[0])):
|
|
100
|
+
print(f"{p}\t{len(srcs)}\t{sorted(srcs)[0]}")
|
|
101
|
+
|
|
102
|
+
# An exception nobody exercises is a SILENCER waiting for a future real case to land on it.
|
|
103
|
+
# Report the unexercised ones so the list stays a set of decisions, not accumulated residue.
|
|
104
|
+
for a in sorted(accepted - exercised):
|
|
105
|
+
print(f"STALE\t{a}")
|
|
106
|
+
raise SystemExit(1 if phantom else 0)
|
|
107
|
+
PY
|
|
108
|
+
)
|
|
109
|
+
rc=$?
|
|
110
|
+
|
|
111
|
+
if [ "$rc" -eq 2 ] || [ "$out" = "EXTRACTOR_BROKE" ]; then
|
|
112
|
+
echo "FAIL package-coverage: extractor scanned 0 shipped docs — the check broke, it did not pass"
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
STALE_LIST=$(printf '%s\n' "$out" | grep '^STALE ' | cut -f2- || true)
|
|
117
|
+
STALE_N=$(printf '%s' "$STALE_LIST" | grep -c . || true)
|
|
118
|
+
EXERCISED_N=$(( ${#ACCEPTED_ABSENT[@]} - ${STALE_N:-0} ))
|
|
119
|
+
|
|
120
|
+
if [ "$rc" -ne 0 ]; then
|
|
121
|
+
echo "FAIL package-coverage: shipped document(s) point at file(s) the package omits:"
|
|
122
|
+
printf '%s\n' "$out" | grep -v '^STALE ' | while IFS=$'\t' read -r path n src; do
|
|
123
|
+
[ -z "$path" ] && continue
|
|
124
|
+
printf ' %s (named by %s shipped doc(s), e.g. %s)\n' "$path" "$n" "$src"
|
|
125
|
+
done
|
|
126
|
+
echo " Fix: add the path to package.json files[], OR list it in ACCEPTED_ABSENT here"
|
|
127
|
+
echo " with a one-sentence reason why shipping it would be wrong."
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Advisory only — a stale exception is hygiene, not a shipped-doc defect, so it must not
|
|
132
|
+
# convert a clean package into a red gate (that trains the runner to ignore the check).
|
|
133
|
+
if [ "${STALE_N:-0}" -gt 0 ]; then
|
|
134
|
+
echo "⚠️ package-coverage: ${STALE_N} ACCEPTED_ABSENT entry(ies) no longer exercised — remove, or a"
|
|
135
|
+
echo " future real omission can land on the stale exception and be silenced:"
|
|
136
|
+
printf '%s\n' "$STALE_LIST" | sed 's/^/ /'
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
echo "PASS package-coverage: every referenced path is shipped or explicitly accepted (${#ACCEPTED_ABSENT[@]} accepted, ${EXERCISED_N} exercised)"
|
|
140
|
+
exit 0
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -99,6 +99,53 @@ else
|
|
|
99
99
|
fail=1
|
|
100
100
|
fi
|
|
101
101
|
|
|
102
|
+
# package-coverage — a shipped doc must not point at a file the tarball omits. Distinct from the
|
|
103
|
+
# ref-path check below: that one asks "does this path exist at all", this one asks "does the
|
|
104
|
+
# CONSUMER get it". Measured 2026-07-28: 35 paths existed, were named by a shipped doc, and were
|
|
105
|
+
# absent from the tarball — including templates/predelete_check.sh, which CLAUDE.md instructs you
|
|
106
|
+
# to run before a destructive op. Wired here in the same commit that created it, because the two
|
|
107
|
+
# previous anchors this session shipped with zero callers.
|
|
108
|
+
if [ -f scripts/package_coverage_check.sh ]; then
|
|
109
|
+
if ! bash scripts/package_coverage_check.sh; then
|
|
110
|
+
fail=1
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
|
|
115
|
+
# edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
|
|
116
|
+
# that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
|
|
117
|
+
# indistinguishable from "nothing is related", so the doctrine degraded silently. The checker's
|
|
118
|
+
# --fix-separators path WRITES, so its anchor runs here rather than being invoked by hand.
|
|
119
|
+
# Package/other-machine mode: the checker self-SKIPs when no memory dir exists.
|
|
120
|
+
if [ -f scripts/test_memory_link_check.sh ] && [ -f scripts/memory_link_check.py ]; then
|
|
121
|
+
if ! bash scripts/test_memory_link_check.sh; then
|
|
122
|
+
fail=1
|
|
123
|
+
fi
|
|
124
|
+
elif [ -f scripts/memory_link_check.py ]; then
|
|
125
|
+
echo "FAIL memory-link-check: checker present but scripts/test_memory_link_check.sh missing"
|
|
126
|
+
fail=1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# session-close gate lanes (② harvest-loop discharge + ⑤ card-last) and the ⑤-b card-drift probe.
|
|
130
|
+
# Both anchors calibrate scripts/session_close_check.sh, which the pre-push hook runs on every push
|
|
131
|
+
# — an uncalibrated instrument there produces exactly the false verdicts the close chain exists to
|
|
132
|
+
# prevent. test_card_drift_probe.sh had shipped with ZERO callers since it was written; wiring it
|
|
133
|
+
# here closes that, and the anchors are added to files[] in the same change so package mode runs
|
|
134
|
+
# them too rather than reporting a deleted anchor.
|
|
135
|
+
for _anchor in scripts/test_session_close_lanes.sh scripts/test_card_drift_probe.sh; do
|
|
136
|
+
if [ ! -f scripts/session_close_check.sh ]; then
|
|
137
|
+
echo "SKIP ${_anchor##*/} (subject scripts/session_close_check.sh absent)"
|
|
138
|
+
elif [ -f "$_anchor" ]; then
|
|
139
|
+
if ! bash "$_anchor"; then
|
|
140
|
+
fail=1
|
|
141
|
+
fi
|
|
142
|
+
else
|
|
143
|
+
# subject present, anchor gone => the calibration was deleted. Real failure, not a skip.
|
|
144
|
+
echo "FAIL ${_anchor##*/}: session_close_check.sh present but its anchor is missing"
|
|
145
|
+
fail=1
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
|
|
102
149
|
# Referenced-path existence is a source-tree check. The npm package intentionally
|
|
103
150
|
# ships a narrower runtime surface, so package-mode selfcheck skips this section.
|
|
104
151
|
if [ -d ".claude/rules" ]; then
|
|
@@ -42,10 +42,28 @@ fi
|
|
|
42
42
|
# masking a real match as pipeline failure so the warning NEVER fired on true positives
|
|
43
43
|
# (caught by a Sonnet blind probe 2026-07-10, 5/5 deterministic repro). `grep -c` reads the
|
|
44
44
|
# whole stream; `|| true` guards its exit-1-on-zero.
|
|
45
|
+
#
|
|
46
|
+
# SATISFIABLE (2026-07-28): this warning used to fire on EVERY close that touched an FH asset, with
|
|
47
|
+
# no way for the session to discharge it — the script had no means of observing whether harvest-loop
|
|
48
|
+
# ran. A permanently-firing line is noise, and noise trains the runner to skim past the ❌ lines that
|
|
49
|
+
# do matter (the same objection raised against pre-push:484 on 2026-07-28 — one standard, both places).
|
|
50
|
+
# So the obligation now has a mechanical discharge: a `harvest-loop` mention in TODAY's fh_completed
|
|
51
|
+
# file. HONEST SCOPE: this tests ACKNOWLEDGMENT, not execution — the script cannot see a skill run.
|
|
52
|
+
# Recording "harvest-loop: skipped, <reason>" discharges it exactly as recording a run does, which is
|
|
53
|
+
# correct: CLAUDE.md ② accepts "harvest-loop (or an explicit skip note)". What it now catches is the
|
|
54
|
+
# real miss class — closing with FH assets changed and *no decision recorded either way*.
|
|
45
55
|
FH_CHANGED=$(git -C "$FH" log --since="today 00:00" --name-only --pretty=format: 2>/dev/null \
|
|
46
56
|
| grep -cE '^(plugins/.*SKILL\.md|\.claude/rules/|templates/|CLAUDE\.md|knowledge/)' || true)
|
|
47
57
|
if [ "${FH_CHANGED:-0}" -gt 0 ]; then
|
|
48
|
-
|
|
58
|
+
HL_NOTED=0
|
|
59
|
+
[ -f "$FH/tracks/_meta/fh_completed_${TODAY}.md" ] \
|
|
60
|
+
&& HL_NOTED=$(grep -ciE 'harvest[-_]loop' "$FH/tracks/_meta/fh_completed_${TODAY}.md" || true)
|
|
61
|
+
if [ "${HL_NOTED:-0}" -gt 0 ]; then
|
|
62
|
+
echo "✅ ② FH assets changed today ($FH_CHANGED path-touch(es)) — harvest-loop decision recorded in fh_completed_${TODAY}.md"
|
|
63
|
+
else
|
|
64
|
+
echo "⚠️ ② FH assets changed today ($FH_CHANGED path-touch(es)) and fh_completed_${TODAY}.md records no"
|
|
65
|
+
echo " harvest-loop decision — run it, or write one line stating the skip and why (either discharges this)"
|
|
66
|
+
fi
|
|
49
67
|
fi
|
|
50
68
|
|
|
51
69
|
# ④ real-time completion log — required whenever any commit landed today
|
|
@@ -100,6 +118,18 @@ if [ -f "$CARD" ]; then
|
|
|
100
118
|
NEWER=$(find "$FH/tracks/_meta" -maxdepth 1 -type f \( -name "fh_completed_*.md" -o -name "fh_signal_*.md" \) -newer "$CARD" 2>/dev/null | wc -l | tr -d ' ')
|
|
101
119
|
if [ "$NEWER" -gt 0 ]; then
|
|
102
120
|
echo "❌ ⑤ card-last violated — $NEWER close artifact(s) newer than the session card; re-run ⑤ (delta update)"
|
|
121
|
+
# NAME the offenders. Recurrence N=3 (2026-07-28, three closes in one day): every repair so far
|
|
122
|
+
# was a prose vow ("next time I'll write the finding into the card first") and every one failed,
|
|
123
|
+
# because the reflex fires mid-close. What is mechanizable is not the reflex but the COST of the
|
|
124
|
+
# miss — a bare count makes ⑤ a re-read of the whole session, while naming the files and showing
|
|
125
|
+
# what landed after the card makes the delta update a minute's work, which is what actually gets
|
|
126
|
+
# done rather than deferred. Diagnosis, not prevention: this line does not claim to stop the miss.
|
|
127
|
+
find "$FH/tracks/_meta" -maxdepth 1 -type f \( -name "fh_completed_*.md" -o -name "fh_signal_*.md" \) -newer "$CARD" 2>/dev/null \
|
|
128
|
+
| while IFS= read -r f; do
|
|
129
|
+
echo " ↳ ${f#"$FH"/}"
|
|
130
|
+
tail -n 3 "$f" 2>/dev/null | sed 's/^/ │ /'
|
|
131
|
+
done
|
|
132
|
+
echo " → fold the above into the card, save the card LAST, then re-push."
|
|
103
133
|
FAIL=1
|
|
104
134
|
else
|
|
105
135
|
echo "✅ ⑤ card is the newest close artifact (card-last holds)"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sidecar_wait.sh — run a sidecar to COMPLETION and report a typed verdict about it.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS (measured twice, 2026-07-28 and 2026-07-29)
|
|
5
|
+
#
|
|
6
|
+
# A session dispatched `codex exec` and `agy -p` into the background and read their output files
|
|
7
|
+
# one second and thirty seconds later. Both were empty at that moment, so the session recorded
|
|
8
|
+
# "both sidecars returned 0-output", wrote that into a gate marker, a PR body, a session card and
|
|
9
|
+
# a memory file, and did the adversarial work itself instead.
|
|
10
|
+
#
|
|
11
|
+
# Both sidecars had in fact answered. codex produced 48 KB containing three findings — one HIGH
|
|
12
|
+
# (a normalization collision that silently reroutes a link and then looks clean forever) and one
|
|
13
|
+
# MED that showed the change was OVER-APPLIED. agy produced a HIGH of its own (anchor-form links
|
|
14
|
+
# were counted as fixable but never rewritten, so the fixer broke idempotence and over-reported
|
|
15
|
+
# its own writes). Every one was real; all were confirmed by execution and fixed.
|
|
16
|
+
#
|
|
17
|
+
# So the failure was never the sidecars. It was reading a still-running process and calling the
|
|
18
|
+
# silence a result. That mis-reading then propagated as a claim about ANOTHER system — the worst
|
|
19
|
+
# shape a measurement error can take, because it retires a working mechanism.
|
|
20
|
+
#
|
|
21
|
+
# The reflex fires mid-work and prose does not stop it (three consecutive card-last violations the
|
|
22
|
+
# day before are the same lesson). So the wait becomes mechanical: this script will not emit a
|
|
23
|
+
# verdict while the process is alive, and "no output" is only sayable after the process exits.
|
|
24
|
+
#
|
|
25
|
+
# VERDICTS (typed — grep these, never the prose)
|
|
26
|
+
# SIDECAR_VERDICT=COMPLETE exit=<n> bytes=<n> process exited on its own
|
|
27
|
+
# SIDECAR_VERDICT=TIMEOUT waited=<n>s bytes=<n> still alive when the budget ran out; NOT a result
|
|
28
|
+
# SIDECAR_VERDICT=EMPTY exit=<n> exited cleanly having written nothing — the only
|
|
29
|
+
# state in which "the sidecar said nothing" is true
|
|
30
|
+
#
|
|
31
|
+
# Usage:
|
|
32
|
+
# bash scripts/sidecar_wait.sh <outfile> <timeout_seconds> -- <command> [args...]
|
|
33
|
+
# printf '%s' "$prompt" | bash scripts/sidecar_wait.sh out.txt 600 -- codex exec -m gpt-5.5 -
|
|
34
|
+
#
|
|
35
|
+
# Exit: 0 = COMPLETE (with or without output) · 1 = TIMEOUT (verdict withheld, not a failure claim)
|
|
36
|
+
set -uo pipefail
|
|
37
|
+
|
|
38
|
+
OUT="${1:?usage: sidecar_wait.sh <outfile> <timeout_s> -- <cmd...>}"
|
|
39
|
+
BUDGET="${2:?missing timeout seconds}"
|
|
40
|
+
shift 2
|
|
41
|
+
[ "${1:-}" = "--" ] && shift
|
|
42
|
+
[ $# -gt 0 ] || { echo "sidecar_wait: no command given" >&2; exit 2; }
|
|
43
|
+
|
|
44
|
+
: > "$OUT"
|
|
45
|
+
"$@" > "$OUT" 2>&1 &
|
|
46
|
+
PID=$!
|
|
47
|
+
|
|
48
|
+
waited=0
|
|
49
|
+
last_size=0
|
|
50
|
+
# Poll rather than `wait`, so a live-but-quiet process is distinguishable from a dead one and the
|
|
51
|
+
# caller can SEE progress. A silent minute on a reasoning model is normal; the earlier misreading
|
|
52
|
+
# happened precisely because silence was treated as termination.
|
|
53
|
+
while kill -0 "$PID" 2>/dev/null; do
|
|
54
|
+
if [ "$waited" -ge "$BUDGET" ]; then
|
|
55
|
+
size=$(wc -c < "$OUT" 2>/dev/null | tr -d ' ')
|
|
56
|
+
echo "SIDECAR_VERDICT=TIMEOUT waited=${BUDGET}s bytes=${size:-0} pid=$PID"
|
|
57
|
+
echo " the process is STILL RUNNING — this is not 'no output'. Raise the budget, or kill $PID" >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
sleep 5
|
|
61
|
+
waited=$((waited + 5))
|
|
62
|
+
size=$(wc -c < "$OUT" 2>/dev/null | tr -d ' ')
|
|
63
|
+
if [ "${size:-0}" -ne "$last_size" ]; then
|
|
64
|
+
echo " … ${waited}s elapsed, ${size} bytes so far (alive)" >&2
|
|
65
|
+
last_size=${size:-0}
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
wait "$PID"; rc=$?
|
|
70
|
+
size=$(wc -c < "$OUT" 2>/dev/null | tr -d ' ')
|
|
71
|
+
if [ "${size:-0}" -eq 0 ]; then
|
|
72
|
+
echo "SIDECAR_VERDICT=EMPTY exit=$rc"
|
|
73
|
+
else
|
|
74
|
+
echo "SIDECAR_VERDICT=COMPLETE exit=$rc bytes=$size"
|
|
75
|
+
fi
|
|
76
|
+
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# substrate_jump_detector.sh — detect substrate-version jumps (the trigger that was a phantom).
|
|
3
|
+
#
|
|
4
|
+
# WHY: the substrate self-adaptation loop's initiate leg cited a "substrate-version jump trigger"
|
|
5
|
+
# that had NO detector (codex census 2026-07-10 refuted the self-assessment — the trigger existed
|
|
6
|
+
# only as inventory text). This is STRUCTURE-ENFORCING mechanization per the durable-mechanization
|
|
7
|
+
# criterion (sonnet_floor_doctrine.md): version drift lives OUTSIDE the session's context boundary —
|
|
8
|
+
# an infinitely strong model still cannot know what changed on the machine between sessions.
|
|
9
|
+
#
|
|
10
|
+
# WHAT: snapshots substrate versions to a gitignored state file; on the next run, diffs and emits
|
|
11
|
+
# a jump notice naming the doctrine's shed/advance pass. Silent when nothing changed.
|
|
12
|
+
# Wire: one line in the SessionStart hook (fh_session_load.sh) or run standalone.
|
|
13
|
+
#
|
|
14
|
+
# Exit: always 0 (detector, not gate). State: tracks/_meta/.substrate_versions (gitignored).
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
FH="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
19
|
+
STATE="$FH/tracks/_meta/.substrate_versions"
|
|
20
|
+
|
|
21
|
+
snapshot() {
|
|
22
|
+
# one line per component: name=version (unavailable components recorded as absent — an
|
|
23
|
+
# appearing/disappearing component is itself a jump)
|
|
24
|
+
echo "claude=$(claude --version 2>/dev/null | head -1 || echo absent)"
|
|
25
|
+
echo "codex=$(codex --version 2>/dev/null | head -1 || echo absent)"
|
|
26
|
+
echo "agy=$(agy --version 2>/dev/null | head -1 || echo absent)"
|
|
27
|
+
echo "node=$(node --version 2>/dev/null || echo absent)"
|
|
28
|
+
echo "git=$(git --version 2>/dev/null || echo absent)"
|
|
29
|
+
echo "os=$(uname -sr 2>/dev/null || echo absent)"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
CURRENT="$(snapshot)"
|
|
33
|
+
|
|
34
|
+
if [ ! -f "$STATE" ]; then
|
|
35
|
+
printf '%s\n' "$CURRENT" > "$STATE"
|
|
36
|
+
echo "🧭 [substrate] baseline snapshot recorded ($(echo "$CURRENT" | wc -l | tr -d ' ') components)"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
PREV="$(cat "$STATE")"
|
|
41
|
+
if [ "$CURRENT" = "$PREV" ]; then
|
|
42
|
+
# silent no-op — a detector that talks every session trains the reader to skip it
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
echo "🧭 [substrate] VERSION JUMP detected — substrate loop initiate leg fires:"
|
|
47
|
+
# show only changed lines (name-keyed diff, bash-3.2 safe)
|
|
48
|
+
while IFS= read -r cur; do
|
|
49
|
+
name="${cur%%=*}"
|
|
50
|
+
old=$(printf '%s\n' "$PREV" | grep -m1 "^$name=" || echo "$name=<new>")
|
|
51
|
+
[ "$cur" != "$old" ] && echo " $old → $cur"
|
|
52
|
+
done <<EOF
|
|
53
|
+
$CURRENT
|
|
54
|
+
EOF
|
|
55
|
+
echo " → run the shed/advance pass: re-check capability-compensating scaffolding against the new"
|
|
56
|
+
echo " substrate (sonnet_floor_doctrine.md §durable-mechanization — shed what the model no longer"
|
|
57
|
+
echo " needs, advance what the new substrate enables). Removals go through the 4-axis gate."
|
|
58
|
+
|
|
59
|
+
printf '%s\n' "$CURRENT" > "$STATE"
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_card_drift_probe.sh — session_close_check.sh ⑤-b 카드-드리프트 프로브의 known-pair 픽스처.
|
|
3
|
+
#
|
|
4
|
+
# WHY: 프로브 자체가 계기다 — CLAUDE.md §Instrument Calibration 이 요구하는 known-pair
|
|
5
|
+
# (양성 1 + 음성 1)를 통과하지 못하는 프로브는 배선하면 안 된다(다음 오판정의 원천이 된다).
|
|
6
|
+
# 이 파일이 그 캘리브레이션의 회귀 앵커: 프로브 정규식/토큰추출을 고칠 때마다 재실행.
|
|
7
|
+
#
|
|
8
|
+
# 픽스처 3종:
|
|
9
|
+
# P (known-positive): 카드가 🔴 "foo-digest 미가동 — 산출물 0" 주장 + 실물
|
|
10
|
+
# tracks/_meta/foo_digest_2026_07_22.md 존재 → ⑤-b 경고가 떠야 한다
|
|
11
|
+
# N1 (known-negative): 카드가 부재 주장하는 bar-report 는 진짜 없음 → 경고 0
|
|
12
|
+
# N2 (과발화 가드): 같은 실물이 있어도 부재-주장이 아닌 🟢 줄 → 경고 0
|
|
13
|
+
# (건강한 날 뜨는 경고는 무시를 학습시킨다 — 과발화도 결함)
|
|
14
|
+
#
|
|
15
|
+
# Exit 0 = 3/3 캘리브레이션 통과 · exit 1 = 프로브 계기 불량 (배선 금지)
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
CHECK="$SCRIPT_DIR/session_close_check.sh"
|
|
20
|
+
FAILED=0
|
|
21
|
+
|
|
22
|
+
run_fixture() { # $1=name $2=card-content $3=make-artifact(0/1) $4=expect-warn(0/1)
|
|
23
|
+
local name="$1" card="$2" mkart="$3" expect="$4"
|
|
24
|
+
local T; T=$(mktemp -d)
|
|
25
|
+
mkdir -p "$T/tracks/_meta/logs"
|
|
26
|
+
printf '%s\n' "$card" > "$T/tracks/_meta/reference_next_session_starter.md"
|
|
27
|
+
[ "$mkart" = 1 ] && touch "$T/tracks/_meta/foo_digest_2026_07_22.md"
|
|
28
|
+
# git 없는 디렉토리라도 다른 스텝은 조용히 지나간다(모두 2>/dev/null 가드)
|
|
29
|
+
local out; out=$(bash "$CHECK" "$T" 2>/dev/null)
|
|
30
|
+
local warned=0
|
|
31
|
+
printf '%s\n' "$out" | grep -q "⑤-b card-drift" && warned=1
|
|
32
|
+
rm -rf "$T"
|
|
33
|
+
if [ "$warned" = "$expect" ]; then
|
|
34
|
+
echo "✅ $name (warn=$warned, expected=$expect)"
|
|
35
|
+
else
|
|
36
|
+
echo "❌ $name — warn=$warned, expected=$expect"
|
|
37
|
+
FAILED=1
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
run_fixture "P known-positive: 부재주장+실물존재 → 경고" \
|
|
42
|
+
"- 🔴 **foo-digest 잡 미가동(07-22)** — 로그도 산출물도 0(launchd 확인 필요)." 1 1
|
|
43
|
+
|
|
44
|
+
run_fixture "N1 known-negative: 부재주장+진짜부재 → 무경고" \
|
|
45
|
+
"- 🔴 **bar-report 잡 미가동** — 산출물 0, 미착수." 0 0
|
|
46
|
+
|
|
47
|
+
run_fixture "N2 과발화가드: 실물존재+부재주장아님 → 무경고" \
|
|
48
|
+
"- 🟢 **foo-digest 가동 중** — 오늘자 산출 확인." 1 0
|
|
49
|
+
|
|
50
|
+
run_fixture "N3 정정문맥가드: 부재주장 인용+오판정 명시 → 무경고" \
|
|
51
|
+
"- 🔴 **foo-digest 산출 누락 수리 미완**. 기존 카드의 \"미가동\"은 오판정이었음." 1 0
|
|
52
|
+
|
|
53
|
+
run_fixture "N4 날짜토큰가드: 부재주장이나 토큰이 날짜뿐 → 무경고" \
|
|
54
|
+
"- 🔴 잡 미가동 2026-07-22 산출물 0" 1 0
|
|
55
|
+
|
|
56
|
+
# challenger A-1 반례 (2026-07-23): 살아있는 주장 + 무관한 debunk 어휘 = 경고가 떠야 한다.
|
|
57
|
+
# debunk-단독 가드는 이 두 줄을 무음 삼켰다(FN) — 인용부 조건이 판별자.
|
|
58
|
+
run_fixture "P2 살아있는 주장+무관한 '정정 필요' → 경고 (A-1 FN 앵커)" \
|
|
59
|
+
"- 🔴 **foo-digest 미가동 지속** — 산출물 0. 지난 카드의 실행횟수 수치는 정정 필요." 1 1
|
|
60
|
+
|
|
61
|
+
run_fixture "P3 살아있는 주장+무관한 '거짓' → 경고 (A-1 FN 앵커)" \
|
|
62
|
+
"- 🔴 **foo-digest 미가동** — 산출물 0, 로그는 거짓 성공만 찍힘" 1 1
|
|
63
|
+
|
|
64
|
+
run_fixture "N5 위치-언급 디렉토리 → 무경고 (A-2 FP 앵커)" \
|
|
65
|
+
"- 🟡 bar-report 미생성 — tracks/_meta/ 산출물 0" 0 0
|
|
66
|
+
|
|
67
|
+
run_fixture "N6 카드 자신 매치 제외 (A-3 FP 앵커)" \
|
|
68
|
+
"- 🔴 next_session_starter 갱신 부재 — 0건" 0 0
|
|
69
|
+
|
|
70
|
+
run_fixture "P4 영어 부재주장 (A-4 앵커)" \
|
|
71
|
+
"- 🔴 **foo-digest job not running** — zero outputs" 1 1
|
|
72
|
+
|
|
73
|
+
run_fixture "N7 인용된 부재키워드+정정 → 무경고 (실카드 07-22 클래스)" \
|
|
74
|
+
"- 🔴 foo-digest 산출 누락. 기존 카드의 \"미가동\" 주장은 오판정이었음" 1 0
|
|
75
|
+
|
|
76
|
+
echo "── card-drift probe calibration: $([ "$FAILED" -eq 0 ] && echo "PASS (전 픽스처) — 배선 가능" || echo "FAIL — 계기 불량, 배선 금지") ──"
|
|
77
|
+
exit "$FAILED"
|
|
@@ -155,6 +155,32 @@ n=$(s_hits "$TMP/dependency_guards.sh")
|
|
|
155
155
|
[ "$n" -eq 2 ] && ok "dependency guards (\`[ -f lib ] || exit 0\`) still detected — scope exclusion did not swallow them" \
|
|
156
156
|
|| bad "dependency guards: expected 2 S-hits, got $n — 'guard library missing → allow' is hidden again"
|
|
157
157
|
|
|
158
|
+
# S5 known pair — added 2026-07-28 when the probe shipped with NO fixture of its own and every one
|
|
159
|
+
# of the 9 hits it produced in this repo turned out to be a false positive. Both directions are
|
|
160
|
+
# pinned because narrowing an all-FP probe is one edit away from a blind one.
|
|
161
|
+
cat > "$TMP/s5_positive.sh" <<'EOF'
|
|
162
|
+
#!/usr/bin/env bash
|
|
163
|
+
set -uo pipefail
|
|
164
|
+
N=$(find /nope . -maxdepth 1 2>/dev/null | grep -c . || echo 0)
|
|
165
|
+
M=$(git log --oneline 2>/dev/null | wc -l || echo 0)
|
|
166
|
+
EOF
|
|
167
|
+
cat > "$TMP/s5_negative.sh" <<'EOF'
|
|
168
|
+
#!/usr/bin/env bash
|
|
169
|
+
set -uo pipefail
|
|
170
|
+
# `a || b || echo 0` is NOT a pipeline — no stage can emit a second line.
|
|
171
|
+
_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
172
|
+
# A pipeline whose failing stage emits nothing: the fallback supplies the only line, as intended.
|
|
173
|
+
J=$(printf '%s' "$x" | jq -r '.a // 0' 2>/dev/null || echo 0)
|
|
174
|
+
# A comment describing the defect must not be scored as the defect: cmd | grep -c . || echo 0
|
|
175
|
+
EOF
|
|
176
|
+
n=$(s_hits "$TMP/s5_positive.sh")
|
|
177
|
+
[ "$n" -eq 2 ] && ok "S5 known-positive: counter-stage fallbacks (grep -c, wc) detected 2/2" \
|
|
178
|
+
|| bad "S5 known-positive: expected 2 S-hits, got $n — the pipefail disarm is invisible"
|
|
179
|
+
|
|
180
|
+
n=$(s_hits "$TMP/s5_negative.sh")
|
|
181
|
+
[ "$n" -eq 0 ] && ok "S5 known-negative: \`||\` chains, empty-on-failure pipelines and comments stay silent" \
|
|
182
|
+
|| bad "S5 known-negative: $n hit(s) — S5 is noise again (9/9 FP was its measured state)"
|
|
183
|
+
|
|
158
184
|
n=$(p_hits "$TMP/kp.py")
|
|
159
185
|
[ "$n" -ge 1 ] && ok "python known-positive: pre-existing probes still fire" \
|
|
160
186
|
|| bad "python known-positive: no hits — the Python probes regressed"
|