@ssheleg/sheleg-dev 0.6.0 → 0.8.0

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/CHANGELOG.md +318 -0
  2. package/README.md +115 -1
  3. package/SECURITY.md +199 -52
  4. package/bin/sheleg-dev.js +18 -0
  5. package/package.json +2 -2
  6. package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
  7. package/plugins/sheleg-dev/hooks/hooks.json +29 -0
  8. package/plugins/sheleg-dev/hooks/lib/moneygate.js +587 -0
  9. package/plugins/sheleg-dev/hooks/money-gate.js +68 -0
  10. package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +7 -3
  11. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/README.md +77 -0
  12. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/assert-dedup-contract.mjs +397 -0
  13. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/manifest.json +75 -0
  14. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-capi-from-thank-you-page.json +32 -0
  15. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-capi-server.json +26 -0
  16. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-pixel-browser.json +12 -0
  17. package/plugins/sheleg-dev/skills/ad-tracking/fixtures/reference-emitter.mjs +129 -0
  18. package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +17 -1
  19. package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +82 -77
  20. package/plugins/sheleg-dev/skills/crypto-payments/references/callback-route-hardening.md +55 -0
  21. package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +349 -10
  22. package/plugins/sheleg-dev/skills/crypto-payments/references/testing-and-local-dev.md +60 -0
  23. package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +1 -1
  24. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/README.md +111 -0
  25. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/assert-money-invariants.mjs +564 -0
  26. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/charge-refunded-partial.json +52 -0
  27. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/charge-refunded-remainder.json +63 -0
  28. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-async-payment-failed.json +43 -0
  29. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-completed-async-unpaid.json +43 -0
  30. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-completed-paid.json +42 -0
  31. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle-next-period.json +70 -0
  32. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle-redelivery.json +70 -0
  33. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle.json +61 -0
  34. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-update-proration.json +90 -0
  35. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/manifest.json +172 -0
  36. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/reference-handler.mjs +315 -0
  37. package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +39 -2
  38. package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +43 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,324 @@ All notable changes to this project are documented here.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.8.0 — 2026-08-20
