@chrono-meta/fh-gate 1.4.77 → 1.4.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.claude/rules/fh_4axis_gate.md +63 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/AGENTS.md +96 -260
  4. package/CLAUDE.md +2 -7
  5. package/docs/codex-compat.md +4 -1
  6. package/knowledge/shared/harness-core/agents_md_runtime_details.md +233 -0
  7. package/knowledge/shared/harness-core/loop_engineering.md +1 -1
  8. package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +1 -1
  9. package/knowledge/shared/learnings/subagent_invocations_log.yaml +14 -0
  10. package/knowledge/shared/rules/operational_adaptation.md +1 -130
  11. package/package.json +14 -5
  12. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  13. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  14. package/plugins/fh-meta/skills/install-doctor/SKILL.md +88 -0
  15. package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
  16. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +117 -3
  17. package/scripts/fh_node_check.sh +184 -0
  18. package/scripts/fh_session_load.sh +101 -31
  19. package/scripts/halffix_propagation_scan.sh +126 -0
  20. package/scripts/package_coverage_check.sh +40 -1
  21. package/scripts/pipe_verdict_guard.sh +93 -0
  22. package/scripts/selfcheck.sh +114 -21
  23. package/scripts/session_close_check.sh +15 -1
  24. package/scripts/sidecar_calibrate.sh +275 -0
  25. package/scripts/test_card_drift_probe.sh +55 -0
  26. package/scripts/test_halffix_lanes.sh +170 -0
  27. package/scripts/test_node_check_lanes.sh +179 -0
  28. package/scripts/test_ollama_panel_lanes.sh +120 -0
  29. package/scripts/test_package_coverage_lanes.sh +250 -0
  30. package/scripts/test_pipe_verdict_guard_lanes.sh +96 -0
  31. package/scripts/test_sidecar_calibrate_lanes.sh +218 -0
  32. package/scripts/test_sidecar_wait_stdin.sh +13 -1
  33. package/templates/.git-hooks/pre-commit +9 -0
  34. package/templates/settings.PreToolUse.snippet.json +49 -0
  35. package/templates/settings.SessionStart.snippet.json +54 -0
  36. package/scripts/consent_registry_check.sh +0 -390
  37. package/scripts/test_consent_registry.sh +0 -255
  38. package/templates/consent_classes.yaml.example +0 -75
