@chrono-meta/fh-gate 1.4.88 → 1.4.89
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/CLAUDE.md +36 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +37 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +73 -0
- package/package.json +4 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/prepush_guard_check.sh +15 -0
- package/scripts/psa_scan_lib.sh +36 -0
- package/scripts/selfcheck.sh +138 -17
- package/scripts/session_close_check.sh +55 -5
- package/scripts/test_selfcheck_state_lanes.sh +103 -0
- package/scripts/test_session_close_chain_lanes.sh +79 -4
- package/scripts/test_version_lockstep_lanes.sh +82 -0
- package/scripts/universal_guard_check.sh +19 -6
- package/scripts/version_lockstep_check.sh +80 -0
- package/templates/.git-hooks/pre-commit +8 -0
- package/templates/.git-hooks/pre-push +14 -2
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "fh-meta",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.89",
|
|
15
15
|
"description": "Hub meta-operations toolkit — 35 skills + 7 agents. New in 1.4.53: `fh-codex-doctor` (npm bin) — Codex adapter drift scanner; reads the documented M1/M2/M3 skill tier map + skill/agent source and reports codex-native/adapter-required/claude-native/unclassified per unit, wired into `npm test`/`prepublishOnly` (fail-closed on unclassified Claude-native primitives). New in 1.4.49: steel-quench gains Step 0.6 Verdict-Invariance Probe (groundedness axis — a load-bearing judged gate's verdict must track behavior, not rubric phrasing; measured flip-count over cross-family paraphrases; arXiv:2605.06161 Policy Invariance anchor); multi_model_sidecar_strategy §Vendor-native harness (a model is strongest in its own vendor CLI — Claude/CC, GPT/codex, Gemini/Antigravity; a universal router degrades all of them, so it stays an autocomplete/QA sidecar, never orchestration); predelete_check.sh fail-closed rewrite; memory-hygiene A-TMA anchor. New in 1.4.48: phantom-quench + steel-quench gain external frontier anchors (arXiv:2607.02052 package-hallucination; arXiv:2607.02057 prompt-coverage-adequacy); README model-flat claim reframed from a per-release point-curve to structural invariants (operation flattens across tiers; depth tier-order fixed within a generation). New in 1.4.47: onboarding step ① surfaces the Mode D companion-store session-start load in the auto-read salience anchor (previously only in the local binding + rules, so a greeting could skip the load). New in 1.4.46: context-doctor command-output axis (route to rtk/proxy for verbose CLI stdout, complementing .claudeignore; risk-gated to token-scarce envs). New in 1.4.41: context-doctor 2026 trigger vocab (context engineering/rot/collapse) + phantom-citation hardening; hub measurement-integrity-checklist (cross-model measurement pre-flight: display-name pin/reps≥3/discriminating probe). New in 1.4.40: install-wizard queryable-wiki scaffold (INDEX + session-start read + R/W/C ingest). New in 1.4.39: auto-decorrelation (cross-family verifier sidecar recruitment) + video-ingest (capability-routed video ingestion). New in 1.4.x: verify-axis check-class taxonomy (mandatory-pass/measured/judged), no-reinvention Tier-0 inventory, 7-class failure taxonomy, Destructive-Op Gate, Wave-T (Temper), tier-floor governance, Mode D Model Notice, FC consent lane, default-Sonnet guidance. New in 1.3.0: public-surface-audit, field-harvest Mode B auto-trigger, 4-axis gate scope ext. Validated cross-CLI: Claude Code, Codex, Gemini.",
|
|
16
16
|
"source": "./plugins/fh-meta"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fh-commons",
|
|
20
|
-
"version": "1.4.
|
|
20
|
+
"version": "1.4.89",
|
|
21
21
|
"description": "Project-agnostic utility skills — 4 skills (convergence-loop · deliberation · mcp-circuit-breaker · token-budget-gate) + 1 agent (quench-challenger). Domain-independent utilities transplantable into any project.",
|
|
22
22
|
"source": "./plugins/fh-commons"
|
|
23
23
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -602,6 +602,42 @@ Default operation is a **standard interactive session**. Agent dispatch (single
|
|
|
602
602
|
|
|
603
603
|
**Why not Agent View by default**: Agent View introduces worktree isolation (blocks settings.json writes, Stop hook timing differs), session context gaps (session card stale content bug), and path friction — with no benefit unless the user is actively managing multiple agent sessions. Parallel agents via `Agent` tool work identically in a standard session.
|
|
604
604
|
|
|
605
|
+
**Fourth reason — gate-integrity in a worktree, and the answer is CONDITIONAL on how `core.hooksPath`
|
|
606
|
+
was set (measured 2026-08-05, both arms).** Do not carry a single verdict here; the two installs
|
|
607
|
+
behave differently:
|
|
608
|
+
|
|
609
|
+
| `core.hooksPath` | Which hook actually runs in a worktree | Consequence |
|
|
610
|
+
|---|---|---|
|
|
611
|
+
| **relative** — `templates/.git-hooks`, the form every FH doc installs (`CHEATSHEET.md`, `.claude/rules/fh_4axis_gate.md`, `install-wizard`, `self_evolution_routine.md`) | the **worktree's own copy** | Editing that copy *inside the worktree* disables the gate for that worktree — measured: neutralized hook → FH-asset commit with no marker succeeded (`rc=0`). The verifier becomes the verified, and the edit is invisible to `git status` in the main tree. |
|
|
612
|
+
| **absolute** — a hand-set full path (this operator's machine; **not** what any doc tells you to run) | the **main tree's copy** | A worktree-local edit has no effect; a known-positive is blocked there exactly as in the main tree (`rc=1`). |
|
|
613
|
+
|
|
614
|
+
An earlier draft of this section reported only the absolute-path arm and declared the
|
|
615
|
+
"worktree bypasses the gates" hypothesis *refuted* — from **n=1 on a non-canonical setting**, with a
|
|
616
|
+
do-not-revisit label attached. The relative-path arm, which is what everyone else runs, reproduces
|
|
617
|
+
the bypass. Freezing a conclusion is a defect when the measurement did not cover the shipped
|
|
618
|
+
configuration.
|
|
619
|
+
|
|
620
|
+
Separately and in **both** arms, the **evidence side** breaks: `tracks/` is gitignored, so it does not
|
|
621
|
+
follow into a worktree, so the Axis 2+3 marker and the Axis 4 `edit_manifest.yaml` are *structurally
|
|
622
|
+
absent* — an FH-asset commit in a worktree fails on evidence it has no way to have. That degrades
|
|
623
|
+
fail-closed (correct), but a gate that **cannot** be satisfied is what trains the bypass. Note the
|
|
624
|
+
hook itself prints `mkdir -p …/tracks/_meta` on that failure, i.e. the actor's own error message
|
|
625
|
+
teaches the marker-creation path — so "just don't fabricate it" is prose sitting under a machine
|
|
626
|
+
instruction pointing the other way.
|
|
627
|
+
|
|
628
|
+
**Therefore: do not commit FH assets from a worktree.** Not "carry the evidence in carefully" — a
|
|
629
|
+
carried marker and a fabricated one are byte-identical, so *marker provenance* is unenforceable by
|
|
630
|
+
construction. Land FH-asset changes from the standard session.
|
|
631
|
+
|
|
632
|
+
**Do not let that unenforceability launder the enforceable part** (caught by an adversarial round on
|
|
633
|
+
the paragraph above, which had used it to do exactly that): *being in a worktree* is trivially
|
|
634
|
+
detectable — `git rev-parse --git-common-dir` differs from `--git-dir` there and matches in the main
|
|
635
|
+
tree — and `templates/.git-hooks/pre-commit` currently has **zero** lines of worktree detection. A
|
|
636
|
+
true statement about one thing (provenance) was standing in for an untested claim about another
|
|
637
|
+
(location). It is left un-mechanized for a *scope* reason, not an impossibility one: measured
|
|
638
|
+
recurrence is 1, below this repo's own N≥3 mechanization threshold. If it recurs, the check is a
|
|
639
|
+
two-line hook addition, not a research problem.
|
|
640
|
+
|
|
605
641
|
**Forbidden responses**: *"I can't do that — I'm not in that project's cwd"* — self-check Agent dispatch first.
|
|
606
642
|
|
|
607
643
|
Mapped paths: check `auto_project_mapping.md` or `find ~/projects -maxdepth 1 -type d` for actuals.
|
|
@@ -196,6 +196,13 @@ destroys live state without anyone noticing. This is why the loss class is calle
|
|
|
196
196
|
plaintext only (encoded tokens out of scope); a line-split backstop catches a token wrapped across
|
|
197
197
|
lines; `PUBLIC_SURFACE_OK=1` overrides and is logged to a gitignored audit trail for the weekly audit.
|
|
198
198
|
Residuals (split-encoding, override-not-populated, override abuse) are documented, not silent.
|
|
199
|
+
**Verdict labelling (2026-08-06)**: with the override absent, a clean scan reports
|
|
200
|
+
`⚠️ PARTIAL — company/companion literals UNMEASURED`, **never `✅ PASS`**. The commit still
|
|
201
|
+
proceeds (reversible surface → advisory degrade), but a run whose operator-literal layer never
|
|
202
|
+
executed may not present the same verdict as one where it did — a missing measurement is not a
|
|
203
|
+
zero. Anchored in `universal_guard_check.sh` as a **pair**: absent override must say `PARTIAL`,
|
|
204
|
+
and the control (override present, no hit) must still say `✅ PASS`, so the label cannot drift
|
|
205
|
+
back to a bare PASS *or* become an unconditional warning.
|
|
199
206
|
- **(c) `npm publish`** — mechanically gated by `scripts/public_surface_scan_files.sh`, wired into
|
|
200
207
|
`prepublishOnly` (`npm run release` also runs it *outside* the lifecycle). Unlike (b) it scans the
|
|
201
208
|
**full content of the exact npm-published file set** (`npm pack --dry-run`), *not* a commit diff — so a
|
|
@@ -204,6 +211,36 @@ destroys live state without anyone noticing. This is why the loss class is calle
|
|
|
204
211
|
or the file set are unresolved, when the parse looks partial, **or when the gitignored operator override
|
|
205
212
|
is absent** — defaults-only would otherwise green-PASS a HIGH company literal on a fresh clone or CI runner.
|
|
206
213
|
|
|
214
|
+
**The git-push surface, and why it was the lenient one (2026-08-06).** (c) blocked on an absent
|
|
215
|
+
override; the `git push` gate in `templates/.git-hooks/pre-push` only warned. Both make content
|
|
216
|
+
public, so two irreversible surfaces were degrading in **opposite directions on the same state** —
|
|
217
|
+
the actual defect, and `git push` (the one nobody publishes through deliberately) was the permissive
|
|
218
|
+
side. The 2026-07-26 reasoning behind that warn was not wrong, it was **unscoped**: an absent
|
|
219
|
+
override in a fresh clone / CI runner / worktree is a legitimate per-operator configuration gap (the
|
|
220
|
+
file is gitignored, so it is absent there *by construction*), and blocking it trains
|
|
221
|
+
`PUBLIC_SURFACE_OK` into a reflex — which disarms the same channel the publish gate depends on.
|
|
222
|
+
|
|
223
|
+
So the warn is **scoped, not reverted**. `psa_detect_operator_context` (`scripts/psa_scan_lib.sh`)
|
|
224
|
+
splits the state: in an **operator-configured checkout** an absent override is *evidence missing
|
|
225
|
+
where evidence is expected* → BLOCK; everywhere else → WARN, exactly as before. The signal is
|
|
226
|
+
`CLAUDE.local.md`, the operator's own gitignored binding file.
|
|
227
|
+
|
|
228
|
+
**Calibration matters here more than the rule** — a second candidate signal, "`tracks/_meta` is
|
|
229
|
+
non-empty", reads as the same test and is not: `tracks/_meta/.gitkeep` and one sibling are
|
|
230
|
+
**tracked**, so a fresh clone satisfies it and would have been blocked, re-shipping the 07-26
|
|
231
|
+
over-block under a new name. It was rejected by measuring it against a known pair, not by reasoning
|
|
232
|
+
about it. **Named residual, deliberately in the under-blocking direction**: an operator who never
|
|
233
|
+
created a `CLAUDE.local.md` stays in the WARN arm. **Second residual, and it is the sharper one**:
|
|
234
|
+
deleting `CLAUDE.local.md` drops this checkout back into the WARN arm, and unlike `PUBLIC_SURFACE_OK=1`
|
|
235
|
+
that bypass **writes no log line**. It is a conscious act on the operator's own file, so it is not a
|
|
236
|
+
weak-model fail-open — but it is a quieter exit than the sanctioned one, which is the wrong ordering
|
|
237
|
+
for a bypass. Not closed here: making it loud means the hook must distinguish "never had one" from
|
|
238
|
+
"had one and lost it", and that needs state the hook does not currently keep. Named rather than
|
|
239
|
+
mechanized, per this repo's own threshold — mechanize on the first measured recurrence.
|
|
240
|
+
Anchored as a pair in `prepush_guard_check.sh`
|
|
241
|
+
(6-a absent override → PASS · 6-b absent override + operator checkout → BLOCK); both arms are
|
|
242
|
+
required, since 6-b passing alone would not distinguish a scoped block from a blanket one.
|
|
243
|
+
|
|
207
244
|
**Named residuals for (c)** — it is a denylist **on the npm CLI path with scripts enabled**, not a
|
|
208
245
|
universal secret-scanner:
|
|
209
246
|
|
|
@@ -996,3 +996,76 @@
|
|
|
996
996
|
invented: a fabricated outcome would poison the 60/40 promotion gate worse than a missing one,
|
|
997
997
|
which is the stated reason the hook only tallies and never writes an entry. Open question for
|
|
998
998
|
the next session that touches the tally: which tool events actually fire SubagentStop here.
|
|
999
|
+
|
|
1000
|
+
- date: '2026-08-05'
|
|
1001
|
+
agent: persona-innovator ×2 · quench-challenger ×3 · beginner ×5 (Agent tool) + codex gpt-5.5 ×2 (CLI)
|
|
1002
|
+
context: >-
|
|
1003
|
+
Innovator autopilot session. Two sister-asset absorption scans (innovator); steel-quench Wave 1
|
|
1004
|
+
and Wave 3 on the selfcheck run-twice repair, plus one opus Axis-2 pass on the AGENTS.md change
|
|
1005
|
+
(quench-challenger); five Sonnet-pinned cold-read sims on one AGENTS.md checklist item
|
|
1006
|
+
(beginner); two cross-family audits (codex CLI, tool-use disabled — not counted by the tally
|
|
1007
|
+
hook, which only fires on Agent-tool SubagentStop).
|
|
1008
|
+
dispatches: '10 via Agent tool + 2 CLI sidecars (tally recorded 14 — the delta is unattributed,
|
|
1009
|
+
see the 2026-08-05 entry above about unknown tally sources; not invented here either)'
|
|
1010
|
+
outcome: accepted
|
|
1011
|
+
evidence: >-
|
|
1012
|
+
Every class changed the outcome, which is why this is `accepted` rather than `partial`.
|
|
1013
|
+
quench-challenger: Wave 1 produced 2S/3A/4B and Wave 3 produced 2S/1A/3B on the REPAIR — one of
|
|
1014
|
+
those overturned a judgment already reported to the operator (a "refuted" verdict measured on a
|
|
1015
|
+
non-canonical absolute core.hooksPath; the documented relative-path install reproduces the
|
|
1016
|
+
bypass). The opus Axis-2 pass produced 2S that caused a full revert of the AGENTS.md change.
|
|
1017
|
+
beginner: 5 rounds, each blocking at a DIFFERENT point, each block introduced by the previous
|
|
1018
|
+
round's repair; author self-catch across rounds 1-4 was ZERO. innovator: 4 external repos
|
|
1019
|
+
verified by gh api with known-pair controls, 3 net-new items confirmed by in-repo greps with
|
|
1020
|
+
live controls. codex: round 1 was fed a STALE diff (my error — the target was not frozen) so 2
|
|
1021
|
+
of its findings were already closed and 1 was a misread; round 2 on a fingerprinted target found
|
|
1022
|
+
the BSD `tr -d '[:space:]'` defect that both same-family rounds missed.
|
|
1023
|
+
note: >-
|
|
1024
|
+
Logged at close, after the session_close_check ④-e block caught the omission — the obligation
|
|
1025
|
+
is prose in CLAUDE.md and I did not discharge it during the session despite dispatching 10+
|
|
1026
|
+
agents. That is the same shape the ④-e check was built for. Consolidated per class with counts
|
|
1027
|
+
rather than one stub per dispatch, which the check explicitly permits.
|
|
1028
|
+
|
|
1029
|
+
- date: '2026-08-06'
|
|
1030
|
+
agent: beginner ×5 (Sonnet-pinned cold read) · quench-challenger ×1 (opus Axis-2) — same session as 2026-08-05
|
|
1031
|
+
context: >-
|
|
1032
|
+
The innovator autopilot session crossed midnight; the tally records 14 dispatches under
|
|
1033
|
+
2026-08-06 while the substantive description lives in the 2026-08-05 entry above. These are not
|
|
1034
|
+
additional work — they are the same session's later half: the five cold-read rounds on the
|
|
1035
|
+
AGENTS.md worktree item and the opus adversarial pass that ended it.
|
|
1036
|
+
dispatches: '6 attributable here (tally: 14 under this date; the remainder shares the
|
|
1037
|
+
unattributed-source question already recorded on 2026-08-05)'
|
|
1038
|
+
outcome: accepted
|
|
1039
|
+
evidence: >-
|
|
1040
|
+
The opus Axis-2 pass returned 2S — a recipe that commits to the WRONG BRANCH (git forbids two
|
|
1041
|
+
worktrees sharing a branch, so the main checkout and a worktree are always on different
|
|
1042
|
+
branches, and the recipe never aligned them), and a hook that hands out a marker-fabrication
|
|
1043
|
+
one-liner at the exact moment it blocks. Both were reproduced against the live repo, and the
|
|
1044
|
+
change was REVERTED in full rather than patched. The five beginner rounds each blocked at a
|
|
1045
|
+
different point with author self-catch of zero across rounds 1-4.
|
|
1046
|
+
note: >-
|
|
1047
|
+
Written after the close check refused the push a SECOND time: the first correction logged the
|
|
1048
|
+
session under 2026-08-05 while the check counts today's date, so "logged" and "logged where the
|
|
1049
|
+
check looks" came apart. Recorded rather than back-dated — the split is real (the tally itself
|
|
1050
|
+
splits 34/14 across the boundary) and pretending otherwise would misstate when the work ran.
|
|
1051
|
+
|
|
1052
|
+
- date: 2026-08-06
|
|
1053
|
+
agent: general-purpose ×2 (블라인드 응시자), fh-commons:quench-challenger ×2 (Axis 2)
|
|
1054
|
+
purpose: >-
|
|
1055
|
+
qasp 풀체인 단련 — 출제자/응시자 분리 블라인드 실행 2회(2막 대조 · 3층 난이도)와,
|
|
1056
|
+
FH 마감 체커 fail-open 수리에 대한 Axis 2 적대검증 2라운드(sonnet → at-floor opus).
|
|
1057
|
+
model: sonnet ×3, opus ×1
|
|
1058
|
+
outcome: accepted
|
|
1059
|
+
evidence: >-
|
|
1060
|
+
블라인드 2회가 계기 결함 2건을 직접 산출했다 — 1차는 두 런 차이를 "플레이키"로 오귀속해
|
|
1061
|
+
리포트에 코드 상태 지문이 없음을 드러냈고(#84→PR #86), 2차는 counts 만 대조해 "변화 없음"이라
|
|
1062
|
+
결론내 집계가 개별을 가리는 문제를 드러냈다(→PR #87). 두 오귀속 모두 **주어진 산출물만 보고는
|
|
1063
|
+
그렇게 결론낼 수밖에 없었다** — 그래서 결함이 응시자가 아니라 계기 쪽임이 판별됐다.
|
|
1064
|
+
챌린저 R1(sonnet)은 수리 대상 바로 윗줄에 같은 fail-open 이 남아 있음을 재현으로 잡았고,
|
|
1065
|
+
게이트가 floor 미달로 거부해 R2(opus, at-floor)를 돌리자 R1 이 못 본 4건이 추가로 나왔다
|
|
1066
|
+
(재발견 0건, 전부 실제 재현). known-pair 6/6.
|
|
1067
|
+
note: >-
|
|
1068
|
+
45 dispatch 대 1 로그 항목으로 ④-e 가 통과한 상태였다 — 이 항목이 그 간극을 메운다.
|
|
1069
|
+
카운트가 큰 것은 Explore 등 내부 에이전트가 함께 집계되기 때문이고, 의미 있는 위임은 위 4건이다.
|
|
1070
|
+
★방법론 소득이 결함보다 크다: 출제자=응시자면 "잘 나온다"로 끝나고, 분리하면 매 라운드
|
|
1071
|
+
계기 결함이 나온다. 그리고 floor 는 장식이 아니었다 — sonnet R1 이 놓친 HIGH 를 opus R2 가 잡았다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrono-meta/fh-gate",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.89",
|
|
4
4
|
"description": "FH runtime adapters — run FH governance, skills, and agents via Claude or Codex with machine-parseable gates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"prepare": "chmod +x bin/fh-gate.js bin/fh-run.js bin/fh-goal.js bin/fh-codex-doctor.js scripts/fh-gate.sh scripts/fh-run.sh scripts/fh-goal.sh",
|
|
28
28
|
"test": "bash scripts/selfcheck.sh",
|
|
29
|
-
"prepublishOnly": "bash scripts/selfcheck.sh && bash scripts/public_surface_scan_files.sh",
|
|
29
|
+
"prepublishOnly": "bash scripts/version_lockstep_check.sh && bash scripts/selfcheck.sh && bash scripts/public_surface_scan_files.sh",
|
|
30
30
|
"release": "bash scripts/public_surface_scan_files.sh && npm publish"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
@@ -64,7 +64,9 @@
|
|
|
64
64
|
"scripts/fh-goal.sh",
|
|
65
65
|
"scripts/count_check.sh",
|
|
66
66
|
"scripts/selfcheck.sh",
|
|
67
|
+
"scripts/version_lockstep_check.sh",
|
|
67
68
|
"scripts/test_selfcheck_state_lanes.sh",
|
|
69
|
+
"scripts/test_version_lockstep_lanes.sh",
|
|
68
70
|
"scripts/package_coverage_check.sh",
|
|
69
71
|
"scripts/test_package_coverage_lanes.sh",
|
|
70
72
|
"scripts/test_fh_gate_regressions.sh",
|
|
@@ -166,6 +166,21 @@ check "operator override absent (per-operator) → PASS " "$R" pass \
|
|
|
166
166
|
"refs/heads/f $(cd "$R" && git rev-parse HEAD) refs/heads/f $B"
|
|
167
167
|
rm -rf "$R"
|
|
168
168
|
|
|
169
|
+
# ── Pair 6-b (2026-08-06): the OTHER arm of the same state, which the 07-26 flag could not see.
|
|
170
|
+
# Identical to 6-a except the checkout carries CLAUDE.local.md — the operator's gitignored binding
|
|
171
|
+
# file, absent in every clone/CI/worktree by construction. There the missing override is not an
|
|
172
|
+
# unset per-operator config, it is evidence missing where evidence is expected, on a surface that
|
|
173
|
+
# publishes. This is what `npm publish` already blocked while `git push` waved through; the two
|
|
174
|
+
# irreversible surfaces now degrade in the same direction.
|
|
175
|
+
# The PAIR is what makes it a measurement: 6-a (no CLAUDE.local.md → PASS) is the known-negative and
|
|
176
|
+
# must keep passing, or this is not a scoped block, it is the 07-26 over-block re-shipped. ──
|
|
177
|
+
R=$(newrepo); B=$(cd "$R" && git rev-parse HEAD)
|
|
178
|
+
( cd "$R" && echo ok > g2.md && git add g2.md && git commit -qm g2 >/dev/null \
|
|
179
|
+
&& rm -f .claude/rules/.public-surface-patterns && printf '# operator binding\n' > CLAUDE.local.md )
|
|
180
|
+
check "override absent + operator checkout → BLOCK" "$R" block \
|
|
181
|
+
"refs/heads/f $(cd "$R" && git rev-parse HEAD) refs/heads/f $B"
|
|
182
|
+
rm -rf "$R"
|
|
183
|
+
|
|
169
184
|
R=$(newrepo); B=$(cd "$R" && git rev-parse HEAD)
|
|
170
185
|
( cd "$R" && echo ok > g.md && git add g.md && git commit -qm g >/dev/null \
|
|
171
186
|
&& : > .claude/rules/.public-surface-patterns.defaults ) # present but EMPTY = broken, not unconfigured
|
package/scripts/psa_scan_lib.sh
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
# PSA_DEFAULTS_OK 1 = committed defaults present, readable, non-empty
|
|
39
39
|
# PSA_OVERRIDE_PRESENT 1 = operator override present, readable, non-empty
|
|
40
40
|
# PSA_BAD_ROWS count of rows dropped as unusable (uncompilable regex, or no TAB)
|
|
41
|
+
# State set by psa_detect_operator_context (separate call — see that function):
|
|
42
|
+
# PSA_OPERATOR_CONTEXT 1 = an operator-configured checkout, where an absent override is missing
|
|
43
|
+
# evidence rather than a legitimate fresh-clone absence
|
|
41
44
|
# A caller that treats PSA_BAD_ROWS>0 or PSA_DEFAULTS_OK=0 as "clean" has a hole: those states mean
|
|
42
45
|
# the instrument is incomplete, and an incomplete instrument cannot certify anything.
|
|
43
46
|
|
|
@@ -115,6 +118,39 @@ PSA_ROWS
|
|
|
115
118
|
PSA_STREAM="$valid"
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
# psa_detect_operator_context <repo_root> → sets PSA_OPERATOR_CONTEXT (0/1)
|
|
122
|
+
#
|
|
123
|
+
# Answers ONE question: is an absent operator override a *legitimate absence* (fresh clone, CI
|
|
124
|
+
# runner, worktree — the file is gitignored, so it is absent there by construction) or *evidence
|
|
125
|
+
# missing where evidence is expected* (a checkout the operator has configured, whose literals should
|
|
126
|
+
# be there and are not)?
|
|
127
|
+
#
|
|
128
|
+
# 2026-07-26 changed pre-push from BLOCK to WARN on an absent override, and that change was right for
|
|
129
|
+
# the first case: this repo's own selfcheck flagged the block as over-firing, and over-blocking trains
|
|
130
|
+
# PUBLIC_SURFACE_OK into a reflex, which disarms the same channel the publish gate depends on. It was
|
|
131
|
+
# wrong for the second case, which the flag could not distinguish. This function is that distinction —
|
|
132
|
+
# it does not reopen the reverted block, it scopes it.
|
|
133
|
+
#
|
|
134
|
+
# Signal: CLAUDE.local.md — the operator's own gitignored binding file. Chosen because it is
|
|
135
|
+
# gitignored (verified: `.gitignore:17`), so no clone, CI checkout or worktree carries it.
|
|
136
|
+
#
|
|
137
|
+
# CALIBRATED, and one candidate was REJECTED by that calibration: "tracks/_meta is non-empty" looks
|
|
138
|
+
# like the same signal and is not — `tracks/_meta/.gitkeep` and one more file are TRACKED, so a fresh
|
|
139
|
+
# clone satisfies it and would have been blocked. The discriminator was measured against a known
|
|
140
|
+
# pair before it was trusted, not reasoned about.
|
|
141
|
+
#
|
|
142
|
+
# NAMED RESIDUAL (deliberate, and in the safe direction): an operator who never created a
|
|
143
|
+
# CLAUDE.local.md stays in the WARN arm. This under-blocks rather than over-blocks — the failure mode
|
|
144
|
+
# this scoping exists to avoid is the reflex, not the miss, and the push gate's content scan still
|
|
145
|
+
# runs on the committed defaults in that arm.
|
|
146
|
+
psa_detect_operator_context() {
|
|
147
|
+
local root="$1"
|
|
148
|
+
PSA_OPERATOR_CONTEXT=0
|
|
149
|
+
[ -n "$root" ] || return 0
|
|
150
|
+
[ -f "$root/CLAUDE.local.md" ] && PSA_OPERATOR_CONTEXT=1
|
|
151
|
+
return 0
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
# psa_scan_tagged — reads "path<TAB>content" lines on stdin, prints one line per reportable hit.
|
|
119
155
|
# Returns 0 = nothing reportable, 1 = at least one hit.
|
|
120
156
|
#
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -25,6 +25,74 @@ check() { # check <label> <cmd...>
|
|
|
25
25
|
fail=1
|
|
26
26
|
fi
|
|
27
27
|
}
|
|
28
|
+
# ⚠️ KNOWN DEFECT, NOT FIXED HERE — `check()` above has the same evidence-discarding shape the
|
|
29
|
+
# lane blocks below were repaired for (2026-08-05): it decides on `"$@" 2>/dev/null` (stderr of the
|
|
30
|
+
# DECIDING run is destroyed) and then re-runs to print. It is left alone deliberately: `check()` is
|
|
31
|
+
# called by every `node --check` / `bash -n` line in this file, so changing it changes the whole
|
|
32
|
+
# surface at once, which is a different job from repairing the four lane blocks (CLAUDE.md
|
|
33
|
+
# §Added-Scope Gate question 2). `scripts/probe_scope_check.sh`'s caller near the probe-scope block
|
|
34
|
+
# carries the same shape. Both are tracked separately — do NOT read the lane-block repair below as
|
|
35
|
+
# having cleared this file.
|
|
36
|
+
|
|
37
|
+
# _show_failure <captured-output> — print a FAILING suite's evidence without truncating it away.
|
|
38
|
+
# Single source for all four lane blocks (a second copy would drift; the divergent-normalizer class).
|
|
39
|
+
# WHY NOT `tail -N`: measured 2026-08-05 on sync_from_be_lanes.sh — output is 98 lines and a planted
|
|
40
|
+
# lane failure at line ~22 is INVISIBLE to `tail -20` (0 hits), while the summary banner still reads
|
|
41
|
+
# "1 failed". The reader gets a FAIL verdict sitting on top of passing log lines — the exact shape
|
|
42
|
+
# this whole repair exists to remove. The failing-line extraction finds it (1 hit, known pair).
|
|
43
|
+
# All four suites mark failures with `❌` (`no()` in sync_from_be_lanes.sh:21, `chk()` in the other
|
|
44
|
+
# three) or an early `FAIL ` line when a subject is missing; grep handles the multi-byte glyph
|
|
45
|
+
# (known pair: 1 hit on a ❌ line, 0 on a ✅-only line — verified, not assumed).
|
|
46
|
+
_show_failure() {
|
|
47
|
+
local out="$1" fails n banner shown nonblank
|
|
48
|
+
# Whitespace-only counts as empty: guarding with [ -z "$out" ] alone let a suite emitting " "
|
|
49
|
+
# fall into the died-early branch and print indented blank lines — silence rendered as evidence.
|
|
50
|
+
# SHELL PATTERN, NOT `tr`: the obvious `tr -d '[:space:]'` is a measured defect on BSD. Given a
|
|
51
|
+
# line containing invalid UTF-8, macOS `tr` aborts with `tr: Illegal byte sequence` and emits
|
|
52
|
+
# NOTHING, so the guard concludes "empty" and reports "no output captured" while real evidence is
|
|
53
|
+
# sitting in $out — the exact mis-report this helper exists to prevent, reintroduced by the guard
|
|
54
|
+
# against it. (GNU tr passes the bytes through; the arms disagree, and the failing arm is the
|
|
55
|
+
# author's own machine.) Case-matching is a shell builtin: no subprocess, no charset decoding, so
|
|
56
|
+
# invalid bytes cannot make it lie. Known pair: invalid-byte→nonblank, spaces/tabs/newlines→blank,
|
|
57
|
+
# ""→blank, "hello"→nonblank (4/4), while the tr form returns 0 bytes on arm 1.
|
|
58
|
+
case "$out" in *[![:space:]]*) nonblank=1 ;; *) nonblank= ;; esac
|
|
59
|
+
# NO LOCALE PIN HERE, and its absence is a measured result — same disposition, and same reasoning,
|
|
60
|
+
# as the pin `.github/workflows/validate.yml` removed after refuting its own locale hypothesis.
|
|
61
|
+
# Two model families independently suspected that matching the multi-byte `❌` would break under a
|
|
62
|
+
# C/POSIX locale (one filed it as UNCALIBRATED for the GNU arm, the other as an unpinned-locale
|
|
63
|
+
# defect), so `LC_ALL=C` was added — then both arms were actually measured:
|
|
64
|
+
# BSD grep (macOS) : C, UTF-8 → 1 hit each
|
|
65
|
+
# GNU grep 3.12 (Linux) : C, C.UTF-8, unset → 1 hit each; and with invalid UTF-8 bytes mixed
|
|
66
|
+
# in, the ❌ line still extracts (no binary-file
|
|
67
|
+
# collapse, the specific feared mode)
|
|
68
|
+
# The hypothesis is REFUTED on both arms, so the pin demonstrated nothing and was removed rather
|
|
69
|
+
# than kept as insurance — a knob retained because it might help is indistinguishable from one
|
|
70
|
+
# that does, and the next reader would inherit it as evidence that the danger is real.
|
|
71
|
+
fails=$(printf '%s\n' "$out" | grep -E '❌|^FAIL ' || true)
|
|
72
|
+
banner=$(printf '%s\n' "$out" | grep -E '════' | tail -1 || true)
|
|
73
|
+
if [ -n "$fails" ]; then
|
|
74
|
+
shown=$(printf '%s\n' "$fails" | head -25)
|
|
75
|
+
printf '%s\n' "$shown" | sed 's/^/ /'
|
|
76
|
+
n=$(printf '%s\n' "$fails" | wc -l | tr -d ' ')
|
|
77
|
+
[ "$n" -gt 25 ] && echo " … ($((n - 25)) more failing lines not shown)"
|
|
78
|
+
elif [ -z "$nonblank" ]; then
|
|
79
|
+
echo " (no output captured — the suite produced nothing before failing)"
|
|
80
|
+
else
|
|
81
|
+
echo " (no ❌/FAIL line found — suite likely died early; showing tail)"
|
|
82
|
+
printf '%s\n' "$out" | tail -12 | sed 's/^/ /'
|
|
83
|
+
fi
|
|
84
|
+
# Summary banner: matched by shape, not by position. A blind `tail -2` re-printed lines already
|
|
85
|
+
# shown above (measured: a ❌ within the last 2 lines appeared twice, and the "N more not shown"
|
|
86
|
+
# notice was immediately followed by one of the lines it had just declined to show), and on empty
|
|
87
|
+
# input it emitted a stray indented line. Print it only when it exists and is not already on screen.
|
|
88
|
+
# Dedupe against what was ACTUALLY PRINTED ($shown), not against the full $fails set. Searching
|
|
89
|
+
# $fails suppressed the banner whenever a failing line beyond the head -25 cut merely contained
|
|
90
|
+
# the banner text — i.e. it hid the banner precisely because a line the reader never saw mentioned
|
|
91
|
+
# it. $shown is empty in the non-fails branches, so the banner prints there as before.
|
|
92
|
+
if [ -n "$banner" ] && ! printf '%s\n' "${shown:-}" | grep -qF -- "$banner"; then
|
|
93
|
+
printf ' %s\n' "$banner"
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
28
96
|
|
|
29
97
|
# Node executables (npm-shipped)
|
|
30
98
|
for f in bin/*.js; do
|
|
@@ -34,7 +102,18 @@ done
|
|
|
34
102
|
# Codex adapter drift: the thin Codex runtime must keep reading canonical FH
|
|
35
103
|
# skill/agent surfaces without silently accepting Claude-native primitives as
|
|
36
104
|
# Codex-native.
|
|
37
|
-
check
|
|
105
|
+
# NOT via check(): that helper decides on a run whose stderr is discarded, and this call site used to
|
|
106
|
+
# additionally discard the subject's STDOUT (`--strict >/dev/null`). fh-codex-doctor writes 100% of
|
|
107
|
+
# its drift diagnostics to stdout (measured 2026-08-05: 686 B stdout / 0 B stderr), so a failure
|
|
108
|
+
# printed a bare `FAIL` line carrying no diagnosis at all — worse than the truncation this session
|
|
109
|
+
# repaired in the lane blocks. Fixed at the call site; check() itself is a separate job (see above).
|
|
110
|
+
if _out=$(node bin/fh-codex-doctor.js --strict 2>&1); then
|
|
111
|
+
echo "PASS fh-codex-doctor --strict"
|
|
112
|
+
else
|
|
113
|
+
echo "FAIL fh-codex-doctor --strict"
|
|
114
|
+
_show_failure "$_out"
|
|
115
|
+
fail=1
|
|
116
|
+
fi
|
|
38
117
|
|
|
39
118
|
# Bash surface: npm-shipped scripts + local bin wrappers + gate-chain infra
|
|
40
119
|
for f in scripts/*.sh bin/fh-gate bin/fh-run bin/fh-goal \
|
|
@@ -447,28 +526,48 @@ fi
|
|
|
447
526
|
if [ ! -f templates/.git-hooks/pre-push ]; then
|
|
448
527
|
echo "SKIP test_tag_version_lanes.sh (subject templates/.git-hooks/pre-push absent)"
|
|
449
528
|
elif [ -f scripts/test_tag_version_lanes.sh ]; then
|
|
450
|
-
|
|
529
|
+
# RUN-ONCE, CAPTURE (2026-08-05) — rationale in the sync_from_be_lanes block later in this file.
|
|
530
|
+
if _out=$(bash scripts/test_tag_version_lanes.sh 2>&1); then
|
|
531
|
+
echo "PASS test_tag_version_lanes.sh (mismatch blocks · match silent · scope · override)"
|
|
532
|
+
else
|
|
451
533
|
echo "FAIL test_tag_version_lanes.sh: the tag/version guard would mis-route"
|
|
452
|
-
|
|
534
|
+
_show_failure "$_out"
|
|
453
535
|
fail=1
|
|
454
|
-
else
|
|
455
|
-
echo "PASS test_tag_version_lanes.sh (mismatch blocks · match silent · scope · override)"
|
|
456
536
|
fi
|
|
457
537
|
else
|
|
458
538
|
echo "FAIL test_tag_version_lanes.sh: pre-push present but its anchor is missing"
|
|
459
539
|
fail=1
|
|
460
540
|
fi
|
|
461
541
|
|
|
542
|
+
# Shipped-manifest version lockstep. Distinct from the tag lane above: that one compares the git TAG
|
|
543
|
+
# to package.json; this one compares package.json to every version string it SHIPS — including the
|
|
544
|
+
# per-plugin entries inside marketplace.json, which the tag lane never opens. Measured 2026-08-06:
|
|
545
|
+
# a bump left the second marketplace entry behind and the tag lane passed 8/8 straight through it.
|
|
546
|
+
if [ ! -f scripts/version_lockstep_check.sh ]; then
|
|
547
|
+
echo "SKIP test_version_lockstep_lanes.sh (subject scripts/version_lockstep_check.sh absent)"
|
|
548
|
+
elif [ -f scripts/test_version_lockstep_lanes.sh ]; then
|
|
549
|
+
if _out=$(bash scripts/test_version_lockstep_lanes.sh 2>&1); then
|
|
550
|
+
echo "PASS test_version_lockstep_lanes.sh (drift blocks · aligned silent · unreadable = exit 2, not pass)"
|
|
551
|
+
else
|
|
552
|
+
echo "FAIL test_version_lockstep_lanes.sh: the shipped-manifest lockstep guard would mis-route"
|
|
553
|
+
_show_failure "$_out"
|
|
554
|
+
fail=1
|
|
555
|
+
fi
|
|
556
|
+
else
|
|
557
|
+
echo "FAIL test_version_lockstep_lanes.sh: version_lockstep_check.sh present but its anchor is missing"
|
|
558
|
+
fail=1
|
|
559
|
+
fi
|
|
560
|
+
|
|
462
561
|
# ④-e dispatch-log reconciliation + its tally hook. Wired in the same commit that ships them: the
|
|
463
562
|
# obligation they mechanize lost 20/20 in a single session, so leaving the checker itself unrun
|
|
464
563
|
# would be the same defect one layer up.
|
|
465
564
|
if [ -f scripts/test_dispatch_log_lanes.sh ]; then
|
|
466
|
-
if
|
|
565
|
+
if _out=$(bash scripts/test_dispatch_log_lanes.sh 2>&1); then
|
|
566
|
+
echo "PASS test_dispatch_log_lanes.sh (date-spelling + verdict + tally-hook lanes)"
|
|
567
|
+
else
|
|
467
568
|
echo "FAIL test_dispatch_log_lanes.sh: the dispatch-log reconciliation would mis-report"
|
|
468
|
-
|
|
569
|
+
_show_failure "$_out"
|
|
469
570
|
fail=1
|
|
470
|
-
else
|
|
471
|
-
echo "PASS test_dispatch_log_lanes.sh (date-spelling + verdict + tally-hook lanes)"
|
|
472
571
|
fi
|
|
473
572
|
fi
|
|
474
573
|
|
|
@@ -477,12 +576,12 @@ fi
|
|
|
477
576
|
# through in silence, then a package discriminator keyed on a file that actually ships. Both are
|
|
478
577
|
# known-POSITIVEs in the suite, so neither can come back green.
|
|
479
578
|
if [ -f scripts/test_selfcheck_state_lanes.sh ]; then
|
|
480
|
-
if
|
|
579
|
+
if _out=$(bash scripts/test_selfcheck_state_lanes.sh 2>&1); then
|
|
580
|
+
echo "PASS test_selfcheck_state_lanes.sh (four input states + both shipped mis-routings)"
|
|
581
|
+
else
|
|
481
582
|
echo "FAIL test_selfcheck_state_lanes.sh: a subject-presence discriminator would mis-route"
|
|
482
|
-
|
|
583
|
+
_show_failure "$_out"
|
|
483
584
|
fail=1
|
|
484
|
-
else
|
|
485
|
-
echo "PASS test_selfcheck_state_lanes.sh (four input states + both shipped mis-routings)"
|
|
486
585
|
fi
|
|
487
586
|
fi
|
|
488
587
|
|
|
@@ -495,12 +594,34 @@ fi
|
|
|
495
594
|
if [ ! -f scripts/sync-from-be.sh ]; then
|
|
496
595
|
echo "SKIP sync_from_be_lanes.sh (subject scripts/sync-from-be.sh absent)"
|
|
497
596
|
elif [ -f scripts/sync_from_be_lanes.sh ]; then
|
|
498
|
-
|
|
597
|
+
# ── RUN-ONCE, CAPTURE — canonical note for the four LANE BLOCKS (2026-08-05) ─────────────────
|
|
598
|
+
# SCOPE, stated precisely because the first draft of this note over-claimed: this covers the four
|
|
599
|
+
# lane blocks only (tag-version · dispatch-log · selfcheck-state · sync_from_be). The same
|
|
600
|
+
# evidence-discarding shape SURVIVES in `check()` at the top of this file and in the
|
|
601
|
+
# probe_scope_check caller — both named there, both deliberately out of scope, both still open.
|
|
602
|
+
# An adversarial round caught the original "all four sites in this file" wording as a false
|
|
603
|
+
# completion claim: it would have stopped the next reader from re-searching. Half a fix with a
|
|
604
|
+
# done-label on it is worse than half a fix.
|
|
605
|
+
# The old form ran the suite twice: once discarded to /dev/null to decide, once re-run to print.
|
|
606
|
+
# For a DETERMINISTIC suite that is merely wasteful. For a non-deterministic one it destroys the
|
|
607
|
+
# evidence: the failing run's output goes to /dev/null and the reader is shown the SECOND run,
|
|
608
|
+
# which may pass. Measured here 2026-08-04 (run 30955950695) — CI printed
|
|
609
|
+
# FAIL sync_from_be_lanes.sh: return-path lanes failed
|
|
610
|
+
# ════ lanes: 70 passed · 0 failed ════
|
|
611
|
+
# i.e. a FAIL verdict over a PASSING transcript, and the actual failure was never recorded
|
|
612
|
+
# anywhere. That is why this lane sat "flaky, cause unknown" on the session card for two days:
|
|
613
|
+
# the instrument was discarding the only evidence that could close it. Reproduced as a known
|
|
614
|
+
# pair before this edit (arm A run-twice → evidence lost + self-contradiction; arm B run-once →
|
|
615
|
+
# evidence preserved), so the fix is anchored, not asserted.
|
|
616
|
+
# NOTE this does NOT make the suite deterministic — the underlying non-determinism is still
|
|
617
|
+
# UNDIAGNOSED and stays an open item. It makes the next occurrence diagnosable instead of
|
|
618
|
+
# self-erasing. Do not read a green CI after this change as the flake being fixed.
|
|
619
|
+
if _out=$(bash scripts/sync_from_be_lanes.sh 2>&1); then
|
|
620
|
+
echo "PASS sync_from_be_lanes.sh (return-path lanes)"
|
|
621
|
+
else
|
|
499
622
|
echo "FAIL sync_from_be_lanes.sh: return-path lanes failed"
|
|
500
|
-
|
|
623
|
+
_show_failure "$_out"
|
|
501
624
|
fail=1
|
|
502
|
-
else
|
|
503
|
-
echo "PASS sync_from_be_lanes.sh (return-path lanes)"
|
|
504
625
|
fi
|
|
505
626
|
else
|
|
506
627
|
echo "FAIL sync_from_be_lanes.sh: sync-from-be.sh present but its anchor is missing"
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
set -uo pipefail
|
|
17
17
|
|
|
18
|
+
# 상속된 git 환경변수를 끊는다 — export 된 GIT_DIR 이 있으면 `git -C "$FH"` 가 인자로 받은
|
|
19
|
+
# 레포가 아니라 그 레포를 잰다(Axis 2 at-floor LOW, 2026-08-06). READ-ONLY 체커라 부작용 없음.
|
|
20
|
+
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
|
|
18
21
|
FH="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
19
22
|
TODAY=$(date +%Y-%m-%d)
|
|
20
23
|
CARD="$FH/tracks/_meta/reference_next_session_starter.md"
|
|
@@ -25,11 +28,58 @@ _mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0;
|
|
|
25
28
|
echo "── session close check: $FH ($TODAY) ──"
|
|
26
29
|
|
|
27
30
|
# ① status snapshot — uncommitted / unpushed work must be known, not forgotten
|
|
28
|
-
DIRTY
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# DIRTY 도 같은 형태였다 — `status --porcelain 2>/dev/null | wc -l` 은 git 이 죽어도 0 을 세어
|
|
32
|
+
# "깨끗함"으로 승격된다(재현: `.git/index` 손상 → exit 128, 출력 0줄). 종료코드를 먼저 본다.
|
|
33
|
+
# ⚠️ 이 줄은 **바로 아래 UNPUSHED 수리와 같은 결함**이었고, 처음엔 아래만 고쳤다 —
|
|
34
|
+
# 반쪽 수리를 고치는 커밋에서 반쪽 수리를 할 뻔했다(Axis 2 챌린저가 잡음, 2026-08-06).
|
|
35
|
+
# `--untracked-files=all` 은 `status.showUntrackedFiles=no` config 를 덮어쓴다. 그 config 아래에서는
|
|
36
|
+
# git 이 **성공적으로 침묵**해(exit 0 · 빈 출력) 종료코드 가드로도 안 잡힌다 — 부재가 다시
|
|
37
|
+
# "깨끗함"으로 렌더된다(Axis 2 at-floor MED, 재현: 미추적 파일 1건이 0 으로 보고됨).
|
|
38
|
+
if _st=$(git -C "$FH" status --porcelain --untracked-files=all 2>/dev/null); then
|
|
39
|
+
DIRTY_KNOWN=1
|
|
40
|
+
DIRTY=$(printf '%s' "$_st" | grep -c . || true)
|
|
41
|
+
else
|
|
42
|
+
DIRTY_KNOWN=0
|
|
43
|
+
DIRTY=0
|
|
44
|
+
fi
|
|
45
|
+
# upstream 유무를 **먼저 판정**한다. `@{u}..` 는 upstream 이 없으면 실패해 0줄을 내고, 그 0을
|
|
46
|
+
# `wc -l` 이 0으로 세어 "nothing unpushed" 로 승격된다 — **한 번도 머신을 떠난 적 없는 커밋이
|
|
47
|
+
# '푸시할 것 없음'으로 읽히는 fail-open**. 부재를 깨끗함으로 읽는 것이라 0 을 신뢰하면 안 된다.
|
|
48
|
+
# (downstream fork's review lane caught it first; this file is the upstream original — 2026-08-06.)
|
|
49
|
+
if git -C "$FH" rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1; then
|
|
50
|
+
UPSTREAM_KNOWN=1
|
|
51
|
+
UNPUSHED=$(git -C "$FH" log --oneline @{u}.. 2>/dev/null | wc -l | tr -d ' ')
|
|
52
|
+
else
|
|
53
|
+
UPSTREAM_KNOWN=0
|
|
54
|
+
UNPUSHED=0
|
|
55
|
+
fi
|
|
56
|
+
[ "$DIRTY_KNOWN" -eq 0 ] && echo "⚠️ ① UNMEASURED — git status failed; working-tree cleanliness is UNKNOWN, not clean"
|
|
57
|
+
[ "$DIRTY_KNOWN" -eq 1 ] && [ "$DIRTY" -gt 0 ] && echo "⚠️ ① $DIRTY uncommitted path(s) — decide: commit or leave deliberately"
|
|
58
|
+
[ "$UPSTREAM_KNOWN" -eq 0 ] && echo "⚠️ ① UNMEASURED — no upstream for this branch; unpushed count is UNKNOWN, not zero"
|
|
59
|
+
[ "$UPSTREAM_KNOWN" -eq 1 ] && [ "$UNPUSHED" -gt 0 ] && echo "⚠️ ① $UNPUSHED unpushed commit(s) — push before close or record why"
|
|
60
|
+
# "as of last fetch" — 원격을 조회하지 않는다. 로컬 remote-tracking ref 가 낡았으면 이 0 도 낡은 값이다
|
|
61
|
+
# (Axis 2 챌린저 MED, 2026-08-06). fetch 를 넣지 않은 것은 마감 체커가 READ-ONLY·오프라인 안전이기 때문.
|
|
62
|
+
# 추적 파일이 assume-unchanged/skip-worktree 로 마킹돼 있으면 그 수정은 porcelain 에 **안 뜬다** —
|
|
63
|
+
# `-uall` 로도 안 잡히는 별개 계기다(Axis 2 at-floor MED, 재현 확인).
|
|
64
|
+
MASKED=$(git -C "$FH" ls-files -v 2>/dev/null | grep -c '^[a-z]' || true)
|
|
65
|
+
[ "${MASKED:-0}" -gt 0 ] \
|
|
66
|
+
&& echo "⚠️ ① $MASKED file(s) assume-unchanged/skip-worktree — their edits are INVISIBLE here"
|
|
67
|
+
|
|
68
|
+
# ★ 잰 범위 ≠ 주장 범위 (Axis 2 at-floor HIGH, 2026-08-06).
|
|
69
|
+
# `@{u}..` 는 **현재 브랜치만** 잰다. 다른 로컬 브랜치에만 있는 미푸시 커밋은 통째로 안 보이는데
|
|
70
|
+
# 화면 문구는 "nothing unpushed"(레포 전체)라고 말한다 — 이 파일의 존재 이유 정중앙이다.
|
|
71
|
+
# 재현: 다른 브랜치에 미푸시 커밋 1건 → `✅ nothing unpushed` 가 그대로 떴다.
|
|
72
|
+
# 원격이 하나도 없으면 이 값이 전 커밋 수로 부풀므로 원격 존재를 먼저 가드한다.
|
|
73
|
+
OTHER_UNPUSHED=0
|
|
74
|
+
if [ -n "$(git -C "$FH" remote 2>/dev/null)" ]; then
|
|
75
|
+
OTHER_UNPUSHED=$(git -C "$FH" log --branches --not --remotes --oneline 2>/dev/null | wc -l | tr -d ' ')
|
|
76
|
+
fi
|
|
77
|
+
[ "${OTHER_UNPUSHED:-0}" -gt 0 ] \
|
|
78
|
+
&& echo "⚠️ ① $OTHER_UNPUSHED commit(s) on local branches never pushed anywhere (all-branch scan)"
|
|
79
|
+
|
|
80
|
+
[ "$DIRTY_KNOWN" -eq 1 ] && [ "$DIRTY" -eq 0 ] && [ "$UPSTREAM_KNOWN" -eq 1 ] && [ "$UNPUSHED" -eq 0 ] \
|
|
81
|
+
&& [ "${OTHER_UNPUSHED:-0}" -eq 0 ] && [ "${MASKED:-0}" -eq 0 ] \
|
|
82
|
+
&& echo "✅ ① working tree clean, nothing unpushed anywhere (as of last fetch)"
|
|
33
83
|
|
|
34
84
|
# ①-b open-PR sweep (surface-not-auto — requires gh; skip silently offline)
|
|
35
85
|
if command -v gh >/dev/null 2>&1; then
|
|
@@ -93,6 +93,109 @@ else
|
|
|
93
93
|
echo " ⏭️ package.json absent — premise unchecked (not a pass)"
|
|
94
94
|
fi
|
|
95
95
|
|
|
96
|
+
echo ""
|
|
97
|
+
echo "── _show_failure: a FAILING suite's evidence must survive to the reader ──"
|
|
98
|
+
# WHY THIS LANE EXISTS (2026-08-05): the four lane blocks in selfcheck.sh used to decide on a
|
|
99
|
+
# discarded run (`>/dev/null`) and then RE-RUN to print. On a non-deterministic suite the re-run can
|
|
100
|
+
# pass, so CI printed a FAIL verdict above a PASSING transcript and the real failure was destroyed —
|
|
101
|
+
# measured in run 30955950695. The repair captures once; this lane is the mechanical anchor for the
|
|
102
|
+
# half that actually makes a failure readable. Without it the repair is unverifiable: reverting to
|
|
103
|
+
# `tail -20` leaves CI green, which is exactly [[feedback_built_but_not_wired]] / anchor-is-decorative.
|
|
104
|
+
# LIFTED, not re-spelled — same reason as the discriminator above.
|
|
105
|
+
FN=$(sed -n '/^_show_failure() {/,/^}$/p' "$SELFCHECK")
|
|
106
|
+
if [ -z "$FN" ]; then
|
|
107
|
+
echo "FAIL _show_failure is no longer defined in selfcheck.sh — this lane cannot verify what it claims."
|
|
108
|
+
echo " If the helper was renamed or removed, update the lane WITH the subject."
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
eval "$FN"
|
|
112
|
+
|
|
113
|
+
# Fixture: a long transcript whose ONLY failing line sits far above any tail window, plus a
|
|
114
|
+
# summary banner at the end that still says something failed. This is the shape that fooled the
|
|
115
|
+
# reader in the CI run above.
|
|
116
|
+
_LONG=$(for i in $(seq 1 40); do echo " ✅ lane L$i ok"; done; echo " ❌ lane L41 tripped — THE EVIDENCE"; for i in $(seq 42 96); do echo " ✅ lane L$i ok"; done; echo "════ lanes: 96 passed · 1 failed ════")
|
|
117
|
+
|
|
118
|
+
_OUT=$(_show_failure "$_LONG")
|
|
119
|
+
printf '%s' "$_OUT" | grep -q 'THE EVIDENCE'; chk $? "the failing line survives (it is 56 lines above the end)"
|
|
120
|
+
printf '%s' "$_OUT" | grep -q '1 failed' ; chk $? "the summary banner is still shown"
|
|
121
|
+
|
|
122
|
+
# CONTROL — the old form must FAIL this same fixture. Without this, the lane could pass for a
|
|
123
|
+
# reason unrelated to the repair (e.g. a fixture short enough that any tail window catches it).
|
|
124
|
+
printf '%s\n' "$_LONG" | tail -20 | grep -q 'THE EVIDENCE'; [ $? -ne 0 ]
|
|
125
|
+
chk $? "CONTROL: the pre-repair form (tail -20) does NOT surface it — the fixture discriminates"
|
|
126
|
+
|
|
127
|
+
# Degenerate inputs: silence must not read as evidence, and a suite that dies before printing any
|
|
128
|
+
# ❌ must say so rather than showing a blank.
|
|
129
|
+
_OUT=$(_show_failure "")
|
|
130
|
+
printf '%s' "$_OUT" | grep -q 'no output captured'; chk $? "empty output is named, not shown as a blank line"
|
|
131
|
+
_OUT=$(_show_failure "some early crash text
|
|
132
|
+
Traceback: boom")
|
|
133
|
+
printf '%s' "$_OUT" | grep -q 'died early'; chk $? "output with no ❌/FAIL falls back and says why"
|
|
134
|
+
|
|
135
|
+
# ── THE DEFECT ITSELF: decide-and-print must be ONE execution ────────────────
|
|
136
|
+
# An earlier version of this lane block tested only the _show_failure HELPER, in isolation, via eval.
|
|
137
|
+
# An adversarial round then reverted a lane block to the original run-twice form — decide on a
|
|
138
|
+
# discarded run, re-run to capture — and this suite still returned PASS (16/16, measured). The anchor
|
|
139
|
+
# was guarding the thing the repair BUILT and not the thing the repair FIXED. That is
|
|
140
|
+
# [[feedback_anchor_can_be_decorative]] with the reversal actually applied, which is the only check
|
|
141
|
+
# that distinguishes the two.
|
|
142
|
+
# The invariant that discriminates: a suite must be EXECUTED EXACTLY ONCE per selfcheck run. The
|
|
143
|
+
# run-twice form necessarily names its subject twice. Keying on the subject path (not on a variable
|
|
144
|
+
# name or a pipe shape) also removes the earlier grep's escape hatch — renaming `_out` no longer
|
|
145
|
+
# evades it, and adding a fifth lane block does not require editing a hardcoded count.
|
|
146
|
+
for _subj in test_tag_version_lanes test_dispatch_log_lanes test_selfcheck_state_lanes sync_from_be_lanes; do
|
|
147
|
+
_n=$(grep -c "bash scripts/${_subj}\.sh" "$SELFCHECK" || true)
|
|
148
|
+
[ "$_n" -eq 1 ]
|
|
149
|
+
chk $? "${_subj}.sh is executed exactly once (found $_n) — 2 means the run-twice form is back"
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
# WIRING — every lane block must route its captured output through the helper. Secondary to the
|
|
153
|
+
# once-only invariant above (this one IS evadable by renaming), kept because it names the intent.
|
|
154
|
+
_CALLS=$(grep -c '_show_failure "\$_out"' "$SELFCHECK")
|
|
155
|
+
[ "$_CALLS" -ge 4 ]; chk $? "every lane block routes failure output through _show_failure (found $_CALLS, expected ≥4)"
|
|
156
|
+
_TAILS=$(grep -c '"\$_out" | tail -' "$SELFCHECK" || true)
|
|
157
|
+
[ "$_TAILS" -eq 0 ]; chk $? "no lane block still truncates with a raw tail (found $_TAILS, expected 0)"
|
|
158
|
+
|
|
159
|
+
# The one non-lane caller that also destroys its evidence at the CALL SITE (not inside check()).
|
|
160
|
+
# `check "..." bash -c '... >/dev/null'` discards the subject's stdout, and fh-codex-doctor writes
|
|
161
|
+
# 100% of its diagnostics to stdout (measured: 686 B stdout / 0 B stderr) — so a strict-mode failure
|
|
162
|
+
# would print a bare FAIL line with zero diagnosis. Anchored here because the fix is one line at the
|
|
163
|
+
# call site and does NOT require touching check() itself.
|
|
164
|
+
_CD=$(grep -c "fh-codex-doctor.js --strict >/dev/null" "$SELFCHECK" || true)
|
|
165
|
+
[ "$_CD" -eq 0 ]; chk $? "fh-codex-doctor's stdout is not discarded at the call site (found $_CD, expected 0)"
|
|
166
|
+
|
|
167
|
+
# Byte-hostile input: a lane emitting invalid UTF-8 must not be reported as "no output". The
|
|
168
|
+
# `tr -d '[:space:]'` form this guard originally used aborts on BSD with "Illegal byte sequence"
|
|
169
|
+
# and emits nothing, so the emptiness check concluded empty while evidence was present.
|
|
170
|
+
# NOTE ON THIS FIXTURE — it deliberately contains NO ❌. The first draft included one, which routed
|
|
171
|
+
# the call into the failing-lines branch, so the emptiness guard (an `elif`) was never reached and the
|
|
172
|
+
# lane passed against the very defect it was written for. Verified by applying the reversion and
|
|
173
|
+
# confirming the diff landed: the lane went green anyway. A fixture that cannot reach the branch
|
|
174
|
+
# under test measures nothing.
|
|
175
|
+
_BAD=$(printf ' \xff\xfe garbage\n crashed before any lane ran BYTE_EVIDENCE\n')
|
|
176
|
+
_OUT=$(_show_failure "$_BAD")
|
|
177
|
+
printf '%s' "$_OUT" | grep -q 'BYTE_EVIDENCE'; chk $? "invalid UTF-8 in the stream does not swallow the evidence"
|
|
178
|
+
printf '%s' "$_OUT" | grep -qv 'no output captured'; chk $? "…and it is not mis-reported as empty output"
|
|
179
|
+
|
|
180
|
+
# The banner must not be suppressed by a line the reader never saw (a failing line past the head cut
|
|
181
|
+
# that merely quotes the banner text).
|
|
182
|
+
_MANY=$(for i in $(seq 1 26); do echo " ❌ f$i"; done; echo " ❌ f27 quoting ════ lanes: 9 passed · 1 failed ════"; echo "════ lanes: 9 passed · 1 failed ════")
|
|
183
|
+
_OUT=$(_show_failure "$_MANY")
|
|
184
|
+
[ "$(printf '%s\n' "$_OUT" | grep -c '════ lanes: 9 passed')" -ge 1 ]
|
|
185
|
+
chk $? "the summary banner still prints when a truncated-away line quotes it"
|
|
186
|
+
|
|
187
|
+
# ── SCOPE OF THIS ANCHOR — stated so it is not over-trusted ───────────────────
|
|
188
|
+
# These lanes catch REVERSION (the run-twice form coming back, the helper being gutted, the
|
|
189
|
+
# call-site redirect returning). They do NOT catch deliberate EVASION: a cross-family round
|
|
190
|
+
# demonstrated three forms that satisfy every check above while still destroying evidence —
|
|
191
|
+
# `_out=$(bash suite >/dev/null 2>&1)` (executed once, captured nothing), a wrapper function
|
|
192
|
+
# (`run_lane() { bash scripts/X.sh; }`, literal appears once, runs twice), and redirect spellings
|
|
193
|
+
# the greps do not enumerate (`1>/dev/null`, a variable). Chasing those with more patterns is the
|
|
194
|
+
# Grep-Collision Treadmill this repo has already logged as P10 — each added regex relocates the
|
|
195
|
+
# evasion instead of closing it. It is bounded rather than escalated: an evading form still routes
|
|
196
|
+
# through _show_failure, whose empty branch prints "(no output captured)" at runtime, so the failure
|
|
197
|
+
# is loud rather than silent. Regression is anchored; evasion is a named residual, not a solved one.
|
|
198
|
+
|
|
96
199
|
echo ""
|
|
97
200
|
if [ "$FAILED" -ne 0 ]; then
|
|
98
201
|
echo "SELFCHECK STATE LANES: FAIL — a discriminator would mis-route"
|
|
@@ -85,6 +85,18 @@ _repo() { # $1=dirname ; makes a git repo with one commit dated $2 (default now
|
|
|
85
85
|
else
|
|
86
86
|
git commit -qm seed
|
|
87
87
|
fi
|
|
88
|
+
# A real repo HAS an upstream. Without one the close check now (correctly) reports
|
|
89
|
+
# `① UNMEASURED — no upstream`, which is not the state any of these lanes means to express —
|
|
90
|
+
# a fixture with no upstream cannot assert "clean tree, nothing unpushed" because the second
|
|
91
|
+
# half of that sentence is genuinely unknown. Every lane below therefore runs on a pushed
|
|
92
|
+
# baseline; upstream ABSENCE is measured on purpose by its own lane (KP-2).
|
|
93
|
+
git init -q --bare "$TMPROOT/$1.git"
|
|
94
|
+
git remote add origin "$TMPROOT/$1.git"
|
|
95
|
+
# `-c core.hooksPath=` : a fixture push must never execute the HOST's git hooks. This repo sets
|
|
96
|
+
# core.hooksPath locally (so a temp repo does not inherit it) but a machine that sets it GLOBALLY
|
|
97
|
+
# would run FH's own pre-push Destructive-Op gate against a throwaway fixture — the suite's result
|
|
98
|
+
# would then depend on the operator's git config rather than on the code under test.
|
|
99
|
+
git -c core.hooksPath= push -q -u origin HEAD
|
|
88
100
|
) >/dev/null 2>&1
|
|
89
101
|
mkdir -p "$T/tracks/_meta"
|
|
90
102
|
printf '%s' "$T"
|
|
@@ -121,13 +133,10 @@ _line "①-N uncommitted path → ⚠️ fires" 'uncommitted path'
|
|
|
121
133
|
_line "①-N uncommitted path → clean line absent" '✅ ① working tree clean' 0 "$OUT"
|
|
122
134
|
_rc "①-N uncommitted is ADVISORY, not blocking" "$RC" 0
|
|
123
135
|
|
|
124
|
-
# unpushed:
|
|
136
|
+
# unpushed: _repo already pushed the seed, so one extra commit is exactly one unpushed commit
|
|
125
137
|
T=$(_repo one_unpushed); _artifacts "$T"
|
|
126
138
|
(
|
|
127
139
|
cd "$T" || exit 1
|
|
128
|
-
git init -q --bare "$TMPROOT/one_unpushed.git" 2>/dev/null
|
|
129
|
-
git remote add origin "$TMPROOT/one_unpushed.git"
|
|
130
|
-
git push -q -u origin HEAD 2>/dev/null
|
|
131
140
|
echo more > second.txt && git add -A && git commit -qm second
|
|
132
141
|
) >/dev/null 2>&1
|
|
133
142
|
_run "$T"
|
|
@@ -141,6 +150,72 @@ _gap "① non-repo reports CLEAN" "$_g" \
|
|
|
141
150
|
"git is unavailable/not a repo → DIRTY=0, UNPUSHED=0 → the check reports '✅ working tree clean'. \
|
|
142
151
|
An instrument that could not look is not a clean result (not found ≠ 0). Should say UNSCANNED."
|
|
143
152
|
|
|
153
|
+
# ── ① not-found ≠ 0 : the five states where git CANNOT answer ────────────────────
|
|
154
|
+
# Origin: the fix that introduced these five guards (2026-08-06) listed all six known pairs in its
|
|
155
|
+
# COMMIT MESSAGE and shipped none of them as a lane — the code changed, the suite did not, and CI
|
|
156
|
+
# went red on the two lanes the change broke rather than on the five it left unmeasured. Prose in a
|
|
157
|
+
# commit message is not a regression anchor: nothing re-runs it. Each pair below is
|
|
158
|
+
# known-positive (the instrument is blind) + a paired control (the clean line must NOT appear),
|
|
159
|
+
# because "the warning fired" and "the warning fired INSTEAD of a false all-clear" are two claims.
|
|
160
|
+
# KP-1 (the healthy case) is the ①-P lane at the top of this section.
|
|
161
|
+
|
|
162
|
+
# KP-2 upstream absent — `@{u}..` fails, prints 0 lines, and `wc -l` counts that 0 as "nothing
|
|
163
|
+
# unpushed". A commit that never left the machine reads as pushed. `--unset-upstream` (not
|
|
164
|
+
# `remote remove`) keeps a remote present, so the all-branch scan still runs: this isolates the
|
|
165
|
+
# upstream leg instead of quietly testing two things at once.
|
|
166
|
+
T=$(_repo kp2_no_upstream); _artifacts "$T"
|
|
167
|
+
git -C "$T" branch --unset-upstream >/dev/null 2>&1
|
|
168
|
+
_run "$T"
|
|
169
|
+
_line "KP-2 no upstream → UNMEASURED, not zero" 'unpushed count is UNKNOWN' 1 "$OUT"
|
|
170
|
+
_line "KP-2 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
171
|
+
_rc "KP-2 → advisory, not blocking" "$RC" 0
|
|
172
|
+
|
|
173
|
+
# KP-3 git status itself fails — a corrupt index makes `status` exit non-zero with EMPTY output,
|
|
174
|
+
# and `| wc -l` renders that emptiness as "0 dirty paths" = clean.
|
|
175
|
+
T=$(_repo kp3_broken_index); _artifacts "$T"
|
|
176
|
+
printf 'garbage' > "$T/.git/index"
|
|
177
|
+
_run "$T"
|
|
178
|
+
_line "KP-3 corrupt index → cleanliness UNKNOWN" 'cleanliness is UNKNOWN' 1 "$OUT"
|
|
179
|
+
_line "KP-3 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
180
|
+
# Measured while writing this lane: a corrupt index makes `ls-files -v` exit 128 too, so the
|
|
181
|
+
# assume-unchanged probe (MASKED) silently reads 0 — the same not-found-≠-0 shape, one layer in.
|
|
182
|
+
# It is NOT a false all-clear (DIRTY_KNOWN=0 already suppresses the clean line), so it is recorded
|
|
183
|
+
# as a residual rather than patched here. `rev-parse @{u}` still exits 0 under a corrupt index,
|
|
184
|
+
# which is what keeps this lane measuring cleanliness and not accidentally re-measuring KP-2.
|
|
185
|
+
|
|
186
|
+
# KP-4 measured scope ≠ claimed scope — `@{u}..` reads the CURRENT branch only, while the message
|
|
187
|
+
# says "nothing unpushed" about the repo. An unpushed commit parked on another local branch is
|
|
188
|
+
# invisible. The current branch stays clean and pushed on purpose: only the other branch is dirty,
|
|
189
|
+
# so a green here would be the exact false all-clear.
|
|
190
|
+
T=$(_repo kp4_other_branch); _artifacts "$T"
|
|
191
|
+
(
|
|
192
|
+
cd "$T" || exit 1
|
|
193
|
+
git checkout -q -b side
|
|
194
|
+
echo side > side.txt && git add -A && git commit -qm side
|
|
195
|
+
git checkout -q -
|
|
196
|
+
) >/dev/null 2>&1
|
|
197
|
+
_run "$T"
|
|
198
|
+
_line "KP-4 unpushed on ANOTHER branch → ⚠️ fires" 'never pushed anywhere' 1 "$OUT"
|
|
199
|
+
_line "KP-4 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
200
|
+
|
|
201
|
+
# KP-5 `status.showUntrackedFiles=no` — git succeeds and stays SILENT (exit 0, empty output), so
|
|
202
|
+
# the exit-code guard of KP-3 cannot catch this one. Only `--untracked-files=all` overrides it.
|
|
203
|
+
T=$(_repo kp5_untracked_off); _artifacts "$T"
|
|
204
|
+
git -C "$T" config status.showUntrackedFiles no
|
|
205
|
+
echo hidden > "$T/hidden.txt"
|
|
206
|
+
_run "$T"
|
|
207
|
+
_line "KP-5 showUntrackedFiles=no → still counted" 'uncommitted path' 1 "$OUT"
|
|
208
|
+
_line "KP-5 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
209
|
+
|
|
210
|
+
# KP-6 assume-unchanged / skip-worktree — edits to a marked TRACKED file never reach porcelain at
|
|
211
|
+
# all, so `-uall` does not help either. A separate instrument (`ls-files -v`) has to surface it.
|
|
212
|
+
T=$(_repo kp6_assume_unchanged); _artifacts "$T"
|
|
213
|
+
git -C "$T" update-index --assume-unchanged unrelated.txt
|
|
214
|
+
echo edited >> "$T/unrelated.txt"
|
|
215
|
+
_run "$T"
|
|
216
|
+
_line "KP-6 assume-unchanged edit → surfaced" 'INVISIBLE here' 1 "$OUT"
|
|
217
|
+
_line "KP-6 → clean line absent (paired)" '✅ ① working tree clean' 0 "$OUT"
|
|
218
|
+
|
|
144
219
|
echo
|
|
145
220
|
echo "══ ①-b open-PR sweep ══"
|
|
146
221
|
_ghstub() { # $1=repo $2=stdout $3=exit
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Known pairs for scripts/version_lockstep_check.sh.
|
|
3
|
+
#
|
|
4
|
+
# The lane this suite exists to prevent: on 2026-08-06 a 1.4.88 → 1.4.89 bump edited the FIRST
|
|
5
|
+
# marketplace plugin entry and left the SECOND at the old version. `test_tag_version_lanes.sh`
|
|
6
|
+
# reported 8/8 PASS through it — that lane compares the git tag to package.json and never opens
|
|
7
|
+
# marketplace.json. The drift was caught by an ad-hoc `sort | uniq -c`. Luck is not a floor.
|
|
8
|
+
#
|
|
9
|
+
# Every lane runs against a THROWAWAY fixture tree, never against this repo, so the suite is
|
|
10
|
+
# hermetic: a real bump in progress can neither turn it green nor turn it red.
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
CHECK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/version_lockstep_check.sh"
|
|
14
|
+
TMPROOT=$(mktemp -d "${TMPDIR:-/tmp}/fh_lockstep.XXXXXX")
|
|
15
|
+
trap 'rm -rf "$TMPROOT"' EXIT
|
|
16
|
+
PASS=0; FAIL=0
|
|
17
|
+
|
|
18
|
+
_fixture() { # $1=name $2=pkg_ver $3=meta_ver $4=commons_ver $5=mk_entry0 $6=mk_entry1
|
|
19
|
+
local T="$TMPROOT/$1"
|
|
20
|
+
mkdir -p "$T/.claude-plugin" "$T/plugins/fh-meta/.claude-plugin" "$T/plugins/fh-commons/.claude-plugin"
|
|
21
|
+
printf '{"name":"fh","version":"%s"}\n' "$2" > "$T/package.json"
|
|
22
|
+
printf '{"name":"fh-meta","version":"%s"}\n' "$3" > "$T/plugins/fh-meta/.claude-plugin/plugin.json"
|
|
23
|
+
printf '{"name":"fh-commons","version":"%s"}\n' "$4" > "$T/plugins/fh-commons/.claude-plugin/plugin.json"
|
|
24
|
+
printf '{"plugins":[{"name":"fh-meta","version":"%s"},{"name":"fh-commons","version":"%s"}]}\n' \
|
|
25
|
+
"$5" "$6" > "$T/.claude-plugin/marketplace.json"
|
|
26
|
+
printf '%s' "$T"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_expect() { # $1=label $2=expected_rc $3=repo ; also captures OUT
|
|
30
|
+
OUT=$(bash "$CHECK" "$3" 2>&1); local rc=$?
|
|
31
|
+
if [ "$rc" = "$2" ]; then echo " ✅ $1 (rc=$rc)"; PASS=$((PASS+1))
|
|
32
|
+
else echo " ❌ $1 — rc=$rc, expected=$2"; printf '%s\n' "$OUT" | sed 's/^/ │ /'; FAIL=$((FAIL+1)); fi
|
|
33
|
+
}
|
|
34
|
+
_says() { # $1=label $2=pattern $3=expected_hit(1|0)
|
|
35
|
+
local h=0; printf '%s\n' "$OUT" | grep -q -- "$2" && h=1
|
|
36
|
+
if [ "$h" = "$3" ]; then echo " ✅ $1"; PASS=$((PASS+1))
|
|
37
|
+
else echo " ❌ $1 — hit=$h, expected=$3"; printf '%s\n' "$OUT" | sed 's/^/ │ /'; FAIL=$((FAIL+1)); fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
echo "══ version lockstep known pairs ══"
|
|
41
|
+
|
|
42
|
+
# KN — everything aligned. The control: without it, a checker that always reported DRIFT would
|
|
43
|
+
# still pass every positive lane below.
|
|
44
|
+
_expect "KN all five strings aligned → exit 0" 0 "$(_fixture kn 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)"
|
|
45
|
+
_says "KN → names the count it actually checked" "4 shipped version string" 1
|
|
46
|
+
|
|
47
|
+
# KP-1 — the measured miss, byte for byte: second marketplace entry left behind.
|
|
48
|
+
_expect "KP-1 second marketplace entry stale → exit 1" 1 "$(_fixture kp1 1.4.89 1.4.89 1.4.89 1.4.89 1.4.88)"
|
|
49
|
+
_says "KP-1 → names the offending entry by index+name" "plugins\[1\] fh-commons" 1
|
|
50
|
+
_says "KP-1 → reports both versions, not just 'mismatch'" "1.4.88 (package.json = 1.4.89)" 1
|
|
51
|
+
|
|
52
|
+
# KP-2 — a plugin.json left behind. This is the shape the tag lane also cannot see.
|
|
53
|
+
_expect "KP-2 plugin.json stale → exit 1" 1 "$(_fixture kp2 1.4.89 1.4.88 1.4.89 1.4.89 1.4.89)"
|
|
54
|
+
_says "KP-2 → names the file" "plugins/fh-meta/.claude-plugin/plugin.json" 1
|
|
55
|
+
|
|
56
|
+
# KP-3 — the reverse direction: package.json bumped alone. Same defect, opposite author error.
|
|
57
|
+
_expect "KP-3 only package.json bumped → exit 1" 1 "$(_fixture kp3 1.5.0 1.4.89 1.4.89 1.4.89 1.4.89)"
|
|
58
|
+
_says "KP-3 → all four downstream strings flagged" "4 of 4" 1
|
|
59
|
+
|
|
60
|
+
# HARNESS-ERROR — an instrument that could not look has not looked. These must be a THIRD state,
|
|
61
|
+
# never folded into PASS: this gate guards `npm publish`, an irreversible surface.
|
|
62
|
+
T=$(_fixture he1 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); rm "$T/.claude-plugin/marketplace.json" "$T"/plugins/*/.claude-plugin/plugin.json
|
|
63
|
+
_expect "HE-1 no manifests at all → exit 2, NOT 0" 2 "$T"
|
|
64
|
+
_says "HE-1 → says alignment is UNKNOWN, not aligned" "UNKNOWN, not aligned" 1
|
|
65
|
+
|
|
66
|
+
T=$(_fixture he2 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); printf '{ broken' > "$T/.claude-plugin/marketplace.json"
|
|
67
|
+
_expect "HE-2 unparseable manifest → exit 2, NOT 0" 2 "$T"
|
|
68
|
+
|
|
69
|
+
T=$(_fixture he3 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); rm "$T/package.json"
|
|
70
|
+
_expect "HE-3 missing package.json → exit 2, NOT 0" 2 "$T"
|
|
71
|
+
|
|
72
|
+
# A manifest present but carrying no version at all is absence, not agreement.
|
|
73
|
+
T=$(_fixture he4 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); printf '{"name":"fh-meta"}\n' > "$T/plugins/fh-meta/.claude-plugin/plugin.json"
|
|
74
|
+
_expect "HE-4 manifest with no version → exit 2, NOT 0" 2 "$T"
|
|
75
|
+
_says "HE-4 → says which file carries none" "carries no version string" 1
|
|
76
|
+
|
|
77
|
+
echo
|
|
78
|
+
echo "──────────────────────────────────────────────"
|
|
79
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
80
|
+
echo "VERSION-LOCKSTEP LANES: FAIL ($PASS pass · $FAIL fail)"; exit 1
|
|
81
|
+
fi
|
|
82
|
+
echo "VERSION-LOCKSTEP LANES: PASS ($PASS/$PASS)"
|
|
@@ -196,8 +196,12 @@ run_case "placeholder BEFORE real, one line → BLOCK" \
|
|
|
196
196
|
# Each of these previously passed at commit time while BLOCKING at publish time — the two copies
|
|
197
197
|
# of this logic had diverged in leniency. run_state_case swaps the pattern source rather than the
|
|
198
198
|
# staged content, so it needs its own runner.
|
|
199
|
-
run_state_case() { # <name> <override-content|__NONE__> <defaults:keep|drop> <expect-exit: block|pass>
|
|
200
|
-
|
|
199
|
+
run_state_case() { # <name> <override-content|__NONE__> <defaults:keep|drop> <expect-exit: block|pass> [expect-verdict-substring]
|
|
200
|
+
# The 5th arg exists because exit code alone cannot see this defect class: a scan whose
|
|
201
|
+
# operator-literal layer never ran exits 0 and used to print the same `✅ PASS` as a scan where
|
|
202
|
+
# every layer ran. Identical verdicts for different amounts of measurement is the fail-open —
|
|
203
|
+
# so the anchor has to read the verdict LINE, not just the status.
|
|
204
|
+
local name="$1" ovc="$2" defmode="$3" expect="$4" want="${5:-}" out rc got
|
|
201
205
|
printf 'operator literal zzsynthoperator\n' > "$SANDBOX/README.md"
|
|
202
206
|
git -C "$SANDBOX" add -- README.md >/dev/null 2>&1
|
|
203
207
|
local ov="$SANDBOX/.psa_state_override"
|
|
@@ -208,17 +212,26 @@ run_state_case() { # <name> <override-content|__NONE__> <defaults:keep|drop> <e
|
|
|
208
212
|
if [ "$defmode" = drop ]; then mv "$defbak" "$SANDBOX/.claude/rules/.public-surface-patterns.defaults" 2>/dev/null; fi
|
|
209
213
|
git -C "$SANDBOX" rm -q --cached -- README.md >/dev/null 2>&1; rm -f "$SANDBOX/README.md" "$ov"
|
|
210
214
|
if [ "$rc" -ne 0 ]; then got=block; else got=pass; fi
|
|
211
|
-
if [ "$got"
|
|
212
|
-
echo " ✅ $name (expected $expect)"
|
|
213
|
-
else
|
|
215
|
+
if [ "$got" != "$expect" ]; then
|
|
214
216
|
echo " ❌ $name — expected $expect, got $got (exit $rc)"
|
|
215
217
|
printf '%s\n' "$out" | sed 's/^/ | /' | head -10
|
|
216
218
|
FAILED=1
|
|
219
|
+
elif [ -n "$want" ] && ! printf '%s' "$out" | grep -qF "$want"; then
|
|
220
|
+
echo " ❌ $name — exit was $got but the verdict line did not say: $want"
|
|
221
|
+
printf '%s\n' "$out" | grep -E 'PASS|PARTIAL|Confidentiality' | sed 's/^/ | /' | head -5
|
|
222
|
+
FAILED=1
|
|
223
|
+
else
|
|
224
|
+
echo " ✅ $name (expected $expect${want:+, verdict names \"$want\"})"
|
|
217
225
|
fi
|
|
218
226
|
}
|
|
219
227
|
run_state_case "no patterns at all (instrument down) → BLOCK" "__NONE__" drop block
|
|
220
228
|
run_state_case "malformed regex in override → BLOCK" "HIGH zzsynth[" keep block
|
|
221
|
-
|
|
229
|
+
# 2026-08-06 — this state still PASSES (a commit is reversible; blocking every fresh clone's first
|
|
230
|
+
# commit is what trains the override into a reflex), but it may no longer CLAIM a clean scan. Pinned
|
|
231
|
+
# as a pair so the label cannot drift back to a bare PASS, and so an unconditional PARTIAL — which
|
|
232
|
+
# would be just as wrong — is caught by the control below it.
|
|
233
|
+
run_state_case "empty override + defaults present → PASS " "" keep pass "PARTIAL"
|
|
234
|
+
run_state_case "control: override PRESENT, no hit → PASS " "HIGH zzunrelatedliteral" keep pass "✅ PASS"
|
|
222
235
|
|
|
223
236
|
# ── Pair 7: pattern-ROW malformations that are not invalid regex. Each of these used to be skipped
|
|
224
237
|
# in silence, i.e. a detector the author believed in that never existed, and a scan that certified
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# version_lockstep_check.sh — every shipped manifest must carry package.json's version.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists (measured 2026-08-06, while republishing 1.4.88 → 1.4.89):
|
|
5
|
+
# CLAUDE.md §④-b says to bump "package.json + every .claude-plugin/plugin.json + marketplace.json".
|
|
6
|
+
# That instruction reads as four files, but `.claude-plugin/marketplace.json` carries **one version
|
|
7
|
+
# per plugin entry** — five strings across four files. A bump that edited the first entry left the
|
|
8
|
+
# second at the old version, and `test_tag_version_lanes.sh` passed 8/8 anyway: that lane compares
|
|
9
|
+
# the git TAG to package.json and never opens marketplace.json. The stale entry was caught by an
|
|
10
|
+
# ad-hoc `sort | uniq -c`, i.e. by luck, not by an instrument.
|
|
11
|
+
#
|
|
12
|
+
# Codex caches plugins on the plugin.json version, so a stale entry ships a plugin that the other
|
|
13
|
+
# runtime believes it already has — the drift is silent on exactly the surface that cannot report it.
|
|
14
|
+
#
|
|
15
|
+
# Exit 0 = all aligned · exit 1 = drift (names every offending file:line) · exit 2 = harness error
|
|
16
|
+
# (a manifest is missing or unparseable — NOT a pass; an instrument that could not look has not
|
|
17
|
+
# looked, and this gate guards an irreversible surface).
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
21
|
+
|
|
22
|
+
command -v python3 >/dev/null 2>&1 || { echo "LOCKSTEP: HARNESS-ERROR — python3 unavailable"; exit 2; }
|
|
23
|
+
|
|
24
|
+
python3 - "$ROOT" <<'PY'
|
|
25
|
+
import json, sys, glob, os
|
|
26
|
+
root = sys.argv[1]
|
|
27
|
+
pkg_path = os.path.join(root, 'package.json')
|
|
28
|
+
try:
|
|
29
|
+
want = json.load(open(pkg_path))['version']
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"LOCKSTEP: HARNESS-ERROR — cannot read {pkg_path}: {e}")
|
|
32
|
+
sys.exit(2)
|
|
33
|
+
|
|
34
|
+
# (path, list-of-(label, version)) — every manifest that ships a version string.
|
|
35
|
+
targets = []
|
|
36
|
+
for p in sorted(glob.glob(os.path.join(root, 'plugins', '*', '.claude-plugin', 'plugin.json'))):
|
|
37
|
+
targets.append(p)
|
|
38
|
+
mk = os.path.join(root, '.claude-plugin', 'marketplace.json')
|
|
39
|
+
if os.path.exists(mk):
|
|
40
|
+
targets.append(mk)
|
|
41
|
+
|
|
42
|
+
if not targets:
|
|
43
|
+
# Absence is not alignment. If the manifests vanished, say so rather than reporting a clean run.
|
|
44
|
+
print("LOCKSTEP: HARNESS-ERROR — no plugin/marketplace manifest found; alignment is UNKNOWN, not aligned")
|
|
45
|
+
sys.exit(2)
|
|
46
|
+
|
|
47
|
+
drift, checked = [], 0
|
|
48
|
+
for p in targets:
|
|
49
|
+
try:
|
|
50
|
+
d = json.load(open(p))
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"LOCKSTEP: HARNESS-ERROR — cannot parse {p}: {e}")
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
rel = os.path.relpath(p, root)
|
|
55
|
+
# A manifest carries a version either at the top level (plugin.json) or once per plugin entry
|
|
56
|
+
# (marketplace.json). Walk both shapes so a new entry cannot be silently uncovered.
|
|
57
|
+
found = []
|
|
58
|
+
if isinstance(d.get('version'), str):
|
|
59
|
+
found.append(('(top-level)', d['version']))
|
|
60
|
+
for key in ('plugins', 'entries'):
|
|
61
|
+
for i, ent in enumerate(d.get(key) or []):
|
|
62
|
+
if isinstance(ent, dict) and isinstance(ent.get('version'), str):
|
|
63
|
+
found.append((f"{key}[{i}] {ent.get('name', '?')}", ent['version']))
|
|
64
|
+
if not found:
|
|
65
|
+
print(f"LOCKSTEP: HARNESS-ERROR — {rel} carries no version string; expected at least one")
|
|
66
|
+
sys.exit(2)
|
|
67
|
+
for label, got in found:
|
|
68
|
+
checked += 1
|
|
69
|
+
if got != want:
|
|
70
|
+
drift.append(f" {rel} :: {label} = {got} (package.json = {want})")
|
|
71
|
+
|
|
72
|
+
if drift:
|
|
73
|
+
print(f"LOCKSTEP: DRIFT — {len(drift)} of {checked} shipped version string(s) do not match package.json")
|
|
74
|
+
print("\n".join(drift))
|
|
75
|
+
print("Bump every string above in the same commit — Codex caches plugins on this version,")
|
|
76
|
+
print("so a stale entry ships as 'already installed' on the runtime that cannot report it.")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
print(f"LOCKSTEP: PASS — {checked} shipped version string(s) all at {want}")
|
|
80
|
+
PY
|
|
@@ -303,6 +303,14 @@ else
|
|
|
303
303
|
echo " → '~' or '{project}'), or PUBLIC_SURFACE_OK=1 git commit … for a reviewed mention."
|
|
304
304
|
FAILED=1
|
|
305
305
|
fi
|
|
306
|
+
elif [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
|
|
307
|
+
# NOT `✅ PASS`. The scan found nothing, but one whole layer of it — the operator/company
|
|
308
|
+
# literals — never ran. A missing measurement is not a zero (CLAUDE.md §Instrument
|
|
309
|
+
# Calibration), and a verdict that reads identically whether a layer ran or not is the
|
|
310
|
+
# fail-open this repo names. The commit is still allowed: a commit is reversible, so the
|
|
311
|
+
# degrade here is advisory by the Surface-Class Degrade Invariant. Push and publish, which
|
|
312
|
+
# are the acts that make content public, block this same state.
|
|
313
|
+
echo " ⚠️ PARTIAL — no hit in the committed defaults; company/companion literals UNMEASURED."
|
|
306
314
|
else
|
|
307
315
|
echo " ✅ PASS"
|
|
308
316
|
fi
|
|
@@ -298,9 +298,21 @@ else
|
|
|
298
298
|
_pp_why=""
|
|
299
299
|
[ "$_PP_APPLICABLE" -eq 1 ] && [ "$PSA_DEFAULTS_OK" -eq 0 ] && _pp_why="committed pattern defaults unreadable/empty"
|
|
300
300
|
[ "$_PP_APPLICABLE" -eq 1 ] && [ "$PSA_BAD_ROWS" -gt 0 ] && _pp_why="${_pp_why:+$_pp_why; }$PSA_BAD_ROWS unusable pattern row(s)"
|
|
301
|
+
# 2026-08-06 — the 07-26 warn stays for the case it was right about, and is SCOPED, not reverted.
|
|
302
|
+
# An absent override is a legitimate configuration gap in a fresh clone / CI runner / worktree (the
|
|
303
|
+
# file is gitignored, so it is absent there by construction) → WARN, exactly as 07-26 concluded.
|
|
304
|
+
# In an operator-configured checkout the same state is missing evidence, not a missing config, and
|
|
305
|
+
# this is an irreversible surface → BLOCK, matching what `public_surface_scan_files.sh` already does
|
|
306
|
+
# at `npm publish`. Two irreversible surfaces degrading in opposite directions on the same state was
|
|
307
|
+
# the actual defect; git push was the lenient one.
|
|
308
|
+
psa_detect_operator_context "$REPO_ROOT"
|
|
301
309
|
if [ "$_PP_APPLICABLE" -eq 1 ] && [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
if [ "$PSA_OPERATOR_CONTEXT" -eq 1 ]; then
|
|
311
|
+
_pp_why="${_pp_why:+$_pp_why; }operator-literal override absent in an operator-configured checkout (company/companion literals NOT scanned)"
|
|
312
|
+
else
|
|
313
|
+
echo " ⚠️ operator-literal override absent — only the committed defaults (home paths + credential"
|
|
314
|
+
echo " shapes) are active. Populate .claude/rules/.public-surface-patterns for company literals."
|
|
315
|
+
fi
|
|
304
316
|
fi
|
|
305
317
|
if [ -n "$_pp_why" ]; then
|
|
306
318
|
if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
|