8
+
9
+ **The self-test that proved the money invariants was measuring one thing and claiming
10
+ another.** `--self-test` deleted a rule, ran an invariant, and asked whether the invariant
11
+ went red. Twelve invariants carried **45 `assert.` calls**, and the verdict line called
12
+ them *"12 assertions, each watched failing"* — so any assertion that was not the sole
13
+ discriminator of a rule could be replaced by `assert.ok(true)` and nothing anywhere would
14
+ notice. Measured against the shipped v0.7.0 pack: three neutered assertions (the
15
+ `clawbacks.map` deepEqual, the `mirror.periodEnd` equality, the `response.body` deepEqual)
16
+ left `--self-test` at exit 0 and `npm test` at 0. The same hole in `ad-tracking`, in the PII
17
+ guard `references/meta-linkedin.md` advertises by name.
18
+
19
+ The `assert` an invariant receives is now a recorder: a failure is remembered instead of
20
+ thrown, so the assertions after it still run, and each call site is one measurement
21
+ identified by its line. **37 of 45 watched failing, one call site at a time. Eight declared
22
+ `assert.unmutated`** — an assertion no rule in the pack varies — **and measured unbreakable,
23
+ with the reason at the call site.** Both directions: an `unmutated` assertion a mutant does
24
+ break is a failure too, or the escape hatch is the bypass. Four assertions that were being
25
+ pre-empted by a `TypeError` on the line above them became measurable rather than declared
26
+ away. `test/fixtures_test.js` grew three plants, one of them the NON-first assertion inside
27
+ a multi-assert invariant — the plant that used to pass.
28
+
29
+ Then the sweep that started there and did not stop:
30
+
31
+ - **The validator's check count did not count checks.** `checks = 10 + len(skill_dirs)`, so
32
+ adding a skill moved the number and adding a check did not — and five ledger rows read it
33
+ as evidence a guard had arrived. Every check is registered; the verdict prints the
34
+ registry's length, and the ledger's quoted copy of that line is compared against it.
35
+ - **The ledger described an artifact nobody ships.** Its shipped block was headed `v0.6.0`
36
+ while `v0.7.0` was tagged and on npm, and 37 rows read *verified locally · unreleased*
37
+ under four blocks closing *"Nothing was released."* The heading is now compared against
38
+ `git describe --tags` and against `package.json`.
39
+ - **The `mcp__.*` half of the manual gate had no guard.** Deleting that entry from
40
+ `hooks/hooks.json` left all three suites green, while the decision module does refuse
41
+ `mcp__…__create_refund` by name. And `README.md`'s copy-channel snippet registered one
42
+ matcher against the plugin's two — a weaker gate handed out by the document that exists
43
+ because the channel has none. Both are now checked; the snippet by matcher set.
44
+ - **This repository measured no body budget at all**, so `crypto-payments/SKILL.md` sat past
45
+ the house working limit and was caught by another repository's auditor. Measured here now,
46
+ and the skill was **split rather than trimmed**: `references/callback-route-hardening.md`
47
+ and `references/testing-and-local-dev.md`, 4894 → 4387 tokens. All six skills `0 GAP`.
48
+ - **Counted numbers became computed ones.** Seven in `SECURITY.md` (all four correct on the
49
+ day they were written, three of them moved by this change) and two in
50
+ `docs/evals/stripe-billing.md`, which restated `4994` tokens / `441` lines / `0 GAP, 13
51
+ PASS` against a measured 4747 / 409 / `0 GAP, 14 PASS`.
52
+ - **A document that ships now resolves against the tarball, not only against a clone.**
53
+ `SECURITY.md` sent a reader to two documents `npm pack` does not contain.
54
+ - **`docs/` joined the checked corpus**, with every document in it classified as live or as
55
+ a dated record — `docs/AGENT_SYNC.md` had been naming six paths this repository does not
56
+ have.
57
+ - **`.claude/agent-sync.json` pointed at a file that did not exist** and guarded the ledger
58
+ without guarding the board.
59
+ - **`CONTRIBUTING.md` called the gate two suites and it runs three**; the PR template asked
60
+ for one third of it. Both derive the list from `package.json` → `scripts.test` now.
61
+ - **`install.sh` — the channel that `rm -rf`s each destination — printed no notice** that
62
+ the gate does not travel with it, while the npm installer did.
63
+
64
+ Fourteen negative self-tests were added, one per guard, and all **42** were watched refusing
65
+ their plants on a green tree. CI run `32293489020` at `6f66255` — 39 steps, 39 `success`,
66
+ 28 of 28 negatives — retires the *"CI has not seen any of this"* limitation the ledger had
67
+ recorded four separate times.
68
+
69
+ ## v0.7.0 — 2026-08-19
70
+
71
+ **The pack that takes money had the weakest controls in the family, and every one of them
72
+ was a sentence.** A conformance audit against the Proof of Done manifesto scored this
73
+ member last on both counts it measured — most requirements absent, fewest mechanically
74
+ enforced — and all three of its enforced rows were the artifact layer, none the money
75
+ layer. The manifesto's own test failed here literally: *a credential that cannot reach
76
+ production is stronger than a sentence saying not to use it there.*
77
+
78
+ Four changes, in the order they were built: the security document stopped describing a
79
+ different skill, the crypto provider gained a credential boundary, the four manual-gate
80
+ categories became a hook that refuses, and the money invariants became fixtures a reader
81
+ runs.
82
+
83
+ ### The four money invariants were prose, and prose delegates enforcement to the reader
84
+
85
+ This pack already knew the invariants a generated integration gets wrong in the ways no
86
+ screen shows — the webhook is the payment and the redirect only proves a browser, the same
87
+ `event_id` on both sides or the revenue counts twice, `amount_refunded` arrives cumulative,
88
+ delivery is not ordered — and shipped every one of them as a paragraph. The giveaway was in
89
+ the document whose whole subject is proving a money defect would be caught: *Mutation testing
90
+ — the only proof that counts* (`references/testing-and-local-dev.md:157` at `90e9621`), whose
91
+ method was the sentence **"For every guard, delete it and re-run."** An instruction where a
92
+ test belongs, in the section arguing that tests are what count.
93
+
94
+ `manifesto.md:200` is that a test is stronger than an instruction, and `:289` that evidence
95
+ proves no more than it observed. So the invariants ship as something a reader runs.
96
+
97
+ **`stripe-billing/fixtures/` and `ad-tracking/fixtures/`** — copy either directory in and run
98
+ it. Twelve provider webhook bodies shaped the way the provider sends them: the January
99
+ renewal, the same `evt_` re-delivered, a mid-cycle proration invoice with two proration lines,
100
+ the February renewal (delivered *first*, it is the out-of-order pair), a `4000`-then-`9000`
101
+ cumulative refund against a `9000` charge, a bank-debit session that completed `unpaid` and
102
+ the `async_payment_failed` that followed it, a card session that cleared, and the pixel
103
+ arguments plus the Conversions API body one purchase produces. Plus one payload that must
104
+ never exist, kept as a fixture: the CAPI body a thank-you-page-sourced emitter sends for a
105
+ charge that never cleared.
106
+
107
+ **Eighteen assertions across two packs, and every one has been watched failing.**
108
+ `node assert-money-invariants.mjs --self-test` deletes one rule at a time from the reference
109
+ handler and requires the matching assertion to go red; it fails if the measured set differs
110
+ from the declared one in *either* direction. Nine rules on the Stripe side, five on the
111
+ tracking side, each with a fixture that isolates it.
112
+
113
+ **Isolating them is the whole engineering, and three masking pairs were found by the tool
114
+ rather than by inspection.** SD-03's mutation sweep reached 10/10 only on its second attempt
115
+ because two mechanisms were each leaning on the other's fixtures; the same shape appeared
116
+ three times here. A redelivery judged by the grant count alone is refused by the event claim
117
+ **or** by the per-period grant marker, so it proves neither — the fixture is kept, declares
118
+ `claim+grant-marker` as a multi-rule mutant, and the claim's real isolator is a **concurrent**
119
+ delivery (the marker reads before it writes, and a read is a round trip) while the marker's is
120
+ the **reconciliation** entry point, which carries no event id at all. A duplicate
121
+ `charge.refunded` is stopped by the claim and by `increment <= 0` alike, so the arithmetic is
122
+ measured by the two-step pair, whose events carry different ids. And in `ad-tracking`, reading
123
+ both sides out of one emitter meant deleting the browser event turned the id *and* name
124
+ assertions red — fixed by comparing the server's output against the shipped pixel fixture, a
125
+ boundary the server does not control.
126
+
127
+ **The fixtures cannot rot.** `check_money_fixtures()` in `test/validate.py` (15 → 16 checks)
128
+ reads each `fixtures/manifest.json` and requires both directions: every claimed invariant has
129
+ a fixture that exists and an assertion by that name, every fixture is claimed by a row, every
130
+ claiming document still carries its recorded phrase and names both the invariant and a path
131
+ under `fixtures/`, and every `fixtures/…` token in the skill's markdown resolves — the bounded
132
+ widening of the B-79 path guard that B-82 asked for. `test/fixtures_test.js` (13 checks) runs
133
+ both packs as processes in both modes and then neuters three assertions to prove `--self-test`
134
+ notices, because a self-test that cannot fail is the same defect one level up.
135
+
136
+ Five negative self-tests, 23 → **28**: a claimed invariant whose fixture is gone, a fixture no
137
+ invariant claims, a reference pointing at a fixture that is not there, a claim reworded away
138
+ from its fixture, and an assertion that can no longer fail. The guard's own first run found a
139
+ real orphan — `checkout-session-completed-paid.json`, claimed by nothing — which is why
140
+ `paid-session-grants-once` exists: without a positive control, a handler refusing *every*
141
+ checkout session would have satisfied `unpaid-session-grants-nothing`.
142
+
143
+ `SECURITY.md` moved with it, including one claim this change **falsified**: *"There is still
144
+ no runtime code inside the six skills"* stopped being true, because two of them now ship
145
+ runnable `.mjs`. It is replaced by what that code does and by the grep that proves the
146
+ boundary — no `child_process`, no `fetch`, no socket, no write, and no `process.env` read at
147
+ all; `readFileSync` of the JSON beside it is the entire I/O surface. Recounted: 30 → **50**
148
+ files in the payload, 4 → **22** non-markdown, 36 → **56** in the tarball.
149
+
150
+ ### `SECURITY.md` described a different skill, in the published tarball
151
+
152
+ The document was a wholesale copy of `seo-aeo-audit`'s. It opened *"documentation plus one
153
+ small Python script"* and named `scripts/page_audit.py`, a `commands/` directory, a
154
+ `cursor/rules/` directory and `references/threats-and-defense.md` — this repository has
155
+ never contained any of them; there is no runtime code in the skill payload at all. A whole
156
+ section described the network behaviour of that auditor, down to `--url`, `--timeout` and
157
+ `--max-bytes`. It closed with *Verifying for yourself*, three commands of which **two could
158
+ not run**: `python3 test/test_page_audit.py` exits 2, and a `grep` at
159
+ `plugins/sheleg-dev/skills/sheleg-dev/scripts/page_audit.py` exits 2 under a skill
160
+ directory named `sheleg-dev` that has never existed.
161
+
162
+ `package.json` ships `SECURITY.md` in the tarball, so the pack was inviting an outside
163
+ reader to verify its safety with commands that fail — for a pack whose subject is payment
164
+ credentials and sign-in. **Six dead references** in one document.
165
+
166
+ Rewritten against what the tree actually returns: 27 files in the skill payload at that
167
+ commit, 26 of them markdown and one a plugin manifest — 30 once the manual gate shipped
168
+ three files beside them, see below; two installers using nothing but Node built-ins and
169
+ coreutils, with no network, no subprocess and no npm lifecycle script; the one destructive
170
+ difference between them (`install.sh` runs `rm -rf` on the destination every time,
171
+ `bin/sheleg-dev.js` skips unless `--force`); credential *names* in examples and no key, with
172
+ the one live-key-shaped line in the payload named and explained; and the five vendor commands
173
+ the advice can lead an agent to run, each with the `file:line` that says it. Every command in
174
+ the new *Verifying for yourself* block was run and its output is what the document claims.
175
+
176
+ ### The guard the B-47 guard was waiting for
177
+
178
+ B-47 fixed this disease inside one table of `CONTRIBUTING.md` and said, in the guard's own
179
+ docstring, that going wider would flag paths being *discussed* rather than used. This is the
180
+ widening, bounded twice instead of not at all:
181
+
182
+ - **By corpus** — the documents whose subject is this repository: `README.md`, `SECURITY.md`,
183
+ `CONTRIBUTING.md`, `.github/PULL_REQUEST_TEMPLATE.md`. A skill reference naming the
184
+ reader's `next.config.ts` or `src/lib/heleket.ts` is describing their project; 41 such
185
+ names in the payload would have been false positives, and a guard with 41 of those gets
186
+ switched off.
187
+ - **By `FOREIGN_BY_DESIGN`** — the cross-repo signposts, enumerated one document at a time
188
+ with a reason. An exemption the document stops naming **fails**, so the list cannot widen
189
+ into a blanket.
190
+
191
+ It reads fenced blocks as well as inline spans, because the worst reference in the old
192
+ document was a fenced command, and it understands `path:line`: an address past the end of the
193
+ file resolves to nothing, which is the same defect one level down.
194
+
195
+ Watched refusing three plants and the real defect. Turned up two more of its own on the first
196
+ run: `.github/PULL_REQUEST_TEMPLATE.md` asked every contributor to paste output from
197
+ `python3 test/test_page_audit.py` and to avoid relative links in `cursor/rules/*.mdc` — both
198
+ inherited from the same copy. Also `CONTRIBUTING.md` named `agent_sync.py` with no owner; it
199
+ now says the script ships with the `agent-sync` skill.
200
+
201
+ ### `crypto-payments` had no test/live credential boundary, in the skill about taking money
202
+
203
+ There was one `HELEKET_API_KEY`. It is *also* the webhook signing secret, so it cannot be
204
+ scoped down; the key carries no `test`/`live` marker; there is one host; and "test mode" is a
205
+ **toggle in merchant settings** — a property of the account, over the same key. The document
206
+ handed that key to the reader in a local-development block with no environment declared beside
207
+ it and then pointed at the dashboard toggle, so a dev, CI or agent run held the production
208
+ credential and nothing in the pack said so. Manifesto M-06 — *a credential that cannot reach
209
+ production is stronger than a sentence saying not to use it there, because the last control
210
+ still works after context loss* — failed literally, in the one skill whose subject is money.
211
+
212
+ **Established the provider's model before designing for it**, and it is worse than the brief
213
+ assumed: Heleket offers no separate test credential at all, so the Stripe-shaped fix of
214
+ reading the environment out of the key prefix does not exist here. The boundary is therefore
215
+ built on the two non-secret things Heleket does expose — the merchant UUID, and a 12-hex
216
+ SHA-256 prefix of the live key — pinned as `HELEKET_LIVE_MERCHANT_ID` and
217
+ `HELEKET_LIVE_KEY_FINGERPRINT`. Same shape as the house pattern in
218
+ `stripe-billing/references/price-integrity.md` (*a declaration separate from the secret, so
219
+ the two can be checked against each other*), different comparand.
220
+
221
+ Shipped: `HELEKET_ENV` with **no default**, because a default is the control disappearing the
222
+ first time somebody copies a `.env`; `assertHeleketEnv()` as a copy-whole snippet that runs at
223
+ module load rather than in the checkout handler, so a run that merely *holds* the key fails
224
+ too; refusals in **both** directions — a live credential declared test, and the quiet one, a
225
+ test credential declared live, where invoices settle to a merchant nobody reconciles and
226
+ nothing errors until the revenue is missing; a refusal for `SKIP_BILLING=true` under
227
+ `HELEKET_ENV=production`, which is a free-money path rather than a shortcut; and a refusal
228
+ when nothing is pinned and a *test* run therefore cannot prove it is not live, because "could
229
+ not prove it was safe" must never read as "it was safe". Named error codes, not sentences, so
230
+ a rewording cannot silently remove a control and an operator can alert on them.
231
+
232
+ **And the exposure that remains, written down.** The assertion checks the declaration; it does
233
+ not limit the credential. Unless a second merchant account is available — which this document
234
+ cannot confirm and now says so instead of implying it — a developer following Option B holds a
235
+ production key, the verifier cannot be issued without the invoice-creating power attached, and
236
+ rotation is the only revocation. An unavoidable risk that is named is a different object from
237
+ one that is silent.
238
+
239
+ The gate went from 13 checks to 14: `check_credential_boundary()` requires that every copyable
240
+ block assigning the secret also assigns the declared environment, that the assertion exists to
241
+ be copied, that both refusal codes are present, and that the residual exposure is written.
242
+ Table-driven, one row, and the empty-corpus case fails rather than passing. Three negative
243
+ self-tests (12 → 15), one per direction plus the original defect restored verbatim. The
244
+ assertion's logic was run over 11 cases, 11/11. `stripe-billing` states the same control at
245
+ `references/price-integrity.md:62-64` and asks for it at `testing-and-local-dev.md:210` while
246
+ shipping no assertion — filed as B-86 rather than fixed here, because a guard added in the
247
+ same breath as the defect it flags turns the gate red for work this change did not do. The
248
+ other three credential-holding skills were not looked at (B-87), and this reference is now
249
+ a 4× size outlier among references, filed as B-88 with a split rather than a trim as the
250
+ remedy — the precedent this repository set at v0.6.0.
251
+
252
+ ### The four manual-gate categories were prose, and prose stops nothing
253
+
254
+ `pod-manifesto/manifesto.md:204` names money movement, irreversible action, production
255
+ access and destructive operations as the authorised person's to decide: *"The agent
256
+ prepares the decision and its evidence. The authorised person decides."* And `:200`: *"a
257
+ precondition is stronger than a warning."*
258
+
259
+ This pack named all four and stopped none. `crypto-payments/SKILL.md:310` said **"Never
260
+ auto-refund from the webhook. Route holds and refunds to a queue a human can see"**;
261
+ `stripe-billing/references/webhook-events.md:170` said a dispute is **"money already gone
262
+ plus a fee … route it to a human — evidence has a deadline"**. The plugin shipped no hooks,
263
+ no permission list and no gate, so both were advice to a reader who could skip it and to an
264
+ agent that never saw it. SD-02's boot assertion was the one real control here, and it runs
265
+ *inside the reader's application* — it cannot see a shell that merely **exports** the same
266
+ live merchant credential before any application starts.
267
+
268
+ **Shipped: a `PreToolUse` hook, `plugins/sheleg-dev/hooks/`.** Eight refusals, unless the
269
+ authorised person has signed the category off for the session: a live-shaped
270
+ `sk_live_…`/`rk_live_…` key reaching a command; an export of `HELEKET_API_KEY` — the one
271
+ credential SD-02 established has no test variant — in a run declaring `test` or declaring
272
+ nothing; `stripe refunds create` or a POST to a `…/v1/refunds` URL or a `create_refund`
273
+ tool; a payout or transfer, including Heleket's `…/v1/payout`; `stripe disputes close`; an
274
+ explicit `--live` flag; a command setting the gate's **own** switch; and `SKIP_BILLING=true`
275
+ in a run declaring production. The last two are never authorisable, and **no** category is
276
+ authorisable in a run that declares a non-production environment — a run that says it is a
277
+ test and then refunds a real card is incoherent whichever half is true.
278
+
279
+ Three invariants borrowed from the family umbrella, each one enforced by the validator so
280
+ it cannot be quietly collapsed. **The deciding is a pure module** —
281
+ `hooks/lib/moneygate.js`, payload and environment in, verdict out, no `require` of its own
282
+ and no filesystem — and the hook only moves bytes. **The hook fails silent**: it catches
283
+ everything and exits 0, because a guard that throws breaks every turn in every session,
284
+ including sessions of packs that never asked for this one; and every refusal names its
285
+ remedy, because one without a next step teaches an operator to switch the hook off.
286
+ **There is no `if` filter** on the hook entry: the reference calls that filter best-effort
287
+ and says it fails open on a command it cannot parse, and `Bash(stripe refunds*)` is exactly
288
+ the shape a `bash -c '…'` wrapper defeats.
289
+
290
+ Two defects this program proved elsewhere shaped the reading. Nothing here decides from
291
+ state the command is about to change (the umbrella's own gate asked "is anything staged?"
292
+ at `PreToolUse`, and an add-then-commit line walked through it). And the gate **reads what
293
+ would run**: a heredoc body fed to `cat` is data while a body fed to `bash` is a script; a
294
+ whole-line comment does not run; an assignment-shaped token is only an assignment in
295
+ command-prefix position, so `grep 'HELEKET_API_KEY=' plugins/` searches rather than
296
+ exports; a money endpoint must be a URL rather than a bare path; and a live key is
297
+ recognised by its **shape**, not its prefix, so `SECURITY.md`'s own sweep for
298
+ `sk_live_[A-Za-z0-9]` still runs.
299
+
300
+ `node test/moneygate_test.js` — **65 fixtures, both directions**, and the allow-plants are
301
+ real commands from this repository: that sweep, a `.env` heredoc fed to `cat`, a
302
+ commented-out `stripe refunds create`, a test-mode `sk_test_` key, and the non-secret
303
+ `HELEKET_LIVE_MERCHANT_ID` pin `assertHeleketEnv()` *requires* in a test run. `npm test`
304
+ now runs the validator and then the fixtures.
305
+
306
+ The gate went from 14 checks to 15 with `check_manual_gate()`. Eight negative self-tests
307
+ (15 → 23): five plant a defect in the gate's shipped shape and require the validator to
308
+ refuse, three break its decision module and require its own fixtures to go red. **Two of
309
+ those three were written because the first attempt went uncaught** — every `sk_live_`
310
+ allow-plant was leaning on the reader denylist while every reader allow-plant was leaning
311
+ on the key's shape, so neither mechanism was individually proven. A third plant escaped
312
+ because the validator's "the hook must require the pure module" check was satisfied by the
313
+ hook's own doc comment; it now reads the `require` expression.
314
+
315
+ **Registration, and what enforces it.** As a Claude Code plugin the hook is live when the
316
+ plugin is enabled, and `claude plugin validate --strict` schema-validates
317
+ `hooks/hooks.json`. Installed by `npx @ssheleg/sheleg-dev`, `install.sh` or `npx skills
318
+ add`, only the six skills are copied and **the gate is absent**: `README.md` → *The manual
319
+ gate* carries the settings snippet, the installer now prints the reminder, and **nothing
320
+ enforces it** — filed as B-90. Nothing writes to a person's settings file; that is the one
321
+ thing this repository must never do unasked. `SECURITY.md`'s counts move with the three new
322
+ files: 27 → **30** in the payload, 33 → **36** in the tarball, and the gate's whole reach is
323
+ two `require` calls with no `child_process`, no `fetch` and no `fs` on either path.
324
+
7
325
  ## v0.6.0 — 2026-08-16