@@ -1,255 +0,0 @@
1
- #!/usr/bin/env bash
2
- # test_consent_registry.sh — known-pair anchor for scripts/consent_registry_check.sh.
3
- #
4
- # WHY (cross-family review rounds 1-3, 2026-07-29)
5
- # The accept-side consent-promotion rule went REJECT -> NARROW-IT -> NARROW-IT across three
6
- # adversarial rounds. Round 3 stopped attacking the prose and attacked the validator, and found
7
- # four ways the FLOOR ITSELF was fail-open. Three were confirmed against controls:
8
- #
9
- # - `promotion_eligible: "false"` (quoted) is a truthy STRING. Every eligibility test inverted,
10
- # so an intended-ineligible class was granted. One quote character disarmed the floor.
11
- # - a duplicate class name silently shadowed the earlier entry, so appending an eligible twin
12
- # below an ineligible one laundered the ineligible class into a grant.
13
- # - `standing_consent: {inline: ...}` matched no pattern, so an EXPIRED inline grant was read as
14
- # "no standing consent recorded" and reported PASS — a grant the checker cannot see is not an
15
- # absent grant, it is a false clean.
16
- #
17
- # Each of those is a check that reported green while doing nothing, which is worse than an absent
18
- # check: it buys confidence without enforcement. Hence this anchor.
19
- #
20
- # Lanes: N* = known-negative (must pass) · P* = known-positive (must be caught) · D* = degrade.
21
- # Exit 0 = all lanes correct. Exit 1 = the validator regressed toward permissiveness.
22
- set -uo pipefail
23
- ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24
- CHK="$ROOT/scripts/consent_registry_check.sh"
25
- [ -f "$CHK" ] || { echo "FAIL: $CHK not found"; exit 1; }
26
-
27
- pass=0; fail=0
28
- ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
29
- bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
30
- TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
31
-
32
- # want_rc: expected exit. want_rule: a rule id that must appear in the output ("" = don't care).
33
- lane() {
34
- local desc="$1" want_rc="$2" want_rule="$3"
35
- bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
36
- local rc=$?
37
- if [ "$rc" -ne "$want_rc" ]; then
38
- bad "$desc — exit $rc, expected $want_rc"; sed 's/^/ /' "$TD/o"; return
39
- fi
40
- if [ -n "$want_rule" ] && ! grep -q "❌ $want_rule" "$TD/o"; then
41
- bad "$desc — exit was right but rule $want_rule was not the reason"; sed 's/^/ /' "$TD/o"; return
42
- fi
43
- ok "$desc"
44
- }
45
-
46
- OKCLASS=' - {name: ok, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
47
- mkreg() { printf 'classes:\n%s\n' "$1" > "$TD/r.yaml"; }
48
- mkuap() { printf '%s\n' "$1" > "$TD/u.md"; }
49
-
50
- mkreg "$OKCLASS"
51
- mkuap 'standing_consent:
52
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
53
- lane "N1 a well-formed registry + eligible unexpired scoped grant passes" 0 ""
54
-
55
- mkuap 'standing_consent:
56
- ok: declined'
57
- lane "N2 a declined record is a valid non-grant state, not a malformed grant" 0 ""
58
-
59
- mkreg ' - {name: s, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: true}'
60
- mkuap 'standing_consent: {}'
61
- lane "P1 a class naming an irreversible SINK cannot declare itself promotable" 1 "R2"
62
-
63
- mkreg ' - {name: f, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [go-public], promotion_eligible: true}'
64
- lane "P2 taint — a class that FEEDS an irreversible sink cannot be promotable" 1 "R2"
65
-
66
- mkreg "$OKCLASS"
67
- mkuap 'standing_consent:
68
- ghost: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
69
- lane "P3 a grant on an unregistered class is refused (unregistered == unknown)" 1 "R3"
70
-
71
- mkuap 'standing_consent:
72
- ok: {granted: 2026-01-01, expires: 2026-06-30, effects: [read], target: t}'
73
- lane "P4 an expired lease does not keep running" 1 "R5"
74
-
75
- mkuap 'standing_consent:
76
- ok: {granted: 2026-07-29, expires: 2026-12-31}'
77
- lane "P5 a grant with no recorded scope has no re-validation baseline" 1 "R6"
78
-
79
- # P6-P8 are the round-3 fail-opens. Each of these PASSED before the fix.
80
- mkreg ' - {name: q, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: "false"}'
81
- mkuap 'standing_consent:
82
- q: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
83
- lane "P6 quoted \"false\" is rejected as a type error, not read as truthy" 1 "R1-b"
84
-
85
- mkreg ' - {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: false}
86
- - {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
87
- mkuap 'standing_consent:
88
- d: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
89
- lane "P7 a duplicate class name cannot launder an ineligible class" 1 "R1-c"
90
-
91
- mkreg "$OKCLASS"
92
- mkuap 'standing_consent: {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
93
- lane "P8 an INLINE grant is parsed, not silently read as 'nothing granted'" 1 "R5"
94
-
95
- # P10-P11 are the round-4 fail-opens. Both PASSED before the fix, and both were confirmed against a
96
- # control: the SAME expired grant was caught when it stood alone.
97
- mkreg "$OKCLASS"
98
- mkuap 'standing_consent: {}
99
-
100
- notes in between
101
-
102
- standing_consent:
103
- ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
104
- lane "P10 an early empty consent block cannot shadow a later grant (first-match)" 1 "R3"
105
-
106
- mkuap 'standing_consent:
107
- ok: {state: "revoked ", granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
108
- if bash "$CHK" "$TD/r.yaml" "$TD/u.md" 2>&1 | grep -q "0 active grant"; then
109
- ok "P11 a mapping non-grant state is normalized like the scalar one (no divergent normalizer)"
110
- else
111
- bad "P11 \`state: \"revoked \"\` was validated as an ACTIVE grant"
112
- bash "$CHK" "$TD/r.yaml" "$TD/u.md" 2>&1 | sed 's/^/ /'
113
- fi
114
-
115
- # P12 is the round-5 fail-open: `or {}` collapsed FALSY non-mappings into a valid-empty mapping
116
- # before the type check, so `[]`, `false` and `0` all reported "no standing consent" and exit 0 —
117
- # while the truthy `[a, b]` was caught. The falsy branch is the one an accident actually produces.
118
- for badval in ' []' ' false' ' 0'; do
119
- mkreg "$OKCLASS"
120
- printf 'standing_consent:\n%s\n' "$badval" > "$TD/u.md"
121
- lane "P12 a falsy non-mapping standing_consent ($badval) is a type error, not 'none granted'" 1 "R3"
122
- done
123
-
124
- # P13/N3 — round-6 fail-open: `standing_consent :` (space or tab before the colon) is the SAME YAML
125
- # key but matched none of the patterns. Standalone it still failed closed via the no-known-form net;
126
- # paired with a normal empty block it was invisible to both the count and the extraction, so the
127
- # empty block parsed and the real grant vanished. Confirmed against a control (the identical expired
128
- # grant in canonical form was caught).
129
- mkreg "$OKCLASS"
130
- mkuap 'standing_consent: {}
131
-
132
- standing_consent : {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
133
- lane "P13 a space-before-colon key cannot hide behind a canonical block" 1 "R3"
134
-
135
- mkuap 'standing_consent :
136
- ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
137
- lane "P13-b a space-before-colon BLOCK form is parsed and its grant validated" 1 "R5"
138
-
139
- mkuap "$(printf 'standing_consent\t: {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}')"
140
- lane "N3 a tab-before-colon key with a VALID grant passes (no over-block)" 0 ""
141
-
142
- # P14 — round-7 fail-open: R6 presence-checked `effects`/`target` but never typed them, while the
143
- # registry side had strict types since R1-b. A fix propagated to one side only is a hole. Control:
144
- # a MISSING field was caught; a type-wrong one passed.
145
- mkreg "$OKCLASS"
146
- mkuap 'standing_consent:
147
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: true, target: 123}'
148
- lane "P14 a type-wrong grant scope is rejected, not counted as recorded" 1 "R6"
149
-
150
- mkuap 'standing_consent:
151
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: "read", target: " "}'
152
- lane "P14-b a scalar effects / whitespace-only target is rejected" 1 "R6"
153
-
154
- mkuap 'standing_consent:
155
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [], target: t}'
156
- lane "P14-c an EMPTY effects list is not a baseline" 1 "R6"
157
-
158
- # H1-H9 — round-8 exhaustive field audit found NINE remaining holes in one pass, after four rounds
159
- # of one-per-round trickle. The reframe ("audit every field, don't hand me the most important one")
160
- # is what surfaced them; the per-round drip was a slow enumeration, not convergence.
161
- mkuap 'standing_consent: {}'
162
- printf 'classes: false\n' > "$TD/r.yaml"
163
- lane "H1 a falsy \`classes\` is not laundered into an empty registry" 1 ""
164
- printf 'classes:\n' > "$TD/r.yaml"
165
- lane "H1-b a null \`classes\` is N/A, not a clean PASS" 0 ""
166
- if grep -q 'N/A' "$TD/o"; then ok "H1-c the empty registry is LABELLED N/A"; else bad "H1-c empty registry read as clean"; fi
167
-
168
- mkreg ' - {name: n, owner: o, mode: 123, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
169
- lane "H2 a non-string scalar registry field is a type error" 1 "R1-b"
170
- mkreg ' - {name: n, owner: o, mode: m, target: "", capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
171
- lane "H2-b a blank registry target is a type error" 1 "R1-b"
172
- mkreg ' - {name: n, owner: o, mode: m, target: t, capabilities: [read], sinks: [123], feeds: [], promotion_eligible: false}'
173
- lane "H3 an unreadable sink item is UNDECLARED, not an empty sink list" 1 "R1-b"
174
-
175
- mkreg "$OKCLASS"
176
- mkuap 'standing_consent:
177
- ok: {expires: 2026-12-31, effects: [read], target: t}'
178
- lane "H5 a grant with no \`granted\` date cannot be audited" 1 "R5"
179
- mkuap 'standing_consent:
180
- ok: {granted: 2026-07-29, expires: 29991231, effects: [read], target: t}'
181
- lane "H7 a non-ISO integer expiry is not a date" 1 "R5"
182
- mkuap 'standing_consent:
183
- ok: {granted: 2026-07-29, expires: 9999-12-31, effects: [read], target: t}'
184
- lane "H7-b an unbounded lease is a transfer wearing a lease's clothes" 1 "R5"
185
- mkuap 'standing_consent:
186
- ok: {state: revokedd, granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
187
- lane "H6 a typo'\''d state fails closed instead of passing as active" 1 "R3"
188
- mkuap 'standing_consent:
189
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [repo-mutation], target: t}'
190
- lane "H8 a grant wider than its registered capabilities is refused" 1 "R7"
191
- mkuap 'standing_consent:
192
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: everything}'
193
- lane "H8-b grant/class target drift is refused" 1 "R7"
194
- mkuap 'standing_consent:
195
- ok: {granted: 2026-07-29, expires: 2020-01-01, effects: [read], target: t}
196
- ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
197
- lane "H9 duplicate YAML keys are rejected, not resolved last-wins" 1 ""
198
-
199
- # H4 had a fix but NO lane — the mutation sweep caught that (disabling it failed zero lanes).
200
- # An uncovered check is a check that gets deleted quietly later.
201
- # NOTE the assertion is on the MESSAGE, not the rule id: with the type check disabled a numeric key
202
- # still exits 1 via "not in the registry", so a rule-id assertion passed either way and the mutation
203
- # sweep caught zero lanes. A lane that cannot separate two paths is not measuring the one it names.
204
- mkuap 'standing_consent:
205
- 123: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
206
- bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
207
- if [ $? -eq 1 ] && grep -q 'grant key 123 must be a non-blank string' "$TD/o"; then
208
- ok "H4 a non-string grant key is refused AS A TYPE ERROR (not merely as unregistered)"
209
- else
210
- bad "H4 numeric grant key was not refused by the type check"
211
- sed 's/^/ /' "$TD/o"
212
- fi
213
-
214
- mkreg ' - {name: c, owner: o, mode: m, target: t, capabilities: "read", sinks: [], feeds: [], promotion_eligible: true}'
215
- mkuap 'standing_consent: {}'
216
- lane "P9 a scalar where a list is required is a type error" 1 "R1-b"
217
-
218
- # N4 — PINS A KNOWN OVER-BLOCK, it does not endorse it. A grant written with a YAML merge key is
219
- # currently REFUSED because the anchor lives outside the extracted fragment (see the residual note
220
- # in consent_registry_check.sh). This lane asserts the CURRENT behaviour so that a future fix shows
221
- # up as a lane change rather than being rediscovered from scratch — and so nobody reads the 40/40
222
- # as "no known over-block".
223
- mkreg "$OKCLASS"
224
- mkuap 'defaults: &d {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}
225
- standing_consent:
226
- <<: *d'
227
- bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
228
- if [ $? -eq 1 ]; then
229
- ok "N4 merge-key grant is refused — KNOWN OVER-BLOCK, pinned not endorsed (see residual note)"
230
- else
231
- ok "N4 merge-key grant now PASSES — the over-block was fixed; update this lane and the residual note"
232
- fi
233
-
234
- rm -f "$TD/r.yaml"
235
- lane "D1 a missing registry is N/A with promotion disabled (never a silent PASS)" 0 ""
236
- if grep -q 'N/A' "$TD/o"; then
237
- ok "D1-b the missing-registry result is LABELLED N/A, not reported as clean"
238
- else
239
- bad "D1-b a missing registry produced exit 0 without an N/A label — that reads as PASS"
240
- fi
241
-
242
- printf 'classes: [ {name: broken\n' > "$TD/r.yaml"
243
- lane "D2 an unparseable registry fails closed (cannot decide == not allowed)" 1 ""
244
-
245
- # The shipped example must satisfy the validator — otherwise the artifact FH hands users is the
246
- # first counter-example. (Hand-verify-one-sample discipline, applied to our own template.)
247
- if bash "$CHK" "$ROOT/templates/consent_classes.yaml.example" /dev/null >/dev/null 2>&1; then
248
- ok "S1 the shipped templates/consent_classes.yaml.example validates"
249
- else
250
- bad "S1 the shipped example registry does NOT validate"
251
- fi
252
-
253
- echo "----"
254
- echo "consent-registry anchor: $pass passed, $fail failed"
255
- [ "$fail" -eq 0 ] || exit 1
@@ -1,75 +0,0 @@
1
- # consent_classes.yaml — the consent-class registry.
2
- #
3
- # Copy to `tracks/_meta/consent_classes.yaml` (gitignored, per-environment) to enable
4
- # accept-side consent promotion. See `knowledge/shared/rules/operational_adaptation.md`
5
- # §Consent promotion.
6
- #
7
- # WHY A REGISTRY AT ALL (cross-family review, 2026-07-29)
8
- # The first draft let the running session name the class it was counting approvals against.
9
- # That defeats the class-inflation guard before it fires: the class does not need to be WIDENED
10
- # later if it was born wide. Recording three approvals as `sim-conductor` instead of
11
- # `dispatch read-only sim on a local artifact` silently grants every future sim-conductor mode.
12
- # So classes are DECLARED here, reviewed by a human, and the session may only count against
13
- # an entry that already exists. A session may propose a new entry; it may not mint one mid-run.
14
- #
15
- # THE FLOOR IS READ FROM THIS FILE, NOT JUDGED AT RUNTIME
16
- # `sinks` is the field the irreversibility floor is decided from. A session asking itself
17
- # "is this reversible?" is the wrong arbiter — it is the party that wants to stop being asked.
18
- # Absent entry / absent file / unparseable entry => NO promotion, keep asking. Unknown is not
19
- # reversible.
20
- #
21
- # TAINT
22
- # A class inherits the floor if it FEEDS an irreversible sink, not only if it crosses one.
23
- # `local-commit` below is the worked example: committing is locally reversible, but if a
24
- # post-commit hook, sync daemon, or CI workflow publishes from that commit, the chain crossed
25
- # go-public and the class must be marked tainted. Declare `feeds:` honestly — the whole point
26
- # is that no single step in a bad chain looks dangerous on its own.
27
-
28
- classes:
29
- # ---- promotion-eligible example -------------------------------------------------
30
- - name: dispatch-readonly-sim-local-artifact
31
- owner: fh-meta:sim-conductor # the gate/skill that does the asking
32
- mode: blind-target-tier-sim
33
- target: a single local file under the current repo
34
- capabilities: [read, dispatch] # read · local-write · network · dispatch · repo-mutation
35
- sinks: [] # crosses nothing irreversible
36
- feeds: [] # enables/queues nothing irreversible
37
- promotion_eligible: true
38
- lease_days: 30 # standing consent is a renewable lease, never permanent
39
-
40
- # ---- NOT promotion-eligible: sink is irreversible --------------------------------
41
- - name: npm-publish
42
- owner: Pre-Publish Surface Gate
43
- mode: registry-publish
44
- target: the package tarball file set
45
- capabilities: [read, network]
46
- sinks: [go-public] # irreversible => never promotes, at any count
47
- feeds: []
48
- promotion_eligible: false
49
- reason: "publish is effectively irreversible — a package is live the instant it ships and may
50
- be cached or forked before any scrub (CLAUDE.md §Pre-Publish Surface Gate)."
51
-
52
- # ---- NOT promotion-eligible: TAINTED (this is the one that looks safe) -----------
53
- - name: local-commit
54
- owner: FH 4-axis commit gate
55
- mode: git-commit
56
- target: the working tree
57
- capabilities: [local-write, repo-mutation]
58
- sinks: [] # committing itself is reversible
59
- feeds: [go-public] # ...but CI/sync publishes from it
60
- promotion_eligible: false
61
- reason: "taint — reversible in isolation, feeds an irreversible sink. Every step in the
62
- workflow-edit -> release-notes -> commit -> CI-publish chain is individually
63
- reversible; the chain is not."
64
-
65
- # ---- NOT promotion-eligible: unclassified ---------------------------------------
66
- - name: run-arbitrary-shell
67
- owner: (none — catch-all)
68
- mode: unrestricted
69
- target: unbounded
70
- capabilities: [read, local-write, network, dispatch, repo-mutation]
71
- sinks: [unknown] # unknown is NOT reversible
72
- feeds: [unknown]
73
- promotion_eligible: false
74
- reason: "sinks cannot be enumerated, so the floor cannot be decided mechanically. A class
75
- whose blast radius is unbounded is never a candidate, regardless of approval count."