@sakk-dev/sakk 0.0.0-stage → 0.4.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,1421 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] - 2026-11-05
11
+
12
+ ### Changed — the npm package is `@sakk-dev/sakk`
13
+
14
+ - npm refused the unscoped name `sakk` as too close to existing packages, so
15
+ Sakk is published as `@sakk-dev/sakk`: install it with
16
+ `npm install -g @sakk-dev/sakk`. The command it installs is still `sakk`,
17
+ so every `sakk …` command in these notes and the docs is unchanged. A test
18
+ now fails when an install line in the docs or the CLI names any other
19
+ package.
20
+
21
+ ### Fixed — the spend cap and the ledger summary can be trusted
22
+
23
+ - One Claude Code log file that cannot be opened (permissions) no longer
24
+ crashes the spend walk. `sakk spend` names the file and its error,
25
+ `SPEND_LOGS_UNLISTABLE`, and exits 1; the other files are still indexed
26
+ and the cap stays closed until a person fixes the file. When any step of
27
+ the guard fails, the deny names that step (spend cap, kill switch, secret
28
+ scan or rules) instead of always blaming the rules (F-192).
29
+ - Reserve no longer trusts the ledger's summary alone. Each sealed segment's
30
+ hash now covers its running total (`total: {settledCount, settledUsd}`,
31
+ a new field in `<ledger>.cold/<seq>.json`), and reserve and the seal check
32
+ the summary against the newest segment. An edited settled total, or a
33
+ lost or rolled-back newest segment, now refuses `LEDGER_ARCHIVE_BROKEN`
34
+ where it used to give cap room back (F-193). A reserve call reads the hot
35
+ file plus that one segment, so its cost still does not grow with history.
36
+ Ledgers sealed by 0.4.0 keep working: their newest segment has no total,
37
+ so reserve walks the chain on each call until the next seal writes one.
38
+ Limit: the hash is unkeyed, so a writer who rewrites the newest segment
39
+ and the summary together passes reserve; `sakk ledger verify` catches it.
40
+ - A seal that crashed before its hot commit no longer locks reserve: the
41
+ segment it left is adopted as written by the next reserve or settle (a
42
+ segment number on disk is never sealed again, and a leftover whose entries
43
+ differ from the hot file is refused); `sakk ledger verify` says so and
44
+ exits 0.
45
+ A summary rolled back over real segments, a stray or out-of-place
46
+ segment file, or a 0.4.0 segment holding an entry with no amount is
47
+ refused (`LEDGER_ARCHIVE_BROKEN`), and verify exits 1 where it used to
48
+ report sound. A seal never rewrites a segment that already exists.
49
+ `sakk init` refuses `LEDGER_ALREADY_EXISTS` beside a leftover
50
+ `<ledger>.cold` or `<ledger>.idx` (F-193).
51
+ - Downgrading is unsupported: segments sealed by this version carry a
52
+ running total that 0.4.0 does not hash, so 0.4.0 cannot verify or
53
+ export them.
54
+ - The test that holds "a call costs the same whatever the history" now sees
55
+ the reads it counts: it patched `fs` without syncing the named imports
56
+ the code uses, so it passed on zero reads whatever the code did. A new
57
+ check refuses that shape in any test or script (F-241).
58
+
59
+ ### Fixed — the guard judges an act however it is spelled
60
+
61
+ - A command or protected name in another case (`RM -rf ~`,
62
+ `.CLAUDE/settings.json`) is judged as the lower-case name. The default
63
+ macOS volume opens both spellings as the same file; on a case-sensitive
64
+ volume this only tightens (F-183).
65
+ - Every spelling git accepts for a destructive flag is denied: long-option
66
+ prefixes (`git push --forc`) and short clusters (`git push -fu`).
67
+ `git push --prune` is destructive. A git alias defined for one command
68
+ (`git -c alias.x=…`) is asked (F-184).
69
+ - `sakk` run through any launcher (`npx sakk cap off`, `node …/bin/sakk.mjs`,
70
+ `unbuffer sakk …`) is judged as `sakk` itself (F-185).
71
+ - A read through a symbolic link to a secret file is denied, and a write or
72
+ delete through a dangling link is judged by where it points; a link loop
73
+ is refused (F-186).
74
+
75
+ ### Changed — the version is 0.4.0
76
+
77
+ - `package.json`, the plugin manifest and the `sakk-litellm` package say
78
+ 0.4.0. Both tutorials were re-run on the packed 0.4.0 tarball in a
79
+ throwaway home; their output, dates and timings come from that run.
80
+ The maintainer pushes the `v0.4.0` tag; `release.yml` then publishes it
81
+ to npm with provenance, and refuses if the tag is not `v` plus this
82
+ version.
83
+
84
+ ### Added — the LiteLLM gateway runs as a service, in CI and as a Python package
85
+
86
+ - CI has a `gateway` job: it installs LiteLLM 1.106.2 from the new hash lock
87
+ `integrations/litellm/requirements.lock` (hashes required, no source builds,
88
+ uv pinned by checksum) and runs the plugin's end-to-end tests against the
89
+ mock provider, with no key and no cost. It adds 15 runner-minutes to each
90
+ pull request commit and each merge to main (about $0.09); the ceilings in
91
+ `test/ci-cost.test.mjs` say so. It is not on main's required checks, which
92
+ the owner sets.
93
+ - Service templates in `integrations/litellm/service/`: a launchd agent
94
+ (`KeepAlive`, at most one restart per 10 s) and a systemd user unit
95
+ (`Restart=always`), both on 127.0.0.1 only, with install, check and
96
+ uninstall commands. `test/gateway-service.test.mjs` checks both against the
97
+ variables `sakk_gateway.py` reads.
98
+ - `integrations/litellm/pyproject.toml` packages the plugin as `sakk-litellm`
99
+ (version 0.4.0, the module alone, Apache-2.0 with NOTICE). It requires
100
+ `litellm[proxy]>=1.106.2,!=1.107.0`, which keeps the compromised releases
101
+ out. Not yet published; `docs/launch/pypi-publish.md` has the steps.
102
+ `test/gateway-package.test.mjs` ties the lock, the package and the CI job
103
+ together.
104
+
105
+ ### Fixed — the gateway releases stuck reservations and stops queueing on a hung ledger (F-242, F-243)
106
+
107
+ - A reservation the gateway made whose settle never arrived (the proxy
108
+ restarted, the ledger refused the settle, or the settle failed twice)
109
+ stayed outstanding for good: it held one of the ledger's outstanding slots
110
+ (`LEDGER_FULL` at 10,000) and kept the books from sealing (F-242). The
111
+ gateway now settles its own reservations (agent `litellm-gateway`) older
112
+ than `SAKK_RESERVATION_TTL_S` (default 3600 s; zero, negative or
113
+ non-numeric values refuse to start) at the worst case they reserved. A
114
+ background sweep does this, started by the next call at most once a
115
+ minute, 50 settles per run, each logged as a WARNING JSON line
116
+ (`"event": "sakk_reservation_expired"`, with `callId`, `settled_usd`,
117
+ `created_at` and `ttl_s`). The dollars stay counted: a worst-case settle
118
+ over-records, never under. With several gateways on one ledger, one may
119
+ settle another's long call; that call's own settle then gets
120
+ `SETTLE_MISMATCH` and an alert.
121
+ - New command `sakk settle <callId> --by <name> [--ledger <path>]`: a person
122
+ at a terminal closes one outstanding reservation at its reserved amount,
123
+ typing `settle` to confirm, and the alerts file gets a
124
+ `SETTLED_AT_RESERVED` line naming who did it. Without a terminal it exits
125
+ 2 with `HUMAN_CONFIRMATION_REQUIRED`; an entry that is already settled is
126
+ a no-op that asks no one; an id the ledger never saw exits 1 with
127
+ `SETTLE_UNKNOWN_CALL`; an entry that awaits reconciliation exits 1 with
128
+ `SETTLE_AWAITS_RECONCILIATION` and points at `sakk reconcile`. When the
129
+ settle lands but the alert cannot be written, it says so and exits 1. The
130
+ guard hook denies `sakk settle` to agents, through every launcher.
131
+ - `SAKK_TIMEOUT_S` defaults to 5 s (it was 2 s, equal to the ledger's own
132
+ 2 s wait for its lock, so a busy but healthy ledger could count as an
133
+ outage and the call went through unmetered) (F-243). A reserve that fails
134
+ now opens a breaker for `SAKK_BREAKER_S` (default 10 s; 0 turns it off;
135
+ negative or non-numeric values refuse to start): until it closes, reserves
136
+ take the outage path at once without asking the ledger, including calls
137
+ already queued behind the failing one, which used to wait out their own
138
+ timeout each in turn. Each one is still counted in the status file and
139
+ logged as `sakk_unavailable_allowed` (or refused with 503 in deny mode).
140
+ Settles are never held by the breaker. The first answer after it closes
141
+ ends the outage as before.
142
+ - Not done yet: a side file of the calls let through during an outage, to
143
+ record them in the ledger afterwards; the status file's outage counter
144
+ (`allowed_calls`, `est_max_unrecorded_usd`) stays the record of them.
145
+
146
+ ### Changed — the LiteLLM gateway lets calls through when Sakk cannot answer
147
+
148
+ - When Sakk's process will not start, crashes, hangs past the timeout or
149
+ replies with garbage, the gateway now sends the call to the provider
150
+ instead of answering 503. `SAKK_ON_UNAVAILABLE=deny` keeps the old 503
151
+ `SAKK_UNAVAILABLE`; any value other than `allow` or `deny` refuses to
152
+ start. **Calls let through this way are not recorded in the ledger and
153
+ not capped.** A refusal from Sakk that arrives within `SAKK_TIMEOUT_S`
154
+ (402 over the cap, halted, reconciliation, any other Sakk code) and the
155
+ gateway's own 403 and 400 refusals stay refusals in both modes. A ledger
156
+ file lock another process holds longer than the timeout is no answer and
157
+ counts as an outage; Sakk waits about 2 s for the lock itself, inside the
158
+ 5 s default timeout (F-243).
159
+ - Each such call logs one JSON WARNING line (`"event":
160
+ "sakk_unavailable_allowed"`, with the model, `est_max_usd`, the reason,
161
+ `outage_since` and `allowed_calls`) and its response, streamed or not,
162
+ carries the header `x-sakk-status: unmetered`. The first answer from Sakk
163
+ again, a reservation or a refusal, ends the outage and logs
164
+ `sakk_outage_ended`.
165
+ - A Sakk process that died is started again within the next call, and that
166
+ call is metered if the new process answers. The request is sent again
167
+ only if writing it to the old process failed; a request that was written
168
+ and got no reply is an outage and is not sent again, because the first
169
+ reservation may have landed (it stays reserved until reconciled).
170
+ - `SAKK_TIMEOUT_S` now defaults to 5 seconds (was 10; 2 seconds for a while
171
+ before release, see F-243 above), for reserve and settle alike; zero,
172
+ negative or non-numeric values refuse to start.
173
+ - New status file, `SAKK_GATEWAY_STATUS` or `<SAKK_LEDGER>.gateway.json`
174
+ with the ledger's folder resolved to its real path on both the gateway and
175
+ the `sakk` side, so a relative or linked ledger path names the same file;
176
+ written whole (temporary file, fsync, rename) with mode 0600 on the
177
+ gateway's event loop:
178
+ `{version: 1, updatedAt, mode, outage, last_outage}`, where `outage` is
179
+ null or `{since, allowed_calls, est_max_unrecorded_usd, last_reason,
180
+ last_at}` and `last_outage` adds `ended_at`. The gateway assumes one
181
+ process per ledger.
182
+ - `sakk status` prints a `gateway` line when the status file exists, and
183
+ `sakk status --json` has a `gateway` key (null without the file). The exit
184
+ code does not change.
185
+ - The console shows a red banner on every page during an outage and a
186
+ warning after one, and, when the status file exists, checks
187
+ `SAKK_GATEWAY_URL` (default `http://127.0.0.1:4000`) at
188
+ `/health/liveliness` (500 ms, cached 5 s), with a red banner when the
189
+ gateway does not answer.
190
+ - `integrations/litellm/config.example.yaml` no longer has a Claude route.
191
+
192
+ ### Fixed — the console's spend figures read right (F-236, F-237, F-238)
193
+
194
+ - The console's 7-day spend chart cut the `$0` off its axis labels and,
195
+ for days under a cent, printed the same label on several lines (F-236).
196
+ The axis now steps in round amounts, prints as many decimals as the step
197
+ needs and widens its margin to the longest label.
198
+ - The console printed spend under $0.00005 as `$0.0000`, so the gateway
199
+ ledger card read as free next to a chart showing the same calls (F-237).
200
+ Amounts under $0.0001 now print with two significant digits, such as
201
+ `$0.000049`; every other amount prints as before.
202
+ - The Today and Gateway ledger cards printed spend and cap as one large
203
+ string, so `$0.000050 / $1.00` broke across two lines on a desktop and
204
+ `— / no cap` across three on a phone (F-238). The cap now sits after the
205
+ amount in smaller type and never breaks inside, and the figure shrinks
206
+ with its card so an amount stays whole on a phone.
207
+
208
+ ### Fixed — `sakk ui --set-password` no longer shows a password (F-240)
209
+
210
+ - The password prompts were shown a moment before echo went off, so a
211
+ password typed the instant a prompt appeared could be echoed. A script
212
+ that answers the prompts (such as `expect`) showed the password in 2 runs
213
+ out of 20. Echo now goes off before each prompt is shown, the way
214
+ Python's `getpass` does it; the same script showed it in none of 60 runs.
215
+ - Keys typed before a prompt appears are still echoed by the terminal
216
+ itself. Wait for the prompt before you type the password.
217
+
218
+ ### Added — `sakk gateway init`: connect your own app, in any language
219
+
220
+ - The README, its Chinese version, the FAQ and `llms.txt` now open with two
221
+ ways in: the coding-agent guard, and your own app through the LiteLLM
222
+ proxy, where the app changes one line,
223
+ `OPENAI_BASE_URL=http://127.0.0.1:4000/v1`. They state what the gateway
224
+ does when the spend ledger cannot answer: calls go through unmetered by
225
+ default with a warning, `SAKK_ON_UNAVAILABLE=deny` refuses them, and 402
226
+ and 403 refusals stay refusals. The guard and the library ledger stay
227
+ fail-closed.
228
+ - New command `sakk gateway init`, with `--dir <path>`, `--ledger <path>`,
229
+ `--port N`, `--litellm <path>` and `--force`. It copies the shipped gateway callback,
230
+ writes `config.yaml` from `config.example.yaml` (keys stay `os.environ/`
231
+ references; no key is written) and a `start.sh` that sets `SAKK_BIN`,
232
+ `SAKK_LEDGER` and `SAKK_GATEWAY_CONFIG` to absolute paths and runs
233
+ `litellm --config … --host 127.0.0.1 --port N`. Defaults: the folder
234
+ `gateway` under `SAKK_HOME` (or `~/.sakk`), the ledger the other commands
235
+ use, port 4000. It prints the one line the app changes. It only checks that
236
+ an executable `litellm` exists on `PATH` or at `--litellm`; it never runs
237
+ it and does not check its version.
238
+ - Exit codes: 0 written; 1 refused or failed; 2 usage. Refusal codes, each
239
+ of which writes nothing: `GATEWAY_USAGE` (2), `GATEWAY_PORT_INVALID` (2,
240
+ port outside 1024-65535), `GATEWAY_EPHEMERAL` (2, run from an npx, bunx or
241
+ pnpm dlx cache whose path the start script would pin; install with
242
+ `npm install -g sakk` first), `GATEWAY_SOURCE_MISSING` (1),
243
+ `GATEWAY_LEDGER_UNREADABLE` (1, a missing or unreadable ledger; create it
244
+ with `sakk init --cap <usd>`), `GATEWAY_LITELLM_NOT_FOUND` (1, prints the
245
+ hash-locked install steps for LiteLLM 1.106.2 and warns off the compromised
246
+ 1.82.7, 1.82.8 and 1.107.0), `GATEWAY_FILES_EXIST` (1, pass `--force` to
247
+ replace them). `GATEWAY_WRITE_FAILED` (1) removes the files that run
248
+ created. The written `start.sh` refuses to start without
249
+ `LITELLM_MASTER_KEY` and exits 1 with `GATEWAY_STALE` when the node, sakk
250
+ or ledger it names is gone.
251
+ - The guard judges `sakk gateway init` as the write of the `start.sh` it
252
+ makes: inside `SAKK_HOME` that is guard state, denied to the agent.
253
+ - The npm package now ships `integrations/litellm/sakk_gateway.py`,
254
+ `config.example.yaml` and its `README.md`, not the Python tests or the mock
255
+ provider. New keywords: `litellm`, `openai-compatible`, `llm-gateway`.
256
+
257
+ ## [0.3.0] - 2026-11-01
258
+
259
+ ### Fixed — the tutorial and FAQ match the 0.3.0 build (F-234)
260
+
261
+ - Both tutorials were re-run on the packed 0.3.0 tarball in a throwaway
262
+ home and say so. Step 2 now creates the `--pack-destination` folder first:
263
+ npm 11 does not, and `npm pack` stopped with `ENOENT`. The `sakk prices`
264
+ output shows the provenance line, and step 9 shows the optional sign-in.
265
+ - The FAQ no longer says the console has no sign-in; it describes
266
+ `--set-password`, what a password session may do and how it ends.
267
+ - New tests in `test/release.test.mjs` tie the version each tutorial names
268
+ to `package.json`, require a `mkdir -p` before every `--pack-destination`
269
+ in the docs, and compare the tutorials' prices block and the FAQ's sign-in
270
+ answer with what the CLI prints.
271
+
272
+ ### Fixed — the console names the public repository (F-235)
273
+
274
+ - The console's "Enterprise · coming soon" panel named the private repository's
275
+ discussions. It now names `github.com/sakk-dev/sakk/discussions`. The repository-name test
276
+ read four hand-picked files; it now reads every file `npm pack` ships plus
277
+ the README and release docs, and refuses any owner but `sakk-dev`.
278
+
279
+ ### Added — optional username and password sign-in for `sakk ui`
280
+
281
+ - `sakk ui --set-password` asks at a terminal, without echo, for a username
282
+ and a password of 15 or more characters, and stores a scrypt hash with a
283
+ random salt in `console-login.json` under `SAKK_HOME` (mode 0600).
284
+ `sakk ui --clear-password` removes it. Neither takes the password on the
285
+ command line or from a pipe (`UI_PASSWORD_NEEDS_TTY`).
286
+ - With a password set, `http://127.0.0.1:<port>/` shows a sign-in page; the
287
+ address `sakk ui` prints still opens the console. A sign-in sets an
288
+ `HttpOnly`, `SameSite=Strict` session cookie that ends after 30 minutes
289
+ idle or 8 hours, or at Sign out. A password session reads; stopping an
290
+ agent still needs `--allow-halt` and the start token.
291
+ - New routes `POST /login` and `POST /logout`, and new codes
292
+ `CONSOLE_LOGIN_FAILED` (wrong username or password, one answer for both),
293
+ `CONSOLE_LOGIN_LOCKED` (five failures in 15 minutes, with `Retry-After`),
294
+ `CONSOLE_LOGIN_BUSY`, `CONSOLE_LOGIN_BAD_BODY`, `CONSOLE_LOGIN_BAD_TYPE`
295
+ and `CONSOLE_LOGIN_TOO_BIG`. A credential file that is a link, readable by
296
+ others or malformed stops `sakk ui` with `CONSOLE_LOGIN_UNSAFE` or
297
+ `CONSOLE_LOGIN_INVALID` and exit 2.
298
+ - With no password set, `sakk ui` is unchanged. SSO, team accounts and named
299
+ approvers stay out of the open-source edition.
300
+ - An unknown argument or flag of `sakk ui` is no longer echoed back in the
301
+ error, so a password typed on the command line does not land on the screen.
302
+
303
+ ### Changed — open-source release: Apache-2.0, DCO and fork-safe CI
304
+
305
+ - **Licence: Apache-2.0** (was AGPL-3.0-only). `LICENSE` is the verbatim
306
+ Apache License 2.0; `NOTICE` names the copyright holder, "The Sakk
307
+ authors", and keeps the MIT notices of the derived material; every SPDX
308
+ header, `package.json` and the plugin manifest say `Apache-2.0`.
309
+ - Contributions are taken under the Developer Certificate of Origin 1.1 —
310
+ a `git commit -s` sign-off on every commit — instead of a CLA. CI checks
311
+ each pull request commit with the base branch's copy of
312
+ `scripts/dco-check.mjs` and names any commit without its author's
313
+ sign-off.
314
+ - `npm install` runs no lifecycle script: the `prepare` hook is gone.
315
+ `npm run hooks` opts a clone in to the git hooks and the repository's
316
+ Claude Code hooks (`.claude/settings.json` now runs them only in a clone
317
+ that opted in).
318
+ - A pull request from a fork skips only the self-evolution and
319
+ base-checker jobs; tests, format, the DCO check and the drill still run.
320
+ The adversarial review never runs for a fork and shows as skipped.
321
+ - The repository moves to `github.com/sakk-dev/sakk` (`package.json`,
322
+ plugin manifest, `SECURITY.md`).
323
+
324
+ ### Added — release and supply-chain workflows
325
+
326
+ - `release.yml`: a pushed `v*` tag publishes to npm by trusted publishing
327
+ (OIDC, no stored token) with provenance; it refuses on a private
328
+ repository, a tag that is not `v` + the package version, or an npm older
329
+ than 11.5.1.
330
+ - `scorecard.yml`: a weekly OpenSSF Scorecard run, skipped while the
331
+ repository is private.
332
+ - `.claude-plugin/marketplace.json`: the repository is its own Claude Code
333
+ plugin marketplace (`/plugin marketplace add sakk-dev/sakk`).
334
+ - `SECURITY.md` points to GitHub private vulnerability reporting and lists
335
+ the supported versions.
336
+
337
+ ### Added — documentation for the open-source release
338
+
339
+ - `README.zh-CN.md`, a Chinese README kept in step with `README.md`.
340
+ - `docs/FAQ.md` and `docs/FAQ.zh-CN.md`; `docs/tutorial/en.md` and
341
+ `docs/tutorial/zh-CN.md`, a first-run tutorial in each language.
342
+ - `docs/ROADMAP.md`, the public form of `docs/V2-BACKLOG.md`, and
343
+ `docs/compare.md`, how Sakk sits beside neighbouring tools.
344
+ - Architecture decision records 0001–0014 under `docs/adr/`, and
345
+ retrospectives under `docs/retros/`.
346
+ - `llms.txt`, a plain index of the docs for AI search tools.
347
+ - `docs/media/sakk-block.gif`: the console's guard page allowing a listing
348
+ and denying a home-folder delete.
349
+ - The README names both plugin routes: the repository's own marketplace,
350
+ and `--plugin-dir` from the npm package.
351
+ - GUARD.md, SPEND.md and the README no longer say a halt or a reached cap
352
+ denies every tool call: the Claude Code install routes shell, file,
353
+ Monitor and MCP tools, not Glob, Grep, WebFetch, WebSearch or Task
354
+ (F-189); loaded as a plugin, the guard sees every tool (F-233).
355
+
356
+ ### Changed — regulator-specific packs and policy leave the public repository
357
+
358
+ - Two example case packs written for a fictional authority,
359
+ `example-agent-oversight` and `example-change-control`, replace the
360
+ CBUAE, DORA and DIFC packs. They keep the same format and back the same
361
+ 9 of the 12 gold cases; packs for specific regulations are not in this
362
+ repository.
363
+ - The regulated-strict reference policy is removed: `policies/` ships
364
+ `permissive-dev.json` and `guard-default.json` only, and a test pins
365
+ that list because `policies/` is in the npm package.
366
+ - The reviewer label gate is renamed `eval/05_LABEL_REVIEW.md`.
367
+
368
+ ### Changed — `sakk ui` follows the frozen v10 open-source design
369
+
370
+ - The console is rebuilt page by page to the frozen v10 open-source
371
+ prototype, on the same `/api` routes (F-223). Long paths and call ids no
372
+ longer widen the page in a narrow window (F-222).
373
+ - A click no longer moves the nav: in a narrow window the nav strip keeps
374
+ its scroll and brings the chosen page into view, and nav items keep their
375
+ width when chosen (F-224).
376
+
377
+ ### Fixed — what the console and CLI report after the 2026-11-01 UAT
378
+
379
+ - `sakk doctor`'s self-test is logged as `sakk-doctor`, not as Claude Code,
380
+ when it fires its fixed probe through the installed hook. The console
381
+ leaves it out of blocked counts, agents and the feed, and keeps it in
382
+ records and export as "Doctor self-test". A decisions line that is not a
383
+ record is counted as unreadable (F-229).
384
+ - The Secret guard page lists secret reads the guard blocked, not only
385
+ secrets found in content (F-226).
386
+ - The Agents page says when the Claude Code hook was installed fail-open
387
+ (F-227).
388
+ - Open items counts the rows it links to; a halt no longer adds one (F-228).
389
+ - `sakk prices` prints the provenance its price table records (source URL,
390
+ commit, SHA-256, fetch date, licence), or says none is recorded, as the
391
+ README already promised; `--json` adds a `provenance` object (or `null`)
392
+ and keeps `source` as the table's path (F-232).
393
+ - A deny by a policy rule reads "A person changes the policy." as its own
394
+ sentence before the dispute line.
395
+ - `sakk ledger export --out` says "entry" or "entries" instead of printing
396
+ `entr(y|ies)` (F-225).
397
+ - The overview's Today card shows "—" and "run sakk spend to count it" for
398
+ a day the spend index has not counted, instead of $0.00. `/api/overview`
399
+ gains `indexed_at` (when the index was last counted, or null) and
400
+ `counted_today`; the console still writes nothing on a GET (F-231).
401
+
402
+ ### Added — the console shows the spend ledger
403
+
404
+ - `sakk ui` has a new **Spend ledger** page, and the overview a ledger card:
405
+ each metered call (agent, status, reserved maximum, actual), the cap,
406
+ settled, reserved now, what is left and the most the next call may
407
+ reserve, plus the refusals and alerts from `sakk-alerts.jsonl` next to the
408
+ ledger, newest first with the cap each was judged against.
409
+ - New read-only route `GET /api/ledger`. It shows the ledger the console was
410
+ started with (`--ledger`, `config.json` under `SAKK_HOME`, or
411
+ `SAKK_LEDGER_PATH`) and takes no query. With no ledger set it answers
412
+ `configured: false`; ledger refusals pass through by their own code; a
413
+ ledger over 32 MiB answers the new code `CONSOLE_LEDGER_TOO_BIG`.
414
+ - `ledgerStatus` and `alertsPathFor` are now exported from `src/ledger.mjs`.
415
+ `sakk status --json` is built by `ledgerStatus`, so the console and the
416
+ CLI print the same object; its shape is unchanged.
417
+
418
+ ### Changed — the decisions tail skips files that are not regular files
419
+
420
+ - The console's log reader opens `decisions.jsonl` without blocking and
421
+ skips a fifo or directory in its place instead of hanging the request (the
422
+ alerts log is read the same way and also refuses a symlink). `truncated` is now also true when the
423
+ 500-line page limit cuts the list, not only when the 1 MiB tail does.
424
+
425
+ ## [0.1.0] - 2026-10-29
426
+
427
+ ### Fixed — a backdated log line cannot escape the daily cap (F-210, F-211)
428
+
429
+ - The spend index booked every log line to the day its own timestamp
430
+ claimed, so a line first seen today claiming an old day landed outside
431
+ every cap window (the F-205 fix covered the future direction only). The
432
+ first whole scan now seals a baseline (`$SAKK_HOME/spend/baseline.json`):
433
+ history already on disk keeps its claimed day, and after sealing a line
434
+ first seen today claiming an earlier day is counted today and flagged
435
+ `backdated` — unless the claimed day's bucket already covers that call's
436
+ usage (a legitimately rewritten log). `sakk spend` reports the count (a
437
+ new `backdated` field in `--json`), and a `SPEND_CAP_REACHED` deny says
438
+ when backdated lines are included (both shapes are additive).
439
+ - The index also trusted file mtime as its scan boundary, so a planted
440
+ file faking an old mtime was never read. The incremental skip now
441
+ requires the index to already know the file at an unchanged size; mtime
442
+ is an optimization input, never a trust boundary (F-211).
443
+
444
+ ### Changed — the release describes one artifact (F-111)
445
+
446
+ - Version 0.1.0 is cut: package.json, the plugin manifest and this
447
+ changelog's newest released section now name the same version, and a
448
+ release test pins them together with the `v0.1.0` tag-name format the
449
+ links below use. Publishing (the tag and npm) remains the owner's step.
450
+
451
+ ### Fixed — a spend check that cannot see the log root fails closed (F-208)
452
+
453
+ - The daily-cap reader treated a missing log ROOT as "no spend yet", so an
454
+ unreadable parent directory, a dangling symlink or a symlink loop at the
455
+ root reported an honest-looking $0 and the cap allowed. The root is now
456
+ read like any subdirectory: only true absence is $0; every other failure
457
+ denies with `SPEND_LOGS_UNLISTABLE`.
458
+
459
+ ### Fixed — a resume no-op never reaches the write path (F-209)
460
+
461
+ - `sakk resume` with nothing halted now answers and exits before the
462
+ in-lock write path is reachable (same message, same exit code), so a halt
463
+ landing between the check and the lock can no longer be lifted without a
464
+ person's confirmation — matching `sakk reconcile` and `sakk unlock`.
465
+
466
+ ### Fixed — the console refuses a cross-site Origin on every route (F-195)
467
+
468
+ - `sakk ui` now refuses a request whose Origin header is not the console's
469
+ own loopback origin on every route — the page, every `/api/*` read and
470
+ the halt write — in one gate ahead of dispatch, so a route added later is
471
+ covered too. A request without an Origin header (a typed URL, curl) still
472
+ works, and the halt write still requires the Origin to be present.
473
+ - Every console response now carries `Cache-Control: no-store`, and a
474
+ non-HTML response's CSP no longer carries an empty-nonce script or style
475
+ allowance.
476
+
477
+ ### Removed — the advertised prices updater (F-175)
478
+
479
+ - The update subcommand of `sakk prices` named in earlier notes was never
480
+ built and is retracted (owner decision D11, 2026-10-29): the hints in
481
+ `sakk ui` and in the PRICE_UNKNOWN refusal no longer point to it, and the
482
+ spend spec and docs name the one real route — copy `prices/snapshot.json`
483
+ to `~/.sakk/prices.json` and edit it by hand.
484
+ - The release check now validates every multi-word `sakk` phrase in the
485
+ README, this changelog, docs, specs and the runtime's user-facing strings
486
+ against the CLI's real subcommands, so an advertised-but-unbuilt command
487
+ cannot recur.
488
+
489
+ ### Changed — agent edits of guard config are denied, not asked (F-188)
490
+
491
+ - The `ask-agent-config` policy is renamed and tightened to
492
+ `deny-agent-config`: from the agent, `sakk guard install|uninstall` is now
493
+ denied instead of asked. Mentions of the old name further down this file
494
+ describe the behavior as it was when those notes were written.
495
+
496
+ ### Added — a NOTICE file for the MIT-derived material (F-199)
497
+
498
+ - A NOTICE file now states the MIT-licensed material this AGPL-3.0-only
499
+ package derives from — the secret-scanning rules ported from gitleaks and
500
+ the price snapshot derived from LiteLLM's model list — with their
501
+ attributions, and ships in the npm package.
502
+
503
+ ### Added — spend failures and oddities get public names
504
+
505
+ - New public error code `SPEND_LOGS_UNLISTABLE`: a spend-log directory that
506
+ cannot be listed now fails closed under its own name.
507
+ - New public error code `LEDGER_PATH_LEGACY`: the legacy `./sakk-ledger.json`
508
+ default path is refused by name instead of being silently read.
509
+ - `sakk spend --json` adds a `future` field: a log line stamped in the
510
+ future counts toward the day it is observed, and is flagged there.
511
+
512
+ ### Changed — unattended runs stop at the human gate
513
+
514
+ - `sakk resume`, `sakk reconcile` and `sakk unlock` without a real terminal
515
+ now exit 2 with `HUMAN_CONFIRMATION_REQUIRED` instead of succeeding, since
516
+ each needs a person's confirmation.
517
+ - The default ledger path moves to `~/.sakk/sakk-ledger.json`.
518
+
519
+ ### Changed — v1 close-out: known limits stated, the rest moved to v2
520
+
521
+ - The 2026-10-28 acceptance run checked all 151 P0 cases by their expected
522
+ result. The owner closed v1 as it stands (docs/DECISIONS.md D10): no code
523
+ changes, and every case that did not pass is a finding (F-182…F-201) with
524
+ its fix in [docs/V2-BACKLOG.md](docs/V2-BACKLOG.md).
525
+ - The gaps an operator should know before relying on v1 are now stated in
526
+ the README "Limits", docs/GUARD.md "Known open in v1" and docs/SPEND.md:
527
+ names compared in one spelling, symlinks judged by their own name, search
528
+ tools outside the hook's matcher, a project-local install in the agent's
529
+ reach, the console's Stop button, and the spend figure's reliance on the
530
+ agent's environment.
531
+ - Corrected in the docs: the README plugin line (`claude --plugin-dir`, one
532
+ session; a persistent install needs a marketplace file, planned for v2),
533
+ the secret-rule counts (195 upstream, 192 enabled), which roadmap items
534
+ ship in the npm package, and the fetching prices updater, which was
535
+ advertised but never built (retracted in this release — F-175).
536
+
537
+ ### Changed — the agent cannot re-point the ledger a halt is read from (F-181)
538
+
539
+ - From the agent, `sakk init` is now denied (`protect-guard-state`), as
540
+ `sakk cap` is. Run by an agent, it could record a ledger the agent writes in
541
+ `~/.sakk/config.json`; the hook reads that ledger first, so a halt held on
542
+ the ledger `SAKK_LEDGER_PATH` names stopped applying. A person runs `init`
543
+ at the terminal as before. Agents or scripts that ran `sakk init` through a
544
+ guarded agent must now run it themselves.
545
+
546
+ ### Fixed — a cache-write breakdown can no longer lower the bill (F-179)
547
+
548
+ - A `cache_creation` breakdown that names only part of the cache writes now
549
+ bills the rest at the 5-minute price, as a log line with no breakdown does;
550
+ before, the breakdown replaced the total and the daily cap counted less than
551
+ was spent. Breakdown tokens beyond the total are billed and count toward
552
+ long context.
553
+ - A breakdown field that is not a whole-token count (a string, a fraction, a
554
+ boolean, a negative number), or a `cache_creation` that is not an object,
555
+ now makes the line unreadable — the cap denies with `SPEND_UNREADABLE` —
556
+ instead of being read as 0.
557
+ - docs/SPEND.md now states that the cap counts tokens only: server-side tools
558
+ billed per request, such as web search, are not included (F-180).
559
+
560
+ ### Changed — the agent cannot lift its own cap or remove its own guard (F-171)
561
+
562
+ - `sakk cap off`, raising the cap, moving its day with `--zone`, and
563
+ `sakk guard uninstall` (when it would remove something) now ask a person at
564
+ the terminal to retype a word. With no terminal on stdin and stderr, or a
565
+ wrong answer, they exit 2 with the new code `HUMAN_CONFIRMATION_REQUIRED`
566
+ and change nothing. Setting a first cap, lowering it, `guard install` and
567
+ `init` never ask. Scripts that ran `cap off` or `guard uninstall` unattended
568
+ must now run them at a terminal.
569
+ - The guard judges Sakk's own commands by the write they make: from the agent,
570
+ `sakk cap …` is denied (`protect-guard-state`), `sakk guard install|uninstall`
571
+ asks (`ask-agent-config`; renamed and tightened to `deny-agent-config` in
572
+ this release — F-188), and `script`, `expect` and `unbuffer` ask
573
+ (`ask-constructed-commands`), because they can type into a prompt.
574
+ - Limits, stated in the README and docs/GUARD.md: the confirmation checks for a
575
+ terminal, not a person; Sakk is known by the name `sakk` only.
576
+ - Known gap, not yet fixed (F-177): `resume`, `reconcile` and `unlock` from a
577
+ guarded agent are allowed, and the default ledger is a project file the
578
+ agent may write, so one agent can lift a halt on another.
579
+
580
+ ### Changed — the README states the guard's limits (REQ-P04, F-173)
581
+
582
+ - The README's limits section now leads with the five the requirements name: a
583
+ command built at run time is only asked or denied; `@` references skip the
584
+ hook; the cap stops the next call; the agent can reach the local logs; older
585
+ Claude Code fails open. The first states where the secret guard stops: a
586
+ name the shell or a program picks at run time (glob, brace, variable,
587
+ `grep -r`, `find -exec`, `xargs`) is judged as written, so such a read of
588
+ `.env` is allowed. The behaviour is unchanged; a test pins both sides.
589
+ - Known gap, not yet fixed (F-176): a delete, write or flag held in a shell
590
+ variable is allowed wherever it points, including `~/.sakk` and
591
+ `git push --force`. The README, docs/GUARD.md and docs/SPEND.md say so.
592
+
593
+ ### Fixed — `sakk halt` now stops the guarded agent (F-172)
594
+
595
+ - No command wrote `ledgerPath` into `~/.sakk/config.json`, so the hook never
596
+ saw a ledger: `sakk halt` recorded a halt the installed guard could not read
597
+ and the agent kept running. `sakk init` now records the ledger's absolute path
598
+ there, and the CLI, the hook and the console's stop button resolve the ledger
599
+ by one shared rule (`--ledger` > the recorded path > `SAKK_LEDGER_PATH` > the
600
+ CLI's `./sakk-ledger.json`), so a halt lands where the hook reads. Re-running
601
+ `init` wires an existing ledger without touching it and never replaces a
602
+ different recorded path.
603
+
604
+ ### Fixed — the shipped price snapshot (F-169)
605
+
606
+ - The shipped `prices/snapshot.json` priced none of the current Claude models,
607
+ so out of the box every metered run was refused as `PRICE_UNKNOWN`. It is now
608
+ regenerated from the LiteLLM table (170 models) by `scripts/prices-trim.mjs`,
609
+ which also writes the third-party credit (`LiteLLM (MIT, (c) Berri AI)`) into
610
+ the snapshot's provenance block. An unknown model still fails closed.
611
+
612
+ ### Fixed — the guard says when it would fail open (F-174)
613
+
614
+ - On Claude Code older than 2.1.295 the hook cannot be installed with
615
+ `onFailure: "block"`, so a hook that times out lets the call run. `sakk guard
616
+ install` now says so, and `sakk doctor` warns on any installed Claude Code
617
+ hook that lacks the field.
618
+
619
+ ### Fixed — spend accounting correctness (F-168, F-170)
620
+
621
+ - An empty `cache_creation:{}` object (which a trimmed or replayed log can emit)
622
+ was read as an authoritative all-zero breakdown, so cache-write tokens were
623
+ priced at `$0`. `costOf` now honours the split only when it enumerates tokens
624
+ and otherwise falls back to the `cache_creation_input_tokens` aggregate, so
625
+ those tokens are billed the same as with no split key (F-168).
626
+ - A fractional token count (e.g. `input_tokens: 1.5`) passed the spend parser's
627
+ validation and then threw deep in pricing, crashing `sakk spend`. Token counts
628
+ are now screened as whole numbers at the parse boundary — a non-integer line is
629
+ marked unreadable — and `daySpend` isolates any pricing throw as one unreadable
630
+ line, so the cap fails closed via `SPEND_UNREADABLE` instead of aborting
631
+ (F-170).
632
+
633
+ ### Fixed — the release certificate's mutant-freshness parser (F-167)
634
+
635
+ - The certificate's drill-freshness parser read mutant ids with a `MUT-[A-Z]`
636
+ regex that was blind to the twelve two-letter mutants (`MUT-AA`…`MUT-AL`):
637
+ their tampered-file hash blocks were never in the verified set, so the
638
+ `freshness: N hashes verified` line a reader relies on could never be issued.
639
+ The parser now reads `MUT-[A-Z]+`, so every mutant the drill emits is in
640
+ reach, and `EXPECTED_MUTANTS` is pinned to the count the drill defines; a test
641
+ fails `npm test` if either drifts from `scripts/mutation-drill.sh`.
642
+
643
+ ### Added — the gitleaks rule port in the secret guard (open-source v1, M3)
644
+
645
+ - The secret guard now runs **192 of gitleaks' 195 rules** (v8.21.2, MIT)
646
+ alongside the eight hand-written ones, in the hook and the commit scan.
647
+ `scripts/gitleaks-port.mjs` reads the pinned `config/gitleaks.toml` and
648
+ generates `src/secret-rules.generated.mjs`, translating RE2 to JS (a leading
649
+ `(?i)` to the `i` flag, a mid `(?i)`, `(?i:…)` and `(?-i:…)` by case-folding,
650
+ `(?P<>)` to `(?<>)`, `\z` to `$`) into syntax Node 20 runs; it refuses any
651
+ construct it does not know (1 rule, in `REFUSED`), never guessing.
652
+ - Rules are keyword-gated and filtered by gitleaks' global allowlist and
653
+ per-rule entropy; one secret matched by two rules is reported once. The port
654
+ is deterministic (a re-run reproduces the file byte-for-byte) and every
655
+ enabled rule is **linear on hostile input** — the `(?:.|\s){0,N}` span idiom,
656
+ which backtracks catastrophically in JS, is rewritten to `[\s\S]`.
657
+ - Two rules ship disabled as reviewed exclusions: `generic-api-key` (too broad
658
+ — it flags ordinary code, and in a fail-closed guard a false positive denies
659
+ a legitimate write) and `curl-auth-user` (a RE2 pattern that is not linear in
660
+ JS). The repository scans clean with the rest enabled.
661
+ - The full per-rule recall corpus is the remaining C-13 stopline
662
+ (`specs/secret-guard`, due 2026-10-28): gitleaks' own samples are generated
663
+ by Go helpers, not literal strings.
664
+
665
+ ### Added — the spend and cap CLI (F-161 follow-up)
666
+
667
+ - `sakk cap set <usd> [--zone <IANA>]` / `sakk cap off`: set or lift the daily
668
+ cap the hook enforces at the next call. A person runs it; the agent is denied
669
+ writing `~/.sakk` directly by the guard (rule `protect-guard-state`), and a
670
+ one-command symlink/hard-link chain that would reach it is not silently
671
+ allowed (it is treated as constructed and needs a person's approval, F-163),
672
+ so the agent cannot raise its own cap by a command that names the file (a
673
+ path held in a shell variable is not caught yet, F-176). The amount must be a positive
674
+ plain decimal (`CAP_USAGE`) and the zone a real IANA name (`CAP_ZONE_INVALID`).
675
+ - `sakk spend [--day <YYYY-MM-DD>] [--json]`: a day's spend by model and what
676
+ could not be priced, read from Claude Code's local logs and labelled an
677
+ estimate, not the invoice; the day boundary follows the configured zone. A
678
+ malformed `--day` is `SPEND_USAGE`.
679
+ - `sakk prices [--json]`: the price table in use and its source file.
680
+ - These complete M2's documented surface (the library and the hook's
681
+ enforcement already shipped; see F-161). A fetching prices updater — the
682
+ one network call — was then still planned; it was never built and is
683
+ retracted (F-175).
684
+
685
+ ### Added — release packaging (open-source v1, M5)
686
+
687
+ - Ships as a **Claude Code plugin**: `.claude-plugin/plugin.json` wires a
688
+ `PreToolUse` hook that runs `node ${CLAUDE_PLUGIN_ROOT}/bin/sakk.mjs hook
689
+ --agent claude-code` on every tool call, `onFailure: block` (fail-closed).
690
+ Installable with `claude plugin install --plugin-dir <path>`; the manifest is
691
+ the only file a local/npm install needs (no marketplace file).
692
+ - The npm package `files` list now ships `.claude-plugin` alongside `src`,
693
+ `bin`, `policies` and `prices`; `test/release.test.mjs` runs the real
694
+ `npm pack` and asserts the tarball excludes `test`, `specs`, `findings`,
695
+ `eval` and `scripts`, so dev-only files never leak into the package.
696
+ - A detector closes a gate blind spot (F-161): every `sakk <command>` named in
697
+ the README or the CHANGELOG must be a real CLI subcommand, so the docs can no
698
+ longer promise a command the binary does not implement. The package version
699
+ is checked to be one source of truth across the binary and the plugin.
700
+ - Publishing itself (npm publish, making the repository public, a marketplace
701
+ listing) stays a person's action.
702
+
703
+ ### Added — the local console `sakk ui` (open-source v1, M4)
704
+
705
+ - `sakk ui [--port N] [--allow-halt] [--ledger <path>]` starts a read-only web
706
+ console on `127.0.0.1` (never `0.0.0.0`) and prints one address carrying a
707
+ fresh 128-bit token. It shows only what M1–M3 recorded, through the same
708
+ functions the CLI uses: today's spend and cap, recent guard decisions, the
709
+ policy in use with a try box, secret blocks, `sakk doctor`'s checks, and the
710
+ model prices — plus an "Enterprise · coming soon" panel.
711
+ - Security: the server binds loopback only; every request carries the token
712
+ (first in the query, then a `SameSite=Strict; HttpOnly` cookie) and is
713
+ checked against the `Host`; a write (stopping an agent, behind
714
+ `--allow-halt`) also needs the `Origin` and a token header; the token
715
+ compare is constant-time and refusals are counted and shown. Responses carry
716
+ a strict CSP (`default-src 'none'`), `nosniff` and `no-referrer`, and never a
717
+ CORS header. The page is one HTML file with inline, nonce'd script and style,
718
+ no external origin, and renders every value with `textContent` so a log
719
+ string is never HTML. The decision log is read in a bounded tail, so a 5 GB
720
+ log is never loaded whole.
721
+ - Read-only by default. The one write stops an agent via the ledger's `halt()`,
722
+ recorded with the OS user name and labelled self-declared; resume stays on
723
+ the command line. New refusal codes: `UI_PORT_INVALID`, `UI_PORT_PRIVILEGED`,
724
+ `UI_PORT_IN_USE`, `CONSOLE_NO_LEDGER`, `CONSOLE_BAD_BODY`, `CONSOLE_BAD_AGENT`,
725
+ `CONSOLE_TRY_EMPTY`. Export is CSV and JSONL; FOCUS and evidence bundles are
726
+ the enterprise edition.
727
+
728
+ ### Fixed — M4 console, from the independent review (F-162)
729
+
730
+ - `GET /api/agents` is now read-only and subprocess-free: it reports each hook's
731
+ installed state via `doctor({probe:false})` instead of firing the hook
732
+ (`spawnSync`) and creating Sakk's home, so a read-only request can no longer
733
+ run subprocesses or block the event loop. It also passes the OS home to
734
+ `doctor`, fixing a path bug. `GET /api/overview` no longer updates the spend
735
+ index — a GET writes nothing. The halt record is labelled "self-declared (the
736
+ OS account name)", the CSV export neutralises spreadsheet formula injection
737
+ (leading `= + - @`), and the spec no longer claims a port-scoped cookie.
738
+
739
+ ### Added — the secret guard in the hook (open-source v1, M3)
740
+
741
+ - A write whose content is a secret, and a shell command (or `apply_patch`
742
+ body) whose text contains a literal secret, are now denied inside the hook
743
+ (`evaluate` step 3), scanned once per call through the existing eight-rule
744
+ `scanText`. This runs for every agent (claude-code, cursor, codex) and over
745
+ every write surface (Write `content`, Edit/MultiEdit new text, NotebookEdit,
746
+ `apply_patch`). Reading a secret file was already denied by the `secret.read`
747
+ rule and is unchanged.
748
+ - The deny names the matched rule and a keyed fingerprint, never the secret
749
+ value; the decision-log line drops the reason text and the content, so the
750
+ value reaches neither the agent nor `~/.sakk/decisions.jsonl`. A scan that
751
+ cannot run fails closed as `HOOK_SECRET_SCAN_FAILED` (a deny, via the
752
+ guard's rules-unknown path). New refusal code: `HOOK_SECRET_SCAN_FAILED`.
753
+ - Deferred: the gitleaks 222-rule port (`specs/secret-guard/`, "Status") needs
754
+ the upstream rule file fetched at a pinned commit (a network download) and
755
+ waits on approval; the hook ships the eight rules until then.
756
+
757
+ ### Added — spend and the daily cap (open-source v1, M2)
758
+
759
+ - A daily spend cap the guard enforces at the next tool call. Spend is read
760
+ from Claude Code's local transcripts (`~/.claude/projects/**/*.jsonl`) and
761
+ priced: each call counted once at its largest recorded usage (dedup by
762
+ `message.id`+`requestId`, max-merge); cache writes priced per duration
763
+ (5m/1h); the whole request billed at the long-context rate past a model's
764
+ threshold. The cap, zone and log folder live in `~/.sakk/config.json`, read
765
+ by a person's configuration — not from the agent's environment — and writing
766
+ `~/.sakk/` is denied by the guard (directly, or via a chained link that needs
767
+ approval, F-163), so an agent cannot silently raise its own cap.
768
+ Fail-closed: a model with no price (`PRICE_UNKNOWN`) or an unreadable log
769
+ line (`SPEND_UNREADABLE`) is a deny while a cap is set.
770
+ - A small, approximate starter price snapshot ships in `prices/` (schema
771
+ `sakk-prices.v1`); unlisted models fail closed until a newer table is placed
772
+ at `~/.sakk/prices.json`. Today's spend and the cap are viewable in the
773
+ console (`sakk ui`, M4). The spend index (`~/.sakk/spend/`) is incremental
774
+ and only grows — a deleted or rewritten log keeps what was counted. The
775
+ figures are an estimate from local logs, not the provider's invoice.
776
+ - New refusal codes: `PRICES_INVALID`, `PRICES_UNREADABLE`, `PRICE_UNKNOWN`,
777
+ `USAGE_INVALID`, `SPEND_UNREADABLE`, `SPEND_CAP_REACHED`,
778
+ `SPEND_TOO_MANY_LOGS`.
779
+ - M2 itself shipped the library and the hook's enforcement, not the CLI; the
780
+ `sakk cap set/off`, `sakk spend` and `sakk prices` commands followed (see the
781
+ F-161 section above). A fetching prices updater — the one network call —
782
+ was then still planned; it was never built and is retracted (F-175).
783
+
784
+ ### Fixed — F-105 archive, from the independent review (F-159)
785
+
786
+ - `sakk ledger verify` now checks the call-id index as well as the segment
787
+ chain, so a lost or wrong index (which silently re-admitted a sealed
788
+ call-id — a double-spend) is caught; `reserve` refuses when the whole index
789
+ is gone but segments are sealed, and `sakk ledger reindex` rebuilds it from
790
+ cold.
791
+ - verify sums the sealed total per segment exactly as it is stored, so an
792
+ intact chain never false-fails on float order.
793
+ - `sakk ledger export --out` reports a typed `LEDGER_EXPORT_FAILED` instead of
794
+ a raw stack. The spec now states verify detects accidental corruption and
795
+ loss, not tampering by a writer who can recompute the unkeyed hashes.
796
+
797
+ ### Added — the ledger's hot/cold split (F-105, schema v3)
798
+
799
+ - Settled entries are sealed into an append-only, hash-chained cold trail
800
+ (`<ledger>.cold/`), with a call-id index (`<ledger>.idx/`), so the live
801
+ `ledger.json` stays small and `reserve`/`settle`/`status` cost the same
802
+ whatever the history — 2万 entries used to cost 96 ms a call, 10万 539 ms;
803
+ now a call is flat. No entry is dropped and nothing is capped.
804
+ - The gate reads only the hot file; its total is `archive.settledUsd` plus the
805
+ hot entries. Cold integrity is on demand: `sakk ledger verify` walks the
806
+ hash chain (exit 1 if a segment is lost or altered), `sakk ledger export`
807
+ rebuilds one flat ledger from cold + hot.
808
+ - Schema `sakk-ledger.v3` (adds the `archive` summary; a ledger becomes v3 the
809
+ first time it seals and an older binary refuses it by schema name). v1/v2
810
+ ledgers read and migrate on first seal.
811
+ - In-flight cap (public API): at most 10,000 reservations may be outstanding
812
+ at once (`LEDGER_FULL`); settling what is open always works. New refusal
813
+ code `LEDGER_ARCHIVE_BROKEN`. `SEAL_AT` (settled kept hot before sealing) is
814
+ a tuning knob via `SAKK_SEAL_AT`, default 1000.
815
+ - The ledger's durable write now uses one shared hardened writer
816
+ (`writeFileAtomic` in `append.mjs`), used for the hot file and the cold
817
+ segments and index shards.
818
+
819
+ ### Added — the agent guard (open-source v1, M1)
820
+
821
+ - `sakk hook --agent claude-code|cursor|codex`: one hook for all three
822
+ agents. Each tool call is classified into actions (`file.read`,
823
+ `file.write`, `file.delete`, `secret.read`, `git.destructive`,
824
+ `prod.delete`, `deploy`, `package.install`, `net.download`,
825
+ `shell.constructed`, `shell.run`, `mcp.<server>.<tool>`) and judged by the
826
+ policy kernel. Any error is a deny with a code.
827
+ - `sakk guard install|uninstall <agent> [--user]`, `sakk try "<command>"`,
828
+ `sakk doctor` (checks the hook really fires).
829
+ - `policies/guard-default.json`: the shipped policy (docs/GUARD.md).
830
+ - Writing outside the project (your home folder and elsewhere, temporary
831
+ folders excepted) now asks first (`ask-write-outside`): the home folder is
832
+ where a manipulated agent would otherwise leave a shell profile or an SSH
833
+ key to persist, so an outbound write is a confirmation, not a silent allow.
834
+ - Scopes (public API): repo-relative, `~sakk/…`, `~home/…`,
835
+ `~outside/<absolute>`, and `~protected/repo|home|sakk` for a delete that
836
+ would take the repository, the home folder or Sakk's state with it
837
+ (F-146).
838
+ - A broken rule file is isolated, never a refusal of every call (RC-09):
839
+ - A project policy that fails to load gives way to the last one that
840
+ loaded, kept in `$SAKK_HOME/policy-good/`. Refusals say so, and
841
+ `alerts.jsonl` records `POLICY_FALLBACK`.
842
+ - With nothing to fall back to, or an unreadable config or ledger, only
843
+ reading the project goes on (`ruleId: rules-unknown`).
844
+ - New refusal codes in hook answers (public API): `HOOK_PAYLOAD_INVALID`,
845
+ `HOOK_INPUT_TOO_LARGE`, `HOOK_POLICY_UNREADABLE`, `HOOK_LEDGER_UNREADABLE`,
846
+ `HOOK_CONFIG_UNREADABLE`, `HOOK_CONTEXT_INVALID`, `HOOK_DEADLINE`,
847
+ `HOOK_PATH_UNREADABLE`, `HOOK_USAGE`.
848
+
849
+ ### Added — bounds on what the books store (F-105, first part; C-04)
850
+
851
+ - New refusal codes (public API):
852
+ - `INPUT_TOO_LONG`: a call id, run id, task id or agent id over 256
853
+ characters, a name over 256, or a halt reason over 1024.
854
+ - `POLICY_TOO_LARGE`: the policy file is over 4 MiB.
855
+ - `ESTIMATE_INVALID` now also refuses estimates below $0.000001.
856
+ - `test/cost-bounds.test.mjs` lists every exported function with the
857
+ refusal that bounds it; a new export without an entry fails the suite.
858
+
859
+ ### Fixed — found while building M1 (F-141 to F-149)
860
+
861
+ - F-141: a checker that could not load locked the repository it guards. It
862
+ now loads its library lazily, and while it cannot load only the gate's own
863
+ files can be repaired.
864
+ - F-142: the adversarial review showed as passed when no key was set. It
865
+ is now skipped at the job (shown as skipped), and a review that cannot be
866
+ made or posted fails.
867
+ - F-143: a hung synchronous child could hold a test run with no bound.
868
+ `test/bounded-spawn.mjs` now bounds every one.
869
+ - F-144: the gate's files are listed once, in `scripts/gate-files.json`.
870
+ - F-145: a wrapper's flag value (`sudo -u root`) is no longer read as the
871
+ command.
872
+ - F-146: a temp-folder allow no longer covers the home folder, and a delete
873
+ of the repository itself is named and denied.
874
+ - F-147: drill ids of any length are recognised.
875
+ - F-148: the guard spec's rule table is compared with the shipped policy.
876
+ - F-149: every mutation-drill anchor is checked by `npm test`, not only by
877
+ the hour-long drill.
878
+ - F-150: the F-105 input bounds no longer lock settled money — a reservation
879
+ made before ids were bounded still settles — and a non-string `runId`,
880
+ `taskId` or `tokens` is now refused (`INPUT_INVALID`) instead of stored
881
+ whole; reconciliation reasons are truncated so a long id cannot jam the
882
+ books.
883
+ - F-151: the cost-bounds detector now accepts only a real test as a guard,
884
+ never a circular self-reference, and covers the non-string inputs.
885
+ - F-152: `max_reservable_usd` is computed once in the ledger and reports 0
886
+ when the headroom is below the estimate floor, so the figure `status`
887
+ shows is one `reserve` actually accepts.
888
+ - F-153: commands that move or destroy files but were not in the classifier's
889
+ set — `dd`, `tar --remove-files`, `rsync --delete`, in-place `perl`/`ruby` —
890
+ are now modelled as the writes and deletes they perform instead of being
891
+ waved through as `shell.run`.
892
+ - F-154: `find -exec`/`-ok` and `xargs` are judged by the command they run
893
+ (a remover deletes, a reader reads), not by the runner's name.
894
+ - F-155: reading a secret file through a command outside the reader list
895
+ (`base64`, `openssl`, `dd if=`) is still a `secret.read`.
896
+ - F-156: a git value-option (e.g. `--namespace x`) can no longer shift the
897
+ subcommand so a `push --force` reads as ordinary work.
898
+ - F-157: while the checker cannot load it now resolves a path's symlink leaf
899
+ (not just its parents), judges a Bash call by its command rather than a
900
+ stray `file_path` key, and denies instead of crashing when the library
901
+ throws a non-error; the loop's shell classifier treats `node --test` as
902
+ read-only only when it injects no module.
903
+ The same review's own detectors are strengthened too: the bounded-spawn
904
+ preload now bounds an explicit null/undefined options slot and the scripts
905
+ that spawn `node --test`, and the ci-honesty detector catches more
906
+ skip/failure-to-green shapes.
907
+ - F-158: the CI base checker runs in its own job with a fresh checkout, so the
908
+ pull request's own checker can no longer move HEAD or write the environment
909
+ to leave the base checker an empty range.
910
+
911
+ ### Changed (release preparation)
912
+
913
+ - Halt, resume, reconcile and unlock records are now described as named:
914
+ they carry the name the operator typed and nothing verifies it (F-126).
915
+ A test fails if a user-facing document gives a record a security
916
+ property that no code provides.
917
+ - README states the open core's limits: self-declared names,
918
+ caller-reported costs, approval-required means refused, voluntary gating.
919
+ Dashboards are no longer listed as not planned; a web console is on the
920
+ enterprise roadmap.
921
+
922
+ ### Fixed (regression of the original modules, findings F-80 … F-115)
923
+
924
+ - Ledger: the concurrency test was sequential and a lockless ledger passed
925
+ it — eight workers now race for real (drill MUT-Z); a ledger reached
926
+ through a symlink or hard link refuses (`LEDGER_PATH_NOT_CANONICAL`)
927
+ instead of forking the books; settle no longer makes a stale ledger
928
+ fresh; alerts and decision logs never follow a planted symlink; a fifo
929
+ ledger or policy refuses instead of blocking.
930
+ - `sakk status` judges reconciliation from both sources, shows a stale
931
+ ledger, exits 3 for either and reports nothing reservable when every
932
+ reserve would refuse; a named `sakk reconcile` confirms stale books.
933
+ - MCP: a nested frame can no longer crash the gate (bounded echo, frame
934
+ isolation); an agent cannot settle its own reconciliation entry; a
935
+ relative `--ledger` is resolved at startup.
936
+ - Policy: `*` inside agent/action names refuses at load; policies that can
937
+ only say yes refuse; decisions validate the path once.
938
+ - Eval: the child environment is an allowlist (operator credentials and
939
+ `NODE_OPTIONS` test flags no longer reach children); a scoring failure
940
+ is that case's FAIL; the answer key is checked in every mode; every
941
+ scorer rule has a negative test.
942
+ - CI: the review workflow actually posts and has a timeout; the timeout
943
+ detector reads every workflow.
944
+
945
+ ### Added
946
+
947
+ - Attack rows for the ledger core, policy kernel, MCP enforcer and gold
948
+ harness environment (E5–E8); campaigns run in the audit and CI; every red
949
+ line names its drill anchors (26 mutants); `specs/ledger`.
950
+
951
+ ### Added (self-evolution loop — docs/SELF_EVOLUTION.md)
952
+
953
+ - `npm run brief -- <paths>`: before a change, the past findings for those
954
+ files, the questions to answer first (`findings/prevention.json`), the
955
+ known blind spots of the checks that will run, and those checks. The
956
+ commit gate prints it.
957
+ - The ledger gate now requires `areas` on every finding, `since` on every
958
+ class-mechanism reference, and for every escape past an existing
959
+ detector a `mechanism_gap` and `mechanism_upgrade`; every class has a
960
+ prevention question; two escapes in a row that upgraded nothing need a
961
+ stop-line review. `findings-report` adds the past-detector escape rate
962
+ and the detector upgrade rate.
963
+ - Rule R-22; AGENTS.md "Before you change anything"; PR template section.
964
+
965
+ ### Fixed (independent review on a second model, findings F-62 … F-79)
966
+
967
+ - Evidence bundles: a REAL run must name its model (verifier and
968
+ builder); every run records `harness_dirty` in `metrics.json`; a REAL
969
+ run on a modified or uncheckable harness tree is INCOMPLETE and its pack
970
+ requirements read `HARNESS_MODIFIED`, never `COVERED`; pack reports and
971
+ `bundle.json` are closed (every key required). The auditor procedure now
972
+ runs the CURRENT verifier from a clone — a bundle can ship an older
973
+ genuine verifier that misses later checks.
974
+ - Gold runner: a copy of sakk inside another git repository refuses
975
+ (`HARNESS_GIT_REQUIRED`) instead of recording that repository's commit.
976
+ - Secret scan: an exempt fiction-token match no longer hides a real secret
977
+ it swallowed; `--staged` ignores git replace refs; PGP private-key
978
+ blocks are detected; `--install-hook` writes pre-commit AND
979
+ pre-merge-commit hooks (both or neither) and the hook refuses with
980
+ `SCAN_HOOK_STALE` after a node upgrade instead of a bare shell error.
981
+ - Kill switch: halt and resume keep `updatedAt`, so a stop never makes a
982
+ stale ledger fresh; `halt.lifts` keeps who lifted which halt in the
983
+ ledger (v2 halt shape, unreleased); blank names refuse; `sakk status`
984
+ escapes operator text.
985
+ - Case packs: `gaps` and `not_covered` must be non-empty; pack evidence
986
+ sentences state what the cases check (G03 markers, G06's reference gate,
987
+ git facts the agent can rewrite).
988
+
989
+ ### Added (class detectors)
990
+
991
+ - `test/bundle-absence.test.mjs`: deletes every key of every bundle
992
+ document; any deletion that still verifies must be declared.
993
+ - Secret-scan context matrix (every secret shape × every fiction-token
994
+ position); ledger-write classification (confirms the books or not);
995
+ the stopwatch detector covers deadlines; the repository self-scan test
996
+ scans the root, not a list of directories.
997
+ - Mutation drill: 23 anchors (was 18).
998
+
999
+ ### Known, not fixed
1000
+
1001
+ - F-75: false positives on translation files and Ansible variables
1002
+ (precision needs a benign corpus and a re-measured attack list).
1003
+ - F-76: the scorer reads changed files from the agent's own `.git`;
1004
+ disclosed in every affected pack requirement; the fix changes the
1005
+ frozen scorer and needs a decision.
1006
+
1007
+ ### Added (testing-standard audit)
1008
+
1009
+ - `npm run audit`: one command for every layer of the AI-agent testing
1010
+ standard that applies to Sakk — T0 suites, format, secret self-scan,
1011
+ mutation drill, ledger and policy fuzz; T2 `npm run golden-k` (k = 3,
1012
+ pass@1 with Wilson interval, pass^k; FAKE executor = harness only);
1013
+ T3 `npm run attack-asr` (attack list over the kill switch, secret
1014
+ scanning, case packs and evidence bundles, ≥ 5 variants per payload,
1015
+ CLAIMED ASR must be 0, documented non-coverage reported); §14 ledger gate
1016
+ and `npm run findings-report`. Exit codes taken directly; logs in
1017
+ `audit-logs/`. Layer map: `docs/testing-profile.md`.
1018
+ - Mutation drill: 23 anchors (was 7) — one per core guard, including the
1019
+ per-agent halt and resume scoping, the HARNESS_ONLY and HARNESS_MODIFIED
1020
+ rules, the secret-scan exemption (both directions) and staged type
1021
+ changes, the bundle verifier's spend, metrics, parsing, model and
1022
+ closed-report checks, the MCP policy gate, the path matcher, the
1023
+ paid-step cap and the bundle self-check. The certificate expects 23.
1024
+ - `specs/case-packs`, `specs/secret-scan`, `specs/evidence-bundle`: the
1025
+ three-tables files these L1 changes lacked; `test/core-specs.test.mjs`
1026
+ keeps every core file specified and drilled.
1027
+
1028
+ ### Changed (kill switch — behaviour of existing APIs)
1029
+
1030
+ - `reserve()` now refuses an `agentId` that is not a non-empty string
1031
+ (`AGENT_ID_INVALID`); before, `agentId: 7` or `""` was accepted and
1032
+ recorded. `null`/absent stays anonymous. Reason: a numeric id could never
1033
+ match a per-agent halt.
1034
+ - The `sakk` CLI refuses unknown flags, the `--flag=value` form and repeated
1035
+ flags with exit 2 (`USAGE_UNKNOWN_ARGUMENT`, `USAGE_EQUALS_FORM`,
1036
+ `USAGE_REPEATED_FLAG`); before, they were ignored — `sakk resume --by op
1037
+ --agent=bot` lifted the GLOBAL halt.
1038
+
1039
+ ### Added (kill switch)
1040
+
1041
+ - `halt()` / `resume()` and `sakk halt|resume --by <name> [--agent <id>]`:
1042
+ stop all new spending (or one agent's) with a named record stored in a
1043
+ top-level ledger key `halt` — deliberately outside `flags`, which four
1044
+ money paths rewrite wholesale. **Schema `sakk-ledger.v2`**: the first
1045
+ halt upgrades the file to v2, which REQUIRES `halt` (deleting it refuses
1046
+ rather than reading as "not halted"); v1 forbids it; resume keeps v2
1047
+ with an emptied record. Genesis ledgers stay v1. New public codes:
1048
+ `LEDGER_HALTED`, `AGENT_HALTED`, `HALT_BY_REQUIRED`,
1049
+ `RESUME_BY_REQUIRED`, `AGENT_ID_INVALID` (reserve/halt/resume now refuse
1050
+ a non-string or empty agent id — a numeric id could never match a halt;
1051
+ `null` stays anonymous); `sakk status` exits **4** while globally halted
1052
+ (4 wins over 3; both conditions are printed). Agent ids are matched
1053
+ exactly as own keys, so ids like `__proto__` or `constructor` halt and
1054
+ resume like any other.
1055
+ Halting works on stale ledgers and during reconciliation; the CLI
1056
+ retries lock contention; settle stays open for in-flight calls; first
1057
+ halt wins; alerts `HALT_SET` / `HALT_REFUSED` / `HALT_CLEARED`. MCP
1058
+ status shows halts, and no MCP tool can halt or resume. Older versions
1059
+ refuse a v2 ledger by schema name — they can never ignore a stop. Drill gains MUT-G (7 mutants); invariant fuzz gains halt ops and
1060
+ refuses vacuous seeds that never probe a halt.
1061
+
1062
+ ### Added (secret scanning)
1063
+
1064
+ - `sakk scan <paths> | --staged | --install-hook` and `src/secret-scan.mjs`
1065
+ (ported from the predecessor system, English-ized and hardened): eight
1066
+ rules (DSNs with passwords, URL-encoded passwords, AWS keys, `sk-`/`rk-`
1067
+ keys, GitHub tokens, private-key blocks, bearer tokens, password
1068
+ assignments); fingerprint-only findings; a single declared-fiction
1069
+ exemption token, judged on the secret part of a match only (a token in a
1070
+ username exempts nothing). New over the predecessor: staged-blob scanning
1071
+ for pre-commit (read by blob id, so type changes and names like `0:foo`
1072
+ are scanned; submodule pointers skipped and counted), a hook that runs the
1073
+ installed copy by absolute path and is written with `wx` (never overwrites,
1074
+ never writes through a symlink), symlinks skipped (no loops, no escaping
1075
+ the tree), NUL-sniffed binary skipping with UTF-16 decoded as text,
1076
+ keyed fingerprints (random per run, `SAKK_SCAN_KEY` to compare), redacted
1077
+ secret-bearing paths, length-bounded patterns (a 200KB hostile line scans
1078
+ in linear time). Exit 0 clean / 1 findings / 2 the scan could not do its
1079
+ job (`SCAN_PATH_MISSING`, `SCAN_UNREADABLE`, `SCAN_NOT_A_REPO`,
1080
+ `SCAN_HOOK_EXISTS`, `SCAN_HOOK_DIR_MISSING`, `SCAN_FAILED`). Recognized
1081
+ formats only — unquoted `.env` passwords are not detected.
1082
+ Verify-pass round: `--staged` reads each blob id from one `git diff
1083
+ --cached --raw` record (two names that decode alike, or a run from a
1084
+ subdirectory, can no longer hide a blob) and batches blob reads; staged
1085
+ symlink targets are scanned; explicit roots are followed, a fifo/device
1086
+ or a >256 MB input is `SCAN_UNREADABLE`; `--staged` with paths is
1087
+ `SCAN_USAGE`; git missing is `SCAN_GIT_MISSING`; hook write failures are
1088
+ `SCAN_HOOK_UNWRITABLE`, installing from the npx cache is
1089
+ `SCAN_HOOK_EPHEMERAL`; the hook line is shell-quoted and chmod'ed past the
1090
+ umask. Patterns: `sk`/`rk` keys need a digit (identifiers no longer
1091
+ match), `mariadb`/`rediss`/`amqps`/`mssql`/`sqlserver` and empty-user URLs
1092
+ added, aligned `password = "…"` found, a fiction token glued to a real
1093
+ key exempts nothing. UTF-32 and odd-length UTF-16 decoded; names with
1094
+ control characters printed escaped; exit codes set without truncating
1095
+ piped output.
1096
+ Verify round 3: key shapes the digit rule had dropped are back
1097
+ (`sk_live_`/`rk_test_`-prefixed keys need no digit; `sk_<hex>` and `sk-`
1098
+ keys of 40+ chars match); `--no-relative` so `diff.relative=true` cannot
1099
+ hide files from a subdirectory scan; values on the next line
1100
+ (`password:` + newline, wrapped `Bearer`) found; placeholders such as
1101
+ `${REDIS_PASSWORD}` in the secret position are not secrets; a key glued
1102
+ right after the fiction token is found; C1 controls and U+2028/2029 in
1103
+ names escaped; a missing staged object is named as missing;
1104
+ `--install-hook` with paths is `SCAN_USAGE`, without git
1105
+ `SCAN_GIT_MISSING`; ephemeral-cache detection covers npx hash dirs, bunx
1106
+ and pnpm dlx without refusing ordinary paths that contain `_npx`.
1107
+
1108
+ ### Added (case packs)
1109
+
1110
+ - `eval/packs/`: gold cases grouped by regulatory requirement —
1111
+ `cbuae-ai-oversight` (4), `dora-ict-change` (3), `difc-data-protection` (3)
1112
+ requirements over 9 of the 12 cases (G04 and G09 map to no requirement in
1113
+ these packs; control-plane G12 cannot run against a model, and packs admit
1114
+ only `execution_mode: "model"` cases). `gold-runner --pack <id>` runs a
1115
+ pack's cases and writes `pack-report.json` with its provenance (mode, run
1116
+ id, harness commit, stop info). FAKE runs read `HARNESS_ONLY`; only REAL
1117
+ runs can read `COVERED`. Every requirement carries `gaps`, every pack
1118
+ `not_covered` — mandatory fields, so the honest boundary travels with the
1119
+ claim. Refusals: `PACK_UNKNOWN`, `PACK_INVALID`, `SELECTOR_CONFLICT`,
1120
+ `USAGE_EQUALS_FORM`, `USAGE_UNKNOWN_ARGUMENT`, `USAGE_REPEATED_FLAG`,
1121
+ `USAGE_MISSING_VALUE`, `USAGE_MODE_CONFLICT` — the runner now refuses any
1122
+ argument it does not know (a misspelled `--packs x` used to run ALL
1123
+ cases). A REAL pack report names the model (`model`: provider, model id,
1124
+ executor binary, whether `CLAUDE_BIN` overrode it) and whether the harness
1125
+ tree was dirty; `not_covered` is marked non-exhaustive. **Exit code:** a
1126
+ REAL batch that stops early now exits 1 even if every case that ran passed.
1127
+ Verify round 3: `harness_dirty` is computed from the repository top (it
1128
+ could never be true before) and excludes the run's own out dir;
1129
+ `model.executor_binary_resolved` records the file PATH actually ran, and
1130
+ the REAL meaning says the binary is recorded, not verified; empty values
1131
+ and flags the chosen mode never reads refuse (`USAGE_FLAG_NOT_FOR_MODE`).
1132
+ `--validate` checks all packs and reports `packs_ok`.
1133
+
1134
+ ### Added (evidence bundles)
1135
+
1136
+ - `scripts/evidence-bundle.mjs <out-dir> --to <file.tgz>` (`npm run
1137
+ evidence`): packs a run into one self-contained bundle — the out-dir, the
1138
+ case definitions read from the run's own harness commit (`git show`),
1139
+ `bundle.json` (schema `sakk-evidence-bundle.v1`), `MANIFEST.sha256`, a
1140
+ README and `verify.mjs`, a zero-dependency verifier that runs with Node
1141
+ alone. The auditor checks the bundle WITHOUT running its code first:
1142
+ fingerprint = `shasum -a 256 MANIFEST.sha256` against a value received
1143
+ out of band, then `shasum -a 256 -c MANIFEST.sha256` (AUDITOR_REPLAY §6).
1144
+ verify.mjs then checks integrity (regular files only — links are
1145
+ failures, never followed) and internal consistency: one run, one commit,
1146
+ one executor mode cross-checked against every trace's provider; case list
1147
+ vs case directories; verdict vs violations; per-case cost vs its own
1148
+ ledger; every metric except trace_completeness_rate recomputed (pass
1149
+ rates, first-pass yield, counts, costs, p50/p95); pack report recomputed
1150
+ (a FAKE run can only read HARNESS_ONLY). Unreadable, falsy or ill-typed
1151
+ files are named failures (`CASE_FILE_INVALID`, `METRICS_UNVERIFIABLE`,
1152
+ `BUNDLE_META_INVALID`, `NOT_A_REGULAR_FILE`, `MODE_MISMATCH`,
1153
+ `CASE_SET_MISMATCH`, …), never skipped checks. Building refuses
1154
+ (`BUNDLE_SOURCE_INVALID` incl. symlinks and commits the clone lacks,
1155
+ `BUNDLE_TARGET_EXISTS`, `BUNDLE_SELF_CHECK_FAILED`, `BUNDLE_WRITE_FAILED`),
1156
+ verifies the archive itself after unpacking it fresh, and places it with
1157
+ an exclusive link — no partial file is ever left at the target.
1158
+ Authenticity is out of scope until the anchored ledger ships: a full
1159
+ rebuild passes verify.mjs; only the out-of-band fingerprint catches it.
1160
+ Verify-pass round: a third outcome, **INCOMPLETE** (exit 3, like `sakk
1161
+ status`), for a genuine run with unsettled/unreconciled spend, unknown
1162
+ cost, a stopped batch, or selected cases that never ran — such a run used
1163
+ to read PASS while its spend was understated; it now ships labelled and
1164
+ never reads PASS. The runner records `selected_cases` and `stopped` in
1165
+ `metrics.json` (bundles refuse older run dirs). Per-case ledgers are
1166
+ checked against sakk's ledger rules, this run's id and the case's budget
1167
+ (`LEDGER_INVALID`); mode labels and trace model ids join the mode check;
1168
+ pack reports recompute their quoted verdicts, commit, total and stop
1169
+ record; lowercase rule names, duplicate JSON keys (`DUPLICATE_JSON_KEY`),
1170
+ negative durations and empty directories (`EMPTY_DIRECTORY`) fail;
1171
+ `verifyBundle` never throws. The shipped verifier must be committed
1172
+ (`BUNDLE_VERIFIER_DIRTY`) and its commit is recorded (`verifierCommit`);
1173
+ targets must be `.tgz`/`.tar.gz` (`BUNDLE_TARGET_INVALID`), a dangling
1174
+ symlink counts as existing, files over 1 GiB are refused. A SIGINT or
1175
+ SIGTERM during a build lets it finish or fail as a whole — it does not
1176
+ cancel it — and temp files are always removed (SIGKILL excepted).
1177
+ Verify round 3: a truthful overrun past a case cap (sakk flagged it) is
1178
+ INCOMPLETE (`SPEND_OVER_CAP`) and ships, labelled — it used to be
1179
+ unbundleable; an unflagged one is a forgery. Ledgers are checked as
1180
+ closed documents (unknown keys, missing updatedAt, duplicate callIds, an
1181
+ overrun without the flag sakk sets); every paid step in a trace must have
1182
+ a ledger entry and vice versa; estimated costs (`cost_is_estimate`,
1183
+ `real-exec.json` without a cost) are INCOMPLETE; pack reports may only
1184
+ quote selected cases and must name the run's model; stop records compare
1185
+ regardless of key order; hidden-dotfile targets are refused.
1186
+
1187
+ ### Fixed (gold runner)
1188
+
1189
+ - A relative `--out` (e.g. `--out run1`) reached executors and verification
1190
+ commands that run in other working directories, so every case scored
1191
+ FAIL while the same run with an absolute path passed. The out path is now
1192
+ resolved to an absolute path once, at parse time; the printed `outDir` is
1193
+ absolute. Fails safe before and after (no false PASS), but a real-model
1194
+ run with a relative path would have spent budget on wrong verdicts.
1195
+
1196
+ ### Fixed (hardening campaign, 2026-10-16)
1197
+
1198
+ - **C-4 (disk-full could install broken books)**: `writeSync`'s return was
1199
+ unchecked, so a short write under ENOSPC could be fsynced and renamed
1200
+ into place — an atomically installed TRUNCATED ledger (found by the
1201
+ container matrix's tmpfs probe; the post-mortem read came back
1202
+ `LEDGER_MALFORMED`). Writes now account for every byte, zero-progress
1203
+ refuses, any write failure cleans the temp file and surfaces as the new
1204
+ public code `LEDGER_WRITE_FAILED` — and the matrix probe asserts the
1205
+ books remain readable after disk-full.
1206
+ - **C-1**: `sakk-ledger.v1` is a CLOSED schema — unknown top-level keys
1207
+ (incl. a smuggled own `__proto__` key) refuse `LEDGER_SCHEMA_INVALID`.
1208
+ - Campaign harnesses added under `scripts/campaign/` (soak with SIGKILL
1209
+ injection, fault-injection corpus, seeded MCP torture, container matrix
1210
+ with ENOSPC probe); measured lock-contention refusal rate ~0.016% under
1211
+ 8-way chaos (C-2).
1212
+
1213
+ ### Added (policy engine kernel — 🔒L2, design signed 2026-10-15)
1214
+
1215
+ - `sakk/policy` export: `loadPolicy` (validate + SHA-256 over raw bytes) ·
1216
+ `decide` (pure, first-match-wins, mandatory `default_verdict`) ·
1217
+ `logDecision` (hash-pinned JSONL) · `PolicyError`. New public error codes:
1218
+ `POLICY_UNREADABLE`, `POLICY_SCHEMA_INVALID`, `POLICY_VACUOUS_ALLOW`,
1219
+ `POLICY_RULE_SHADOWED`, `CONTEXT_INVALID`, `DECISION_UNLOGGABLE`. The
1220
+ kernel never imports the ledger; enforcers compose the two.
1221
+ - `policies/` reference profiles (`permissive-dev`, `regulated-strict`) ship
1222
+ in the npm package; documented schema at `eval/schemas/policy.schema.json`
1223
+ with a drift-guard test against the kernel's own validation.
1224
+ - Path matcher extracted to `src/match.mjs` — ONE implementation shared by
1225
+ scorer and policy kernel (scope entries are prefix rules, not wildcards).
1226
+ - Mutation drill grows to 6 mutants (MUT-E rule-order, MUT-F vacuous-allow);
1227
+ the pristine control now runs both suites. `npm run fuzz:policy`: seeded
1228
+ policy/context fuzz (5 invariants, benign + adversarial size probes,
1229
+ replayable seed).
1230
+ - Decision log line format (public API): one JSON object per line with
1231
+ `at` (ISO timestamp) · `by` (string|null) · `verdict` · `ruleId`
1232
+ (string|null) · `approversMin` (only on require_approval) · `policyHash`
1233
+ (64-hex) · `context` ({agent, action, scope, estimatedUsd, callId}).
1234
+
1235
+ ### Added (MCP enforcer)
1236
+
1237
+ - `sakk mcp`: zero-dependency MCP stdio server exposing `sakk_reserve` /
1238
+ `sakk_settle` / `sakk_status` — the policy kernel's first enforcer.
1239
+ Ledger path is required (`MCP_LEDGER_PATH_REQUIRED`, no cwd default); a
1240
+ configured policy is loaded once, hash-pinned, and refuses startup on
1241
+ any `POLICY_*` code. Policy refusals surface as `POLICY_DENIED` /
1242
+ `POLICY_APPROVAL_REQUIRED` (public API) with the full decision attached;
1243
+ every decision logs to `<ledger>.decisions.jsonl` beside the ledger. 1 MiB
1244
+ frame cap; contract tests drive the real binary over real frames,
1245
+ including a thinness proof (library replay yields identical books).
1246
+
1247
+ ### Fixed (certificate + MCP adversarial-review round)
1248
+
1249
+ R-05 on PR #3 found 4 merge-blocking defects and 11 more; all closed with
1250
+ attack-shaped regressions:
1251
+
1252
+ - MCP `sakk_settle` swallowed the ledger-halt signal (replied
1253
+ `settled:true` while the whole ledger froze) and truthy-coerced
1254
+ `failedUnknownCost`, breaking thinness — the library's return now rides
1255
+ the response and arguments pass verbatim (a divergence test pins MCP ==
1256
+ library behavior).
1257
+ - Certificate freshness now requires EVERY drill kill to carry a verified
1258
+ hash block (a parse drift silently shrank the verified set); the tree is
1259
+ re-verified right before rendering (TOCTOU); refusals delete any stale
1260
+ certificate; the final write is atomic; `src/mcp.mjs` and the issuer
1261
+ itself join the certified surface; suite summaries with skipped/todo
1262
+ holes refuse; internal failures obey the 0/2 exit contract.
1263
+ - MCP hardening: string args cap at 4096 (`MCP_ARG_TOO_LONG`, public
1264
+ API); batch/primitive frames answer -32600 (never a silent drop); one
1265
+ refusal per oversized flood; set-but-empty `SAKK_POLICY_PATH` refuses to
1266
+ start; `sakk_status` reads with the CLI's Infinity staleness; agent
1267
+ identity reaches the ledger (`agentId`); decision log is per-ledger
1268
+ (`<ledger>.decisions.jsonl`).
1269
+
1270
+ ### Added (gate non-regression certificate)
1271
+
1272
+ - `npm run certificate`: executes every gate (both suites, gold manifest,
1273
+ 6-mutant drill with evidence-freshness verification against the current
1274
+ bytes, both seeded fuzz legs, double-run reproducibility fingerprint) and
1275
+ renders CERTIFICATE.md. Fail-closed: any red gate, stale drill evidence
1276
+ or dirty tree refuses with a `CERT_*` code and emits nothing — the
1277
+ document's existence is the claim.
1278
+
1279
+ ### Fixed (policy kernel adversarial-review round, 2026-10-15)
1280
+
1281
+ The R-05 independent review of the first kernel implementation found 4
1282
+ merge-blocking defects and 8 more; all fixed with attack-shaped regressions:
1283
+
1284
+ - **F1 (critical, fail-open)**: a `default_verdict: "require_approval"`
1285
+ decision carried no `approversMin`, so the reference enforcer's
1286
+ `0 < undefined` comparison acted with ZERO approvals. Every
1287
+ require_approval decision — default path included — now carries it.
1288
+ - **F2**: non-canonical scopes (`prod//x`, `./prod/x`, `/prod/x`) matched no
1289
+ rule and slid past explicit denies into later allows; they now refuse
1290
+ (`CONTEXT_INVALID`). Unicode normalization is the enforcer's documented
1291
+ duty.
1292
+ - **F3**: the reference enforcer counted the same approver twice toward
1293
+ four-eyes; approvers are now a named set excluding the requester.
1294
+ - **F4**: a schema-legal policy could park the quadratic shadow scan for
1295
+ minutes; a work budget now refuses with the new `POLICY_TOO_COMPLEX`, and
1296
+ the fuzz size probe is adversarial-shaped instead of best-case.
1297
+ - F5 decide copies the context BEFORE validating (getter TOCTOU); F6 the
1298
+ log gate accepts only decide()-shaped decisions and serialization
1299
+ failures are typed; F7 duplicate JSON keys refuse (one hashed file, one
1300
+ meaning); F10 glob-looking scope entries refuse at load; F11 context
1301
+ strings cap at 4096 chars; F9 the schema drift guard now catalogs every
1302
+ known kernel-vs-schema divergence honestly.
1303
+ - Verify-pass round (N1–N4): the shadow-scan budget now charges every pair
1304
+ iteration including pre-filtered ones (the guard's own bypass was the
1305
+ last hole — distinct-agent mega-policies spun unbounded); ALL
1306
+ non-canonical scope entries refuse at load, not only globs; the log gate
1307
+ binds `approversMin` to `require_approval` exactly as decide() emits;
1308
+ the signer name obeys the 4096-char cap. Policies beyond ~20k rules must
1309
+ be split (bounded-analysis cost, documented).
1310
+
1311
+ ### Fixed (stranger-test findings, 2026-10-15)
1312
+
1313
+ - README failure-path sample now shows the real recovery sequence: settle the
1314
+ open entry with a human-decided figure, then `clearReconciliation` — the
1315
+ previous snippet refused as written (`RECONCILIATION_ENTRIES_OPEN`).
1316
+ - Eval runner: execution modes from a ZIP/tarball download (no `.git`) now
1317
+ refuse once with `HARNESS_GIT_REQUIRED` (exit 2) instead of eight scattered
1318
+ crashes; `--validate` / `--dry-run` keep working without `.git`.
1319
+ - `sakk reconcile` on a ledger with no halt says `nothing to clear` instead
1320
+ of claiming a clear that never happened.
1321
+ - `sakk status` human output formats money to ≤ 6 decimals (the `1e-6`
1322
+ reserve guard no longer prints a float tail); `--json` keeps raw numbers.
1323
+ - Eval runner main-module detection now compares realpaths: invoked through a
1324
+ symlinked path (macOS `/var` → `/private/var`) the CLI previously became a
1325
+ silent exit-0 no-op — a false green. Regression proves a refusal fires.
1326
+
1327
+ ### Added (release line)
1328
+
1329
+ - docs/AUDITOR_REPLAY.md: the one-page auditor manual — gold-set integrity,
1330
+ deterministic verdict reproduction, judging the judge, and the honest
1331
+ boundary of what is not yet replayable.
1332
+
1333
+ ### Fixed (second-stranger docs pass, 2026-10-15)
1334
+
1335
+ A context-free AI reader walked the five public docs and audited them
1336
+ against the running code:
1337
+
1338
+ - AUDITOR_REPLAY over-claimed "byte-identical" reproduction — run-local
1339
+ fields (runId, durations) differ by construction. It now states precisely
1340
+ what reproduces (judgements and scoring metrics) and ships a **verified**
1341
+ canonical comparison command; defines "guardian test"; marks which
1342
+ evidence files are per-case conditional; aligns the ZIP wording.
1343
+ - README: Install section; `reserve()` throws-on-refusal shown in the
1344
+ sample; crash-between-reserve-and-settle recovery (late settlement by
1345
+ callId) and `estimatedMaxUsd` guidance documented; roadmap ship-state
1346
+ made consistent (eval harness ✅ in-repo, hash-chain/enterprise =
1347
+ roadmap); the two-process race test named.
1348
+ - CONTRIBUTING: quality pipeline rewritten without internal jargon (plain
1349
+ scope tiers, each gate explained); `npm run fuzz` added to the commands.
1350
+ - ARCHITECTURE: enterprise seam column explicitly marked forward-looking.
1351
+ - eval/README: `maintainer-label.json` (reviewer label) and the `PACKET.md` task
1352
+ brief defined at first use.
1353
+
1354
+ ### Added
1355
+
1356
+ - Gold eval harness: 12 cases, deterministic scorer, fixture/gold isolation
1357
+ with a SHA-256 manifest, reviewer label gate, fail-closed real-run
1358
+ authorization chain, recorder-tested real adapter.
1359
+ - Mutation drill (`npm run drill`): four named mutants, each killed by its
1360
+ named guardian test, with a pristine control run.
1361
+ - CLI: `sakk init / status / reconcile` (`npx sakk …`), JSON output, exit 3
1362
+ while reconciliation is required — CI-gateable.
1363
+ - Community files: contributing guide, security policy, code of conduct,
1364
+ issue/PR templates, architecture notes.
1365
+
1366
+ ### Added (v3 quality pipeline)
1367
+
1368
+ - CONSTITUTION.md, AGENTS.md (+ eval/AGENTS.md), docs/RULES.md (R-01…R-13
1369
+ with sources and the first attribution round), specs/ convention,
1370
+ session stop-gate, `npm run fuzz` ledger invariant fuzzing, and an
1371
+ automated per-PR adversarial review workflow (depth scales with the diff).
1372
+
1373
+ ### Fixed
1374
+
1375
+ - Adversarial-review round 2 (the fix batch reviewed author-blind before
1376
+ merge): lock takeover rebuilt on a breaker mutex (judgement+removal in one
1377
+ critical section; crashed breakers fail closed to `sakk unlock`); release
1378
+ authority is a process-local registry; real-mode machine logs relocated
1379
+ out of the evidence store and the gate module copied local (log
1380
+ forgeability by the same-uid adversary is now a DOCUMENTED trade-off,
1381
+ cross-checked by runner verification); fuzz oracle corrected; work-root
1382
+ gate moved first with a real refusal test; --out guard catches crashed
1383
+ runs; CI review workflow is static and base-context only (never executes
1384
+ PR code with a secret).
1385
+ - Review batch D/E/F (16 findings, each with an attack-shaped regression
1386
+ test): atomic single-winner stale-lock takeover + release ownership check;
1387
+ runner-owned evidence paths no longer transit to the real-mode child;
1388
+ real-mode test evidence now reads the machine test log (honest real passes
1389
+ no longer structurally fail); non-object JSON stdout is a structured parse
1390
+ failure; settle overrun records + alerts + halts; flags is schema-required
1391
+ with a dual-source halt and entry-resolving named clearance; retry limits
1392
+ count tool machine logs; canary scan covers all run artifacts; strict case
1393
+ selector (empty/unknown refuse); corrupt log heads fail closed; --out reuse
1394
+ refuses; flag parsing rejects flag-shaped values; tmp-boundary separator.
1395
+ - Durability: ledger writes fsync before the atomic rename (and best-effort
1396
+ directory fsync) — the gate's decision survives power loss.
1397
+ - Cap comparison carries a conservative epsilon so float drift can never
1398
+ fill the cap exactly.
1399
+ - Stale locks from crashed holders on the same host are detected via owner
1400
+ metadata and broken automatically; `forceUnlock`/`sakk unlock --by <name>`
1401
+ cover what automation refuses to judge.
1402
+ - Re-settling a callId with a different amount (or a late unknown-cost flag)
1403
+ is refused with `SETTLE_MISMATCH` instead of being silently ignored.
1404
+ - The real-run authorization gate requires `SAKK_BUDGET_LEDGER_PATH`
1405
+ explicitly and never falls back to `SAKK_LEDGER_PATH`.
1406
+ - CI toolchain pinned (prettier exact version, actions by commit SHA);
1407
+ platform support declared (macOS/Linux).
1408
+
1409
+ ### Added — the ledger, the first cut (2026-10-13)
1410
+
1411
+ - Fail-closed spend ledger (`src/ledger.mjs`): `initLedger`, `reserve`,
1412
+ `settle`, `markReconciliationRequired`, `clearReconciliation`, `readLedger`,
1413
+ `totalsOf`, `LedgerError` — reserve-before-spend, hard caps where reaching
1414
+ the cap refuses, duplicate `callId` refusal, reconciliation-required state
1415
+ that halts all paid calls, atomic lock + temp-rename writes, alert JSONL.
1416
+ - 23-test ledger suite including a real two-process race.
1417
+
1418
+ [Unreleased]: https://github.com/sakk-dev/sakk/compare/v0.4.0...HEAD
1419
+ [0.4.0]: https://github.com/sakk-dev/sakk/compare/v0.3.0...v0.4.0
1420
+ [0.3.0]: https://github.com/sakk-dev/sakk/compare/v0.1.0...v0.3.0
1421
+ [0.1.0]: https://github.com/sakk-dev/sakk/releases/tag/v0.1.0