8
326
 
9
327
  ### The file that promised the deduplication contract contained none of it
package/README.md CHANGED
@@ -20,6 +20,22 @@ Part of the [ssheleg skill family](https://github.com/ssheleg/sshlg-skills).
20
20
 
21
21
  Each carries its own references and loads them only when the work reaches them.
22
22
 
23
+ **Two of them ship a suite rather than a warning.** `stripe-billing` and `ad-tracking`
24
+ carry a `fixtures/` directory — real provider webhook bodies, an assertion pack, and a
25
+ `--self-test` that deletes one rule at a time so you can watch every assertion fail — one
26
+ call site at a time, which is not the same thing as one invariant at a time. Copy the
27
+ directory in and run it:
28
+
29
+ ```bash
30
+ node plugins/sheleg-dev/skills/stripe-billing/fixtures/assert-money-invariants.mjs --self-test
31
+ node plugins/sheleg-dev/skills/ad-tracking/fixtures/assert-dedup-contract.mjs --self-test
32
+ ```
33
+
34
+ That covers the invariants whose failure is money rather than an error page: the webhook is
35
+ the payment and the redirect only proves a browser, one `event_id` on both sides or the
36
+ revenue counts twice, a refund total that arrives cumulative, and delivery that is not
37
+ ordered. Each skill's `fixtures/README.md` is the map.
38
+
23
39
  **`stripe-billing`** — Stripe's own agent toolchain first (CLI, `stripe agent
24
40
  setup`, a sandbox without an account, the MCP implementation planner), then the
25
41
  seam it cannot help with: a pinned API version and SDK retries instead of a loop
@@ -95,6 +111,99 @@ Restart your agent afterwards — skills load at session start.
95
111
 
96
112
  ---
97
113
 
114
+ ## The manual gate
115
+
116
+ Four things this pack tells you to route to a human — a refund, a payout, closing a
117
+ dispute, a live credential — used to be **sentences**. `crypto-payments/SKILL.md` says
118
+ "Never auto-refund from the webhook. Route holds and refunds to a queue a human can
119
+ see"; `stripe-billing/references/webhook-events.md` says a dispute is "money already
120
+ gone plus a fee … route it to a human". An agent reading neither, and a shell nobody
121
+ read, were unaffected by both.
122
+
123
+ So the plugin ships a `PreToolUse` hook. It refuses eight things unless the authorised
124
+ person has signed the category off **for this session**:
125
+
126
+ | Category | What it stops |
127
+ |---|---|
128
+ | `live-key` | a live-shaped `sk_live_…` / `rk_live_…` reaching a command |
129
+ | `credential` | exporting a credential whose provider issues no test value — `HELEKET_API_KEY` — in a run that declares `test`, or declares nothing |
130
+ | `refund` | `stripe refunds create`, a POST to a `…/v1/refunds` URL, or a `create_refund` tool |
131
+ | `payout` | `stripe payouts\|transfers create`, a POST to `…/v1/payouts`, `…/v1/transfers` or Heleket's `…/v1/payout` |
132
+ | `dispute` | `stripe disputes close`, or `…/v1/disputes/…/close` — closing one accepts the loss |
133
+ | `live-flag` | an explicit `--live` / `--live-mode` flag |
134
+ | `self-authorisation` | a command that sets this gate's own switch. Never authorisable |
135
+ | `skip-billing` | `SKIP_BILLING=true` in a run declaring production — the free-money path. Never authorisable |
136
+
137
+ **Authorising one.** The authorised person exports the category — or `all` — in the
138
+ shell that starts the session, and starts a new one:
139
+
140
+ ```bash
141
+ SHELEG_DEV_LIVE_AUTHORISED=refund claude
142
+ ```
143
+
144
+ Two properties of that make it a decision rather than a formality. It is read from the
145
+ environment the **hook** was spawned with, so a value exported inside a tool call never
146
+ reaches it — and a command that tries is refused as `self-authorisation`. And **a run
147
+ declaring a non-production environment can never be authorised**: if `SHELEG_DEV_ENV`,
148
+ `HELEKET_ENV`, `STRIPE_ENV`, `PAYMENTS_ENV`, `APP_ENV` or `NODE_ENV` says anything other
149
+ than production, no variable makes a live operation pass. A run that says it is a test and
150
+ then refunds a real card is incoherent whichever half is true.
151
+
152
+ `SHELEG_DEV_MONEY_GATE=off` disables it. That is deliberate and documented: a gate with no
153
+ off switch gets deleted instead of disabled.
154
+
155
+ **Registration, and what enforces it.** Nothing in this repository writes to your
156
+ settings, by design.
157
+
158
+ - **Installed as a Claude Code plugin** — `plugins/sheleg-dev/hooks/hooks.json` travels
159
+ with the plugin and the hook is live as soon as the plugin is enabled. Nothing further
160
+ to do, and no per-hook switch: enablement is the whole control.
161
+ - **Installed by `npx @ssheleg/sheleg-dev`, `install.sh` or `npx skills add`** — those
162
+ copy the six skills into `~/.claude/skills/` and **carry no hook**. The gate is prose
163
+ again until you register it yourself, in `~/.claude/settings.json` or a project's
164
+ `.claude/settings.json`:
165
+
166
+ ```json
167
+ { "hooks": { "PreToolUse": [
168
+ { "matcher": "Bash",
169
+ "hooks": [ { "type": "command",
170
+ "command": "node \"$HOME/.claude/skills/.sheleg-dev-hooks/money-gate.js\"",
171
+ "timeout": 15 } ] },
172
+ { "matcher": "mcp__.*",
173
+ "hooks": [ { "type": "command",
174
+ "command": "node \"$HOME/.claude/skills/.sheleg-dev-hooks/money-gate.js\"",
175
+ "timeout": 15 } ] }
176
+ ] } }
177
+ ```
178
+
179
+ **Both entries, not just the first.** A refund does not care which door it came
180
+ through: `create_refund` on an MCP server never touches a shell, and the table above
181
+ advertises it. Until 2026-08-20 this snippet carried the `Bash` matcher alone — a
182
+ weaker gate than the plugin's, handed out by the document that exists because the copy
183
+ channels have no gate at all. `test/validate.py` now compares the two matcher sets.
184
+
185
+ Copy `plugins/sheleg-dev/hooks/` to that path first. `npx @ssheleg/sheleg-dev` prints
186
+ this reminder; **nothing enforces it**, which is why the plugin channel is the
187
+ recommended one.
188
+
189
+ The deciding is a pure function in
190
+ [`plugins/sheleg-dev/hooks/lib/moneygate.js`](plugins/sheleg-dev/hooks/lib/moneygate.js) —
191
+ payload and environment in, verdict out, no filesystem and no `HOME`.
192
+ [`plugins/sheleg-dev/hooks/money-gate.js`](plugins/sheleg-dev/hooks/money-gate.js) only
193
+ moves bytes, catches everything and exits 0: a hook that throws breaks every turn in
194
+ every session, including sessions of packs that never asked for this one. There is
195
+ deliberately **no `if` filter** on the hook entry — the Claude Code reference calls that
196
+ filter best-effort and says it fails open on a command it cannot parse.
197
+
198
+ Both directions are fixtured in
199
+ [`test/moneygate_test.js`](test/moneygate_test.js), and the allow-plants are real commands
200
+ from this repository: `SECURITY.md`'s own sweep for `sk_live_[A-Za-z0-9]`, a `.env`
201
+ heredoc fed to `cat`, a commented-out `stripe refunds create`, and the non-secret
202
+ `HELEKET_LIVE_MERCHANT_ID` pin that `assertHeleketEnv()` *requires* in a test run. A guard
203
+ that refuses correct input gets switched off, and then there is no gate at all.
204
+
205
+ ---
206
+
98
207
  ## A note on `crypto-payments`
99
208
 
100
209
  The skill is provider-neutral on purpose. The reusable engineering — signature
@@ -113,7 +222,7 @@ correctly. It does not tell you which one to trust.
113
222
  ## Verify
114
223
 
115
224
  ```bash
116
- python3 test/validate.py
225
+ npm test # the validator, the manual gate's 65 fixtures, the money fixtures' 13 checks
117
226
  ```
118
227
 
119
228
  One version across `package.json`, `plugin.json`, `marketplace.json` and the top
@@ -122,6 +231,11 @@ matter does not error — hosts truncate it silently, which is worse); and
122
231
  `SKILL.md` ↔ `references/` agreement in **both** directions, so neither a
123
232
  dangling link nor a file nobody loads can ship.
124
233
 
234
+ Plus, for the money fixtures, that every assertion has been watched failing and that no
235
+ two rules are covering for each other — `test/fixtures_test.js` neuters assertions,
236
+ including one that is **not** the first inside a multi-assert invariant, and requires each
237
+ pack's `--self-test` to notice. That last plant is the one that used to pass.
238
+
125
239
  CI plants a defect against every one of those guards and requires the validator
126
240
  to fail. A green from a check nobody has watched fail is not evidence.
127
241