@chrono-meta/fh-gate 1.4.71 → 1.4.73
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/rules/.public-surface-patterns.defaults +44 -0
- package/.claude/rules/fh_4axis_gate.md +207 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +26 -2
- package/CATALOG.md +59 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +10 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +554 -0
- package/package.json +21 -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/skills/context-doctor/SKILL.md +42 -4
- package/plugins/fh-meta/skills/context-doctor/SKILL_detail.md +38 -0
- package/plugins/fh-meta/skills/salience-splitter/SKILL.md +1 -1
- package/scripts/chamber_candidate_collect.sh +223 -0
- package/scripts/degrade_direction_scan.sh +222 -0
- package/scripts/fh-gate.sh +76 -2
- package/scripts/fh_session_load.sh +202 -0
- package/scripts/gate_pathspec_check.sh +166 -0
- package/scripts/prepush_guard_check.sh +374 -0
- package/scripts/psa_scan_lib.sh +153 -0
- package/scripts/public_surface_scan_files.sh +157 -0
- package/scripts/selfcheck.sh +16 -0
- package/scripts/session_close_check.sh +171 -0
- package/scripts/test_degrade_scan_shell_probes.sh +185 -0
- package/scripts/test_fh_gate_regressions.sh +46 -2
- package/scripts/test_prepush_stdin_integrity.sh +119 -0
- package/scripts/universal_guard_check.sh +280 -0
- package/templates/.claude/rules/mcp_tool_gating.md +157 -0
- package/templates/.git-hooks/pre-commit +848 -0
- package/templates/.git-hooks/pre-push +585 -0
- package/templates/PRE-PUBLISH-CHECKLIST.md +85 -0
- package/templates/degrade_direction_scan.sh +222 -0
- package/templates/predelete_check.sh +72 -0
- package/templates/regression_guard.sh +563 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# psa_scan_lib.sh — the ONE implementation of public-surface pattern loading and matching.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS FILE EXISTS
|
|
5
|
+
# Three copies of this logic existed (pre-commit, pre-push, public_surface_scan_files.sh). Across a
|
|
6
|
+
# single 2026-07-26 cross-family audit, EVERY confidentiality defect found was a divergence between
|
|
7
|
+
# them, not a flaw in the idea:
|
|
8
|
+
# • the publish copy checked that the override was readable and non-empty; the commit copy did not
|
|
9
|
+
# • the commit copy failed closed when no patterns loaded; the publish copy did too; the push copy
|
|
10
|
+
# had to be told separately
|
|
11
|
+
# • a row with a SPACE instead of a TAB failed closed in two copies and was silently skipped in the third
|
|
12
|
+
# • `head -1` per line (a placeholder shielding a real token) was fixed in one copy and not the others
|
|
13
|
+
# • the LOW-severity file allowlist existed in one copy only — which is how the push gate blocked
|
|
14
|
+
# its own first real push
|
|
15
|
+
# Each was repaired where it was found. The prescription this repo already had recorded for that
|
|
16
|
+
# pattern — [[feedback_divergent_leniency_duplicate_normalizers]]: single source, visible drops,
|
|
17
|
+
# fail-closed — was quoted during that session and then not applied; the duplication was repaired
|
|
18
|
+
# three times instead of removed. This file removes it.
|
|
19
|
+
#
|
|
20
|
+
# WHAT IS SHARED vs WHAT STAYS WITH THE CALLER — the split is deliberate, do not "unify" further:
|
|
21
|
+
# SHARED (here): loading the two pattern layers, validating rows, and deciding whether a given
|
|
22
|
+
# token on a given path is a reportable hit.
|
|
23
|
+
# CALLER'S: WHICH content is scanned (staged diff / pushed range / packed file set), and the
|
|
24
|
+
# DEGRADE DIRECTION when the instrument is incomplete. Those genuinely differ:
|
|
25
|
+
# a commit is local and re-committable, so an absent operator override warns; a push
|
|
26
|
+
# and a publish are the acts that make content public, so the same state blocks.
|
|
27
|
+
# Collapsing that would either block every fresh clone's first commit (training the
|
|
28
|
+
# override into reflex) or let a real leak reach a public remote. This library
|
|
29
|
+
# therefore REPORTS state and never exits.
|
|
30
|
+
#
|
|
31
|
+
# Usage:
|
|
32
|
+
# . "$REPO_ROOT/scripts/psa_scan_lib.sh"
|
|
33
|
+
# psa_load "$defaults_path" "$override_path" # sets PSA_STREAM + the three state flags below
|
|
34
|
+
# psa_scan_tagged <<< "$path<TAB>line"… # prints hits, returns 1 if any hit was reported
|
|
35
|
+
#
|
|
36
|
+
# State set by psa_load (read-only for callers):
|
|
37
|
+
# PSA_STREAM validated rows only (invalid/malformed rows are dropped AND counted)
|
|
38
|
+
# PSA_DEFAULTS_OK 1 = committed defaults present, readable, non-empty
|
|
39
|
+
# PSA_OVERRIDE_PRESENT 1 = operator override present, readable, non-empty
|
|
40
|
+
# PSA_BAD_ROWS count of rows dropped as unusable (uncompilable regex, or no TAB)
|
|
41
|
+
# A caller that treats PSA_BAD_ROWS>0 or PSA_DEFAULTS_OK=0 as "clean" has a hole: those states mean
|
|
42
|
+
# the instrument is incomplete, and an incomplete instrument cannot certify anything.
|
|
43
|
+
|
|
44
|
+
# Placeholder shapes that must never count as a leak. Anchored whole-token: a substring match here
|
|
45
|
+
# would let a real token pass by merely CONTAINING a placeholder word.
|
|
46
|
+
PSA_PLACEHOLDER='^(<[a-z0-9_-]+>|\{[a-z_]+\}|EXAMPLE|dummy|changeme|REDACTED|xxxx|/Users/(EXAMPLE|yourname|\{[a-z_]+\}|<[a-z0-9_-]+>)/|AKIAIOSFODNN7EXAMPLE)$'
|
|
47
|
+
|
|
48
|
+
# Files that name wiring/companion tokens as part of doing their job. LOW severity only — HIGH/MED
|
|
49
|
+
# block everywhere, including here. Kept as a function so the list has exactly one definition.
|
|
50
|
+
psa_low_allowlisted() {
|
|
51
|
+
case "$1" in
|
|
52
|
+
.gitignore|scripts/sync-to-be.sh|.claude/rules/local_fh_context.md|templates/local_fh_context.md|templates/.claude/rules/*|templates/.git-hooks/*) return 0 ;;
|
|
53
|
+
*) return 1 ;;
|
|
54
|
+
esac
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# psa_load <defaults_path> <override_path>
|
|
58
|
+
# Prints diagnostics for every degraded state; sets the flags above. NEVER exits — the caller owns
|
|
59
|
+
# the degrade direction, because it differs by surface (see the header).
|
|
60
|
+
psa_load() {
|
|
61
|
+
local defaults="$1" override="$2" raw ov
|
|
62
|
+
PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=0
|
|
63
|
+
|
|
64
|
+
# Layer 1 — committed defaults. This file ships with the repo, so missing/unreadable/empty is a
|
|
65
|
+
# BROKEN INSTRUMENT, not a configuration choice. A valid override used to mask its absence, and the
|
|
66
|
+
# scan then reported a clean "full pattern set" with every universal pattern silently gone.
|
|
67
|
+
if [ -f "$defaults" ] && [ -r "$defaults" ]; then
|
|
68
|
+
raw=$(cat "$defaults" 2>/dev/null || true)
|
|
69
|
+
if [ -n "$(printf '%s' "$raw" | grep -vE '^[[:space:]]*(#|$)' || true)" ]; then
|
|
70
|
+
PSA_STREAM="$raw"; PSA_DEFAULTS_OK=1
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
[ "$PSA_DEFAULTS_OK" -eq 0 ] && \
|
|
74
|
+
echo " ❌ committed pattern defaults missing/unreadable/empty — universal patterns NOT loaded ($defaults)"
|
|
75
|
+
|
|
76
|
+
# Layer 2 — the gitignored operator literals. Present ONLY when a readable, non-empty read actually
|
|
77
|
+
# contributed patterns: an empty file must not masquerade as a configured gate.
|
|
78
|
+
if [ -f "$override" ]; then
|
|
79
|
+
if [ -r "$override" ]; then
|
|
80
|
+
ov=$(cat "$override" 2>/dev/null || true)
|
|
81
|
+
if [ -n "$(printf '%s' "$ov" | grep -vE '^[[:space:]]*(#|$)' || true)" ]; then
|
|
82
|
+
PSA_STREAM="$PSA_STREAM
|
|
83
|
+
$ov"; PSA_OVERRIDE_PRESENT=1
|
|
84
|
+
else
|
|
85
|
+
echo " ⚠️ operator pattern override is EMPTY — treated as absent."
|
|
86
|
+
fi
|
|
87
|
+
else
|
|
88
|
+
echo " ⚠️ operator pattern override exists but is UNREADABLE — treated as absent."
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Row validation. Two ways a row can be present and still protect nothing, both previously silent:
|
|
93
|
+
# • no TAB → the whole line lands in the severity field and no detector is defined
|
|
94
|
+
# • bad ERE → grep exits >=2 and the error is swallowed downstream as a no-match
|
|
95
|
+
# Both are DROPPED and COUNTED here so the caller can fail closed on an incomplete instrument.
|
|
96
|
+
local valid="" line re rc
|
|
97
|
+
while IFS= read -r line; do
|
|
98
|
+
line="${line%$'\r'}" # CRLF: a trailing CR welds onto the regex and it matches nothing
|
|
99
|
+
case "$line" in ''|\#*) continue;; esac
|
|
100
|
+
re="${line#*$'\t'}"
|
|
101
|
+
if [ "$re" = "$line" ]; then
|
|
102
|
+
echo " ❌ unusable pattern row (no TAB between severity and regex): ${line%%$'\t'*}"
|
|
103
|
+
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
104
|
+
fi
|
|
105
|
+
printf '' | grep -E "$re" >/dev/null 2>&1; rc=$? # 0/1 = compiled; >=2 = regex error
|
|
106
|
+
if [ "$rc" -ge 2 ]; then
|
|
107
|
+
echo " ❌ unusable pattern (invalid regex) — this detector would match nothing: [${line%%$'\t'*}]"
|
|
108
|
+
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
109
|
+
fi
|
|
110
|
+
valid="$valid
|
|
111
|
+
$line"
|
|
112
|
+
done <<PSA_ROWS
|
|
113
|
+
$PSA_STREAM
|
|
114
|
+
PSA_ROWS
|
|
115
|
+
PSA_STREAM="$valid"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# psa_scan_tagged — reads "path<TAB>content" lines on stdin, prints one line per reportable hit.
|
|
119
|
+
# Returns 0 = nothing reportable, 1 = at least one hit.
|
|
120
|
+
#
|
|
121
|
+
# Why path-tagged input: the LOW allowlist is per FILE, so a scanner that flattens content loses the
|
|
122
|
+
# only information needed to apply it. The push copy did exactly that and blocked its own first real
|
|
123
|
+
# push on a companion-store name inside the script whose job is to sync to the companion store.
|
|
124
|
+
psa_scan_tagged() {
|
|
125
|
+
local input hit=0 row sev re path body tok
|
|
126
|
+
input=$(cat)
|
|
127
|
+
[ -n "$input" ] || return 0
|
|
128
|
+
while IFS= read -r row; do
|
|
129
|
+
case "$row" in ''|\#*) continue;; esac
|
|
130
|
+
re="${row#*$'\t'}"; [ "$re" = "$row" ] && continue
|
|
131
|
+
sev="${row%%$'\t'*}"
|
|
132
|
+
while IFS= read -r line; do
|
|
133
|
+
[ -z "$line" ] && continue
|
|
134
|
+
path="${line%%$'\t'*}"; body="${line#*$'\t'}"
|
|
135
|
+
# EVERY match on the line. Taking only the first let a documented placeholder earlier on the
|
|
136
|
+
# line shield a real token later on it.
|
|
137
|
+
while IFS= read -r tok; do
|
|
138
|
+
[ -z "$tok" ] && continue
|
|
139
|
+
printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
|
|
140
|
+
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$path"; then continue; fi
|
|
141
|
+
echo " ❌ $sev leak — ${path}: '$tok'"
|
|
142
|
+
hit=1
|
|
143
|
+
done <<PSA_TOK
|
|
144
|
+
$(printf '%s' "$body" | grep -oiE "$re" 2>/dev/null || true)
|
|
145
|
+
PSA_TOK
|
|
146
|
+
done <<PSA_LINES
|
|
147
|
+
$(printf '%s\n' "$input" | grep -iE "$re" 2>/dev/null || true)
|
|
148
|
+
PSA_LINES
|
|
149
|
+
done <<PSA_PAT
|
|
150
|
+
$PSA_STREAM
|
|
151
|
+
PSA_PAT
|
|
152
|
+
return $hit
|
|
153
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# public_surface_scan_files.sh — Pre-Publish confidentiality floor for `npm publish`.
|
|
3
|
+
#
|
|
4
|
+
# Mechanizes the npm-publish half of the Pre-Publish Surface Gate (CLAUDE.md). The pre-commit
|
|
5
|
+
# confidentiality scan sees only the ADDED LINES of this repo's commits; a token committed before
|
|
6
|
+
# that scan existed, or carried in a files[] entry, would otherwise reach the registry unscanned.
|
|
7
|
+
# This scans the FULL CONTENT of the exact npm-published file set (npm pack --dry-run) against the
|
|
8
|
+
# same operator-private patterns, and blocks `npm publish` on a HIGH/MED hit.
|
|
9
|
+
#
|
|
10
|
+
# Honest scope: covers `npm publish` only (wired via package.json prepublishOnly). The separate-repo
|
|
11
|
+
# go-public surface (gh repo create --public / visibility flip / first push to a new public remote) is
|
|
12
|
+
# not an npm or git op against this repo, so no hook here sees it — it stays prose + PRE-PUBLISH-CHECKLIST.md
|
|
13
|
+
# (genuinely un-hookable). Same shape as the Destructive-Op story: git/npm surface mechanized, the
|
|
14
|
+
# separate-repo go-public surface stays prose.
|
|
15
|
+
#
|
|
16
|
+
# Degrade direction: irreversible surface (publish) → fail-CLOSED. Patterns absent, or the published
|
|
17
|
+
# file set unresolved → BLOCK (proceed only on an explicit, logged PUBLIC_SURFACE_OK=1). Never silent-allow.
|
|
18
|
+
#
|
|
19
|
+
# Override (explicit, logged — mirrors the pre-commit PUBLIC_SURFACE_OK channel):
|
|
20
|
+
# PUBLIC_SURFACE_OK=1 npm publish … ← after conscious review of the flagged hit(s).
|
|
21
|
+
#
|
|
22
|
+
# Patterns are single-source (shared with the pre-commit scan): .claude/rules/.public-surface-patterns.defaults
|
|
23
|
+
# (committed, universal) + .claude/rules/.public-surface-patterns (gitignored, operator literals).
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
|
|
27
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
28
|
+
PSA_DEFAULTS="$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults"
|
|
29
|
+
PSA_OVERRIDE="${PSA_PATTERNS:-$REPO_ROOT/.claude/rules/.public-surface-patterns}"
|
|
30
|
+
# PSA_PLACEHOLDER and the default psa_low_allowlisted now come from scripts/psa_scan_lib.sh.
|
|
31
|
+
# A second copy here is exactly the divergence this refactor removed; do not reintroduce one.
|
|
32
|
+
|
|
33
|
+
# LOW-severity allowlist by file (HIGH/MED still block). For the PUBLISH scan this is nearly vacuous:
|
|
34
|
+
# a published file should carry NO operator-private token at all. Deliberately names no operator-private
|
|
35
|
+
# file literal here (the pre-commit copy of this list does, but it lives in a git-hooks-allowlisted path;
|
|
36
|
+
# THIS script is public-tracked, so hardcoding e.g. a companion-script name would itself be a LOW leak —
|
|
37
|
+
# caught by the pre-commit confidentiality scan, 2026-06-27). Generic template paths only.
|
|
38
|
+
|
|
39
|
+
echo "[Pre-Publish] public-surface scan (npm-published file content)..."
|
|
40
|
+
|
|
41
|
+
# ── Load patterns via the shared library (scripts/psa_scan_lib.sh) ──
|
|
42
|
+
# One implementation of loading + row validation + exemption decisions, shared with pre-commit and
|
|
43
|
+
# pre-push. What this file KEEPS for itself, deliberately:
|
|
44
|
+
# • the file-level `grep -a` scan. It forces every published file to be read as TEXT; the library's
|
|
45
|
+
# line-stream interface would drop back to text-only handling and re-open the hole a challenger
|
|
46
|
+
# found earlier — a token inside an SVG / null-byte file (docs/pillars.svg ships) scanning clean.
|
|
47
|
+
# • a STRICTER LOW allowlist (below). A published artifact should carry no operator-private token
|
|
48
|
+
# at all, so the commit-time list of files that legitimately name wiring tokens does not apply.
|
|
49
|
+
# Sourcing the library and then redefining the function is how that difference stays visible
|
|
50
|
+
# instead of being buried as a divergence.
|
|
51
|
+
PSA_LIB="$REPO_ROOT/scripts/psa_scan_lib.sh"
|
|
52
|
+
if [ ! -r "$PSA_LIB" ]; then
|
|
53
|
+
echo " ❌ scripts/psa_scan_lib.sh missing — the confidentiality scanner cannot run."
|
|
54
|
+
[ "${PUBLIC_SURFACE_OK:-0}" = "1" ] || { echo " Fail-closed on the publish boundary."; exit 1; }
|
|
55
|
+
else
|
|
56
|
+
. "$PSA_LIB"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Stricter than the library default — see the note above. Named here so the difference is legible.
|
|
60
|
+
psa_low_allowlisted() {
|
|
61
|
+
case "$1" in
|
|
62
|
+
templates/*) return 0 ;;
|
|
63
|
+
*) return 1 ;;
|
|
64
|
+
esac
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
psa_load "$PSA_DEFAULTS" "$PSA_OVERRIDE"
|
|
68
|
+
|
|
69
|
+
# Publish is an irreversible surface: EVERY incomplete-instrument state blocks, including the merely
|
|
70
|
+
# absent operator override (which only warns at commit time — see the library header for why the two
|
|
71
|
+
# surfaces degrade differently).
|
|
72
|
+
_psa_why=""
|
|
73
|
+
[ "$PSA_DEFAULTS_OK" -eq 0 ] && _psa_why="committed pattern defaults missing/unreadable/empty"
|
|
74
|
+
[ "$PSA_BAD_ROWS" -gt 0 ] && _psa_why="${_psa_why:+$_psa_why; }$PSA_BAD_ROWS unusable pattern row(s)"
|
|
75
|
+
[ "$PSA_OVERRIDE_PRESENT" -eq 0 ] && _psa_why="${_psa_why:+$_psa_why; }operator-literal override absent/empty (HIGH company/companion literals NOT scanned)"
|
|
76
|
+
if [ -n "$_psa_why" ]; then
|
|
77
|
+
echo " ❌ incomplete confidentiality instrument — $_psa_why"
|
|
78
|
+
if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
|
|
79
|
+
echo " ⚠️ proceeding by PUBLIC_SURFACE_OK=1 (conscious — the scan is incomplete)"
|
|
80
|
+
echo "$(date +%Y-%m-%dT%H:%M:%S) PUBLIC_SURFACE_OK override (npm publish, incomplete instrument)" \
|
|
81
|
+
>> "$REPO_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
|
|
82
|
+
else
|
|
83
|
+
echo " Fail-closed on an irreversible surface: an incomplete instrument cannot certify clean."
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Named residual (cross-family audit 2026-06-27, deferred): this scans the WORKING-TREE content of the
|
|
90
|
+
# packed file list, not the final tarball bytes. A content-generating publish lifecycle (prepack/prepare
|
|
91
|
+
# that writes files AFTER this prepublishOnly scan) could ship bytes this never saw, and a path containing
|
|
92
|
+
# a newline would mis-split the list. The robust fix is to scan the actual `npm pack` tarball; deferred
|
|
93
|
+
# because THIS package's lifecycle is content-neutral (prepare = chmod only, no prepack). Re-open if a
|
|
94
|
+
# content-generating lifecycle is ever added.
|
|
95
|
+
# ── Resolve the exact npm-published file set (fail-closed if unresolved OR partial) ──
|
|
96
|
+
FILES=$(npm pack --dry-run --json 2>/dev/null \
|
|
97
|
+
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{JSON.parse(s)[0].files.forEach(f=>console.log(f.path))}catch(e){process.exit(3)}})' 2>/dev/null || true)
|
|
98
|
+
if [ -z "$FILES" ]; then
|
|
99
|
+
echo " ❌ could not resolve the npm-published file set (npm pack --dry-run failed)."
|
|
100
|
+
[ "${PUBLIC_SURFACE_OK:-0}" = "1" ] && { echo " ⚠️ proceeding by PUBLIC_SURFACE_OK=1"; exit 0; }
|
|
101
|
+
echo " Fail-closed on an irreversible surface. Fix npm pack or override with PUBLIC_SURFACE_OK=1."
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Wrong-set guard (challenger M6): a future npm --json shape change could yield a NON-empty but PARTIAL
|
|
106
|
+
# file list (forEach iterates a renamed/nested structure without throwing) → files silently unscanned.
|
|
107
|
+
# npm always ships package.json in the tarball; its absence means the parse got a wrong set → fail-closed.
|
|
108
|
+
if ! printf '%s\n' "$FILES" | grep -qx "package.json"; then
|
|
109
|
+
echo " ❌ published file set looks wrong — 'package.json' (always shipped) is absent from the parse."
|
|
110
|
+
[ "${PUBLIC_SURFACE_OK:-0}" = "1" ] && { echo " ⚠️ proceeding by PUBLIC_SURFACE_OK=1"; exit 0; }
|
|
111
|
+
echo " Fail-closed (possible npm --json shape change). Verify npm pack output or PUBLIC_SURFACE_OK=1."
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# ── Scan full content of each published file ──
|
|
116
|
+
LEAK=0
|
|
117
|
+
while IFS= read -r f; do
|
|
118
|
+
[ -z "$f" ] && continue
|
|
119
|
+
path="$REPO_ROOT/$f"
|
|
120
|
+
[ -f "$path" ] || continue
|
|
121
|
+
while IFS=$'\t' read -r sev regex; do
|
|
122
|
+
[ -z "$regex" ] && continue
|
|
123
|
+
case "$sev" in \#*) continue;; esac
|
|
124
|
+
while IFS= read -r tok; do
|
|
125
|
+
[ -z "$tok" ] && continue
|
|
126
|
+
printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
|
|
127
|
+
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$f"; then continue; fi
|
|
128
|
+
echo " ❌ $sev leak — $f: '$tok' would ship to the registry"
|
|
129
|
+
LEAK=1
|
|
130
|
+
# -a forces every published file to scan as TEXT (challenger S1): -I skipped binary-classified
|
|
131
|
+
# files, so a token in an SVG / null-byte file (docs/pillars.svg ships) would slip unscanned.
|
|
132
|
+
done <<< "$(grep -aoiE "$regex" "$path" 2>/dev/null | sort -u || true)"
|
|
133
|
+
done <<< "$PSA_STREAM"
|
|
134
|
+
done <<< "$FILES"
|
|
135
|
+
|
|
136
|
+
if [ "$LEAK" -eq 1 ]; then
|
|
137
|
+
if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
|
|
138
|
+
echo " ⚠️ public-surface hit(s) allowed by PUBLIC_SURFACE_OK=1 (conscious, reviewed intent)"
|
|
139
|
+
echo "$(date +%Y-%m-%dT%H:%M:%S) PUBLIC_SURFACE_OK override (npm publish) — suppressed hit(s) above" \
|
|
140
|
+
>> "$REPO_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
echo " An operator-private token would ship in the npm package. Generalize it (real home path → '~'"
|
|
144
|
+
echo " or '{project}'; companion/corp name → a generic phrase), or PUBLIC_SURFACE_OK=1 npm publish …"
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Renamed with the refactor: the flag is PSA_OVERRIDE_PRESENT, set by psa_load. The bare name was a
|
|
149
|
+
# leftover that referenced nothing under `set -u` and aborted the scan at its final line.
|
|
150
|
+
if [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
|
|
151
|
+
echo " ⚠️ PASS (DEFAULTS-ONLY) — no home-path leak, but HIGH operator literals were NOT scanned (override absent)."
|
|
152
|
+
else
|
|
153
|
+
echo " ✅ PASS — no operator-private token in the published file set (full pattern set)."
|
|
154
|
+
fi
|
|
155
|
+
# Residual (named, not closed): the scan is a DENYLIST against loaded patterns — an un-patterned secret
|
|
156
|
+
# shape (an API key the patterns don't describe) still ships. This gate is not a general secret-detector.
|
|
157
|
+
exit 0
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -83,6 +83,22 @@ else
|
|
|
83
83
|
fail=1
|
|
84
84
|
fi
|
|
85
85
|
|
|
86
|
+
# degrade-scan shell probes — the anchor was written 2026-07-28 and shipped with ZERO callers,
|
|
87
|
+
# reproducing [[feedback_built_but_not_wired]] in the same session that cited it. The subject
|
|
88
|
+
# (scripts/degrade_direction_scan.sh) and the anchor both ship, so this runs in package mode too;
|
|
89
|
+
# only a missing SUBJECT is a legitimate skip.
|
|
90
|
+
if [ ! -f scripts/degrade_direction_scan.sh ]; then
|
|
91
|
+
echo "SKIP degrade-scan shell probes (subject scripts/degrade_direction_scan.sh absent)"
|
|
92
|
+
elif [ -f scripts/test_degrade_scan_shell_probes.sh ]; then
|
|
93
|
+
if ! bash scripts/test_degrade_scan_shell_probes.sh; then
|
|
94
|
+
fail=1
|
|
95
|
+
fi
|
|
96
|
+
else
|
|
97
|
+
# subject present, anchor gone => the calibration was deleted. Real failure, not a skip.
|
|
98
|
+
echo "FAIL degrade-scan shell probes: scan present but scripts/test_degrade_scan_shell_probes.sh missing"
|
|
99
|
+
fail=1
|
|
100
|
+
fi
|
|
101
|
+
|
|
86
102
|
# Referenced-path existence is a source-tree check. The npm package intentionally
|
|
87
103
|
# ships a narrower runtime surface, so package-mode selfcheck skips this section.
|
|
88
104
|
if [ -d ".claude/rules" ]; then
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session_close_check.sh — mechanical checklist for the session close chain (CLAUDE.md §Session Wrap-up ①–⑥).
|
|
3
|
+
#
|
|
4
|
+
# WHY (loop_engineering.md census, 2026-07-10): the close chain's complete/persist legs were PROSE —
|
|
5
|
+
# card-last ordering and step coverage lived on salience alone, and the measured misses (card
|
|
6
|
+
# staleness class) all landed on exactly these legs. This script is the MECH floor: it VERIFIES
|
|
7
|
+
# state, it does not perform the steps (the session still runs them; the script catches skips).
|
|
8
|
+
# Built on operator instruction 2026-07-10 (strengthen-the-weak pass; evidence-threshold override
|
|
9
|
+
# recorded — the miss class was already measured, only the build trigger was overridden).
|
|
10
|
+
#
|
|
11
|
+
# READ-ONLY. Exit 0 = close state consistent · exit 1 = a close invariant is violated (card-last
|
|
12
|
+
# broken, or a required artifact missing). Advisory lines are prefixed ⚠️ , violations ❌ .
|
|
13
|
+
#
|
|
14
|
+
# Usage: bash scripts/session_close_check.sh [repo_root] (run at close time, before ⑥ push)
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
FH="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
19
|
+
TODAY=$(date +%Y-%m-%d)
|
|
20
|
+
CARD="$FH/tracks/_meta/reference_next_session_starter.md"
|
|
21
|
+
FAIL=0
|
|
22
|
+
|
|
23
|
+
_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
24
|
+
|
|
25
|
+
echo "── session close check: $FH ($TODAY) ──"
|
|
26
|
+
|
|
27
|
+
# ① status snapshot — uncommitted / unpushed work must be known, not forgotten
|
|
28
|
+
DIRTY=$(git -C "$FH" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
29
|
+
UNPUSHED=$(git -C "$FH" log --oneline @{u}.. 2>/dev/null | wc -l | tr -d ' ')
|
|
30
|
+
[ "$DIRTY" -gt 0 ] && echo "⚠️ ① $DIRTY uncommitted path(s) — decide: commit or leave deliberately"
|
|
31
|
+
[ "$UNPUSHED" -gt 0 ] && echo "⚠️ ① $UNPUSHED unpushed commit(s) — push before close or record why"
|
|
32
|
+
[ "$DIRTY" -eq 0 ] && [ "$UNPUSHED" -eq 0 ] && echo "✅ ① working tree clean, nothing unpushed"
|
|
33
|
+
|
|
34
|
+
# ①-b open-PR sweep (surface-not-auto — requires gh; skip silently offline)
|
|
35
|
+
if command -v gh >/dev/null 2>&1; then
|
|
36
|
+
PRS=$(gh pr list --author "@me" --state open --json number 2>/dev/null | grep -c '"number"' || true)
|
|
37
|
+
[ "${PRS:-0}" -gt 0 ] && echo "⚠️ ①-b $PRS open PR(s) by you — classify: self-mergeable vs awaiting-external"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# ② FH assets changed today → harvest-loop owed
|
|
41
|
+
# NOTE: no `grep -q` here — under `set -o pipefail`, -q's early exit SIGPIPEs git log (141),
|
|
42
|
+
# masking a real match as pipeline failure so the warning NEVER fired on true positives
|
|
43
|
+
# (caught by a Sonnet blind probe 2026-07-10, 5/5 deterministic repro). `grep -c` reads the
|
|
44
|
+
# whole stream; `|| true` guards its exit-1-on-zero.
|
|
45
|
+
FH_CHANGED=$(git -C "$FH" log --since="today 00:00" --name-only --pretty=format: 2>/dev/null \
|
|
46
|
+
| grep -cE '^(plugins/.*SKILL\.md|\.claude/rules/|templates/|CLAUDE\.md|knowledge/)' || true)
|
|
47
|
+
if [ "${FH_CHANGED:-0}" -gt 0 ]; then
|
|
48
|
+
echo "⚠️ ② FH assets changed today ($FH_CHANGED path-touch(es)) — harvest-loop (or an explicit skip note) is owed"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ④ real-time completion log — required whenever any commit landed today
|
|
52
|
+
COMMITS_TODAY=$(git -C "$FH" log --since="today 00:00" --oneline 2>/dev/null | wc -l | tr -d ' ')
|
|
53
|
+
FC="$FH/tracks/_meta/fh_completed_${TODAY}.md"
|
|
54
|
+
if [ "$COMMITS_TODAY" -gt 0 ] && [ ! -f "$FC" ]; then
|
|
55
|
+
echo "❌ ④ commits landed today but tracks/_meta/fh_completed_${TODAY}.md is missing"
|
|
56
|
+
FAIL=1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ④-b npm freshness — files[] assets changed since last version tag → republish owed.
|
|
60
|
+
# Patterns are NARROWED to the actually-shipped subpaths (package.json files[]): knowledge/ ships only
|
|
61
|
+
# shared/{harness-core,dialogue,rules}; docs/ ships only {codex-compat,CONTRIBUTING,pillars}. A broad
|
|
62
|
+
# ^knowledge/ / ^docs/ over-matched git-tracked-but-UNshipped files (e.g. knowledge/shared/learnings/
|
|
63
|
+
# subagent_invocations_log.yaml, which changes almost every self-dev session) → guaranteed per-session
|
|
64
|
+
# false positive that trains the runner to ignore the line (Axis-2 challenger catch 2026-07-13).
|
|
65
|
+
SHIP_RE='^(plugins/|knowledge/shared/(harness-core|dialogue|rules)/|docs/(codex-compat|CONTRIBUTING|pillars)|README|AGENTS\.md|CLAUDE\.md|CHEATSHEET|CATALOG\.md)'
|
|
66
|
+
LAST_TAG=$(git -C "$FH" describe --tags --abbrev=0 2>/dev/null || true)
|
|
67
|
+
if [ -n "$LAST_TAG" ] && [ -f "$FH/package.json" ]; then
|
|
68
|
+
CHANGED_SINCE_TAG=$(git -C "$FH" diff --name-only "$LAST_TAG"..HEAD 2>/dev/null)
|
|
69
|
+
if printf '%s\n' "$CHANGED_SINCE_TAG" | grep -qE "$SHIP_RE"; then
|
|
70
|
+
echo "⚠️ ④-b npm-shipped assets changed since $LAST_TAG — propose lockstep republish (never auto)"
|
|
71
|
+
fi
|
|
72
|
+
# ④-b-drift: auto-FIRE a drift-CANDIDATE reminder (not a parity verdict) — **in BOTH directions**.
|
|
73
|
+
# The two entry points are read by DIFFERENT runtimes (CLAUDE.md → Claude Code · AGENTS.md/codex-compat
|
|
74
|
+
# → Codex/OpenCode and other non-CC runtimes). A rule that lands in only one is INVISIBLE to the other,
|
|
75
|
+
# so either file changing alone is a candidate — not just the CC→Codex direction.
|
|
76
|
+
# HONEST SCOPE: this tests file co-occurrence, not topical parity — false-positive (a genuinely
|
|
77
|
+
# runtime-specific change needs no mirror) and false-negative (file touched for an unrelated reason)
|
|
78
|
+
# are both possible. The reminder is mechanized; the drift DETERMINATION stays judged (sync, or record
|
|
79
|
+
# drift:none).
|
|
80
|
+
# Origin (2026-07-19): the REVERSE direction was unwired and a real miss slipped through — a field
|
|
81
|
+
# harness's boundary-crossing behavior rules landed in AGENTS.md only, leaving Claude Code sessions
|
|
82
|
+
# unaware of a rule whose violation destroys a downstream harness's identity. Half a check caught none
|
|
83
|
+
# of it, because the miss happened to travel the unwired way.
|
|
84
|
+
_ENTRY_CC='^(CLAUDE\.md|knowledge/shared/(harness-core|dialogue|rules)/)'
|
|
85
|
+
_ENTRY_CX='^(AGENTS\.md|docs/codex-compat)'
|
|
86
|
+
if printf '%s\n' "$CHANGED_SINCE_TAG" | grep -qE "$_ENTRY_CC" \
|
|
87
|
+
&& ! printf '%s\n' "$CHANGED_SINCE_TAG" | grep -qE "$_ENTRY_CX"; then
|
|
88
|
+
echo "⚠️ ④-b drift candidate (CC→Codex): shipped CLAUDE.md/knowledge changed but AGENTS.md/docs/codex-compat did not — JUDGE entry-point parity (sync, or record drift:none if genuinely unaffected)"
|
|
89
|
+
fi
|
|
90
|
+
if printf '%s\n' "$CHANGED_SINCE_TAG" | grep -qE "$_ENTRY_CX" \
|
|
91
|
+
&& ! printf '%s\n' "$CHANGED_SINCE_TAG" | grep -qE "$_ENTRY_CC"; then
|
|
92
|
+
echo "⚠️ ④-b drift candidate (Codex→CC): AGENTS.md/docs/codex-compat changed but CLAUDE.md/knowledge did not — JUDGE entry-point parity (a rule living only in AGENTS.md is invisible to Claude Code sessions)"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ⑤ CARD-LAST invariant — the card must be the NEWEST close artifact. A card older than
|
|
97
|
+
# fh_completed / signal files written this session = ⑤ ran before ①–④ finished (the bug class).
|
|
98
|
+
if [ -f "$CARD" ]; then
|
|
99
|
+
CARD_E=$(_mtime "$CARD")
|
|
100
|
+
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
|
+
if [ "$NEWER" -gt 0 ]; then
|
|
102
|
+
echo "❌ ⑤ card-last violated — $NEWER close artifact(s) newer than the session card; re-run ⑤ (delta update)"
|
|
103
|
+
FAIL=1
|
|
104
|
+
else
|
|
105
|
+
echo "✅ ⑤ card is the newest close artifact (card-last holds)"
|
|
106
|
+
fi
|
|
107
|
+
else
|
|
108
|
+
echo "❌ ⑤ session card missing: $CARD"
|
|
109
|
+
FAIL=1
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# ⑤-b card-drift probe — 카드의 "부재 주장"을 실물과 대조 (advisory, never FAIL).
|
|
113
|
+
# WHY (N=3, 2026-07-22 주간감사 🟥1): 카드 🔴 "frontier-digest 미가동 — 로그도 산출물도 0"이
|
|
114
|
+
# 오판정이었다(실측 launchd 14/14 발화·산출 12/14). 07-20 S-4 재발에 이어 3회째 →
|
|
115
|
+
# operations.md §Recurrence escalation: 습관 규칙이 아니라 기계 프로브.
|
|
116
|
+
# HONEST SCOPE: 카드 🔴/🟡 줄에서 부재-주장 키워드를 잡고, 그 줄의 이름/경로 토큰으로
|
|
117
|
+
# 실물을 글롭 검색한다. 어휘가 안 겹치면 못 잡는다(무음 FN) — 앵커지 floor 가 아니다.
|
|
118
|
+
# 방향은 advisory: 가역 표면에서 하드 블록은 --no-verify 를 학습시킨다(#165 HIGH-1 동일 원리).
|
|
119
|
+
_ABSENCE_RE='미가동|산출물[^가-힣]*0|로그[^가-힣]*0|0건|부재|안 돌|미생성|not running|no output|zero output'
|
|
120
|
+
# 부정/정정 문맥 가드 — 부재-주장을 **인용하며** 정정하는 줄만 건너뛴다. 판별자는 debunk
|
|
121
|
+
# 어휘 단독이 아니라 **부재-키워드가 인용부호 안에 있는가** — challenger A-1 실측: 살아있는
|
|
122
|
+
# 주장 + 무관한 '정정 필요' 가 같은 줄이면 debunk-단독 가드가 진짜 경고를 무음 삼켰다(FN).
|
|
123
|
+
_DEBUNK_RE='오판정|정정|거짓|아니었|반증'
|
|
124
|
+
_QUOTED_ABSENCE_RE='["“”'"'"'『][^"“”'"'"'』]*(미가동|부재|미생성)'
|
|
125
|
+
if [ -f "$CARD" ]; then
|
|
126
|
+
DRIFT_HITS=0
|
|
127
|
+
while IFS= read -r line; do
|
|
128
|
+
# 부재-주장 줄에서 검증 가능한 토큰 2계층 추출:
|
|
129
|
+
# (a) 명시 경로/글롭 (슬래시나 * 포함) — 그대로 글롭 확장
|
|
130
|
+
# (b) 이름 토큰 (하이픈/언더스코어 포함 ≥6자, e.g. frontier-digest) — -/_ 정규화 후
|
|
131
|
+
# tracks/_meta{,/logs} 파일명 부분일치 검색
|
|
132
|
+
# `*` 는 추출 클래스에서 제외 — 마크다운 강조(**tok**)가 토큰에 붙어 글롭 분기로
|
|
133
|
+
# 오폭한다(known-pair P 픽스처가 잡은 계기 불량, 2026-07-23). 경로 판정은 슬래시로만.
|
|
134
|
+
tokens=$(printf '%s\n' "$line" | grep -oE '[A-Za-z0-9_./-]+' \
|
|
135
|
+
| sed 's/^[.-]*//; s/[.-]*$//' \
|
|
136
|
+
| grep -E '(/|[A-Za-z0-9]+[-_][A-Za-z0-9]+)' | grep -E '.{6,}' \
|
|
137
|
+
| grep -vE '^[0-9._-]+$' | sort -u) # 날짜/숫자 토큰 제외 (실카드 FP)
|
|
138
|
+
[ -z "$tokens" ] && continue
|
|
139
|
+
while IFS= read -r tok; do
|
|
140
|
+
found=0
|
|
141
|
+
case "$tok" in
|
|
142
|
+
*/*) # (a) 경로 — **파일만** 인정(-f). 디렉토리를 세면 카드의 위치-언급
|
|
143
|
+
# (tracks/_meta/ 등)이 "실물"로 잡힌다(challenger A-2 FP). 인프라 루트 제외.
|
|
144
|
+
case "$tok" in tracks/_meta|tracks/_meta/|tracks/_meta/logs|tracks/_meta/logs/) continue ;; esac
|
|
145
|
+
# shellcheck disable=SC2086
|
|
146
|
+
for f in $FH/$tok $FH/${tok}*; do [ -f "$f" ] && found=$((found+1)); done ;;
|
|
147
|
+
*) # (b) 이름 토큰 — 양쪽 구분자 변형으로 검색
|
|
148
|
+
norm_u=$(printf '%s' "$tok" | tr '-' '_'); norm_h=$(printf '%s' "$tok" | tr '_' '-')
|
|
149
|
+
found=$(find "$FH/tracks/_meta" -maxdepth 2 -type f ! -name "$(basename "$CARD")" \( -name "*${norm_u}*" -o -name "*${norm_h}*" \) 2>/dev/null | wc -l | tr -d ' ') ;;
|
|
150
|
+
esac
|
|
151
|
+
if [ "${found:-0}" -gt 0 ]; then
|
|
152
|
+
echo "⚠️ ⑤-b card-drift: 카드가 부재를 주장하는데 실물이 있다 — 토큰 '$tok' 매치 ${found}건. 줄: $(printf '%s' "$line" | cut -c1-80)…"
|
|
153
|
+
echo " → 주장을 손검증하라 (미가동≠산출누락 — 07-22 오판정 클래스). advisory, 차단 아님."
|
|
154
|
+
DRIFT_HITS=$((DRIFT_HITS+1)); break
|
|
155
|
+
fi
|
|
156
|
+
done <<CARD_TOK_EOF
|
|
157
|
+
$tokens
|
|
158
|
+
CARD_TOK_EOF
|
|
159
|
+
done <<CARD_LINE_EOF
|
|
160
|
+
$(grep -E '(🔴|🟡)' "$CARD" 2>/dev/null | grep -E "$_ABSENCE_RE" | grep -v '^description:' \
|
|
161
|
+
| while IFS= read -r _l; do
|
|
162
|
+
if printf '%s\n' "$_l" | grep -qE "$_DEBUNK_RE" \
|
|
163
|
+
&& printf '%s\n' "$_l" | grep -qE "$_QUOTED_ABSENCE_RE"; then continue; fi
|
|
164
|
+
printf '%s\n' "$_l"
|
|
165
|
+
done || true)
|
|
166
|
+
CARD_LINE_EOF
|
|
167
|
+
[ "$DRIFT_HITS" -eq 0 ] && echo "✅ ⑤-b no card absence-claim contradicted by on-disk artifacts"
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
echo "── close check: $([ "$FAIL" -eq 0 ] && echo CONSISTENT || echo VIOLATIONS) ──"
|
|
171
|
+
exit "$FAIL"
|