@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.
- package/.claude/rules/fh_4axis_gate.md +63 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +96 -260
- package/CLAUDE.md +2 -7
- package/docs/codex-compat.md +4 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +233 -0
- package/knowledge/shared/harness-core/loop_engineering.md +1 -1
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +14 -0
- package/knowledge/shared/rules/operational_adaptation.md +1 -130
- package/package.json +14 -5
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +88 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +117 -3
- package/scripts/fh_node_check.sh +184 -0
- package/scripts/fh_session_load.sh +101 -31
- package/scripts/halffix_propagation_scan.sh +126 -0
- package/scripts/package_coverage_check.sh +40 -1
- package/scripts/pipe_verdict_guard.sh +93 -0
- package/scripts/selfcheck.sh +114 -21
- package/scripts/session_close_check.sh +15 -1
- package/scripts/sidecar_calibrate.sh +275 -0
- package/scripts/test_card_drift_probe.sh +55 -0
- package/scripts/test_halffix_lanes.sh +170 -0
- package/scripts/test_node_check_lanes.sh +179 -0
- package/scripts/test_ollama_panel_lanes.sh +120 -0
- package/scripts/test_package_coverage_lanes.sh +250 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +96 -0
- package/scripts/test_sidecar_calibrate_lanes.sh +218 -0
- package/scripts/test_sidecar_wait_stdin.sh +13 -1
- package/templates/.git-hooks/pre-commit +9 -0
- package/templates/settings.PreToolUse.snippet.json +49 -0
- package/templates/settings.SessionStart.snippet.json +54 -0
- package/scripts/consent_registry_check.sh +0 -390
- package/scripts/test_consent_registry.sh +0 -255
- 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."
|