staysfixed 0.7.2 → 0.9.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 (65) hide show
  1. package/CHANGELOG.md +429 -0
  2. package/README.md +193 -57
  3. package/docs/design-v2.md +24 -4
  4. package/docs/getting-started.md +19 -6
  5. package/docs/guards.md +2 -2
  6. package/docs/how-v2-works.md +12 -11
  7. package/docs/mcp.md +17 -8
  8. package/docs/settings.md +564 -0
  9. package/docs/watching.md +10 -4
  10. package/examples/staysfixed.config.electron.js +17 -6
  11. package/examples/staysfixed.config.web.js +22 -5
  12. package/package.json +2 -1
  13. package/src/cli/index.js +55 -46
  14. package/src/cli/status.js +45 -1
  15. package/src/cli/watch-flags.js +54 -0
  16. package/src/core/config.js +54 -3
  17. package/src/core/paths.js +15 -0
  18. package/src/guard/run.js +70 -3
  19. package/src/report/console.js +50 -6
  20. package/src/run.js +11 -0
  21. package/src/types.js +3 -0
  22. package/src/v2/adapters/android-driver.js +6 -1
  23. package/src/v2/adapters/android.js +97 -2
  24. package/src/v2/adapters/child.js +101 -0
  25. package/src/v2/adapters/contract.js +42 -5
  26. package/src/v2/adapters/electron.js +72 -6
  27. package/src/v2/adapters/http.js +18 -11
  28. package/src/v2/adapters/ios-driver.js +64 -14
  29. package/src/v2/adapters/ios.js +247 -25
  30. package/src/v2/adapters/process.js +783 -71
  31. package/src/v2/adapters/python.js +495 -0
  32. package/src/v2/adapters/source.js +373 -18
  33. package/src/v2/adapters/web-driver.js +134 -24
  34. package/src/v2/adapters/web.js +149 -18
  35. package/src/v2/adapters/windows.js +18 -1
  36. package/src/v2/browsers.js +66 -3
  37. package/src/v2/cause.js +61 -17
  38. package/src/v2/check.js +653 -69
  39. package/src/v2/ci.js +130 -35
  40. package/src/v2/cli.js +65 -42
  41. package/src/v2/cluster.js +220 -14
  42. package/src/v2/coverage.js +43 -176
  43. package/src/v2/detect.js +308 -60
  44. package/src/v2/doctor.js +353 -54
  45. package/src/v2/escalate.js +5 -1
  46. package/src/v2/init.js +183 -66
  47. package/src/v2/intent.js +9 -23
  48. package/src/v2/journeys/from-suite.js +336 -30
  49. package/src/v2/journeys/index.js +99 -6
  50. package/src/v2/mcp/tools.js +90 -16
  51. package/src/v2/normalise.js +169 -23
  52. package/src/v2/observation.js +19 -33
  53. package/src/v2/rank.js +216 -23
  54. package/src/v2/reference.js +160 -24
  55. package/src/v2/remote.js +113 -18
  56. package/src/v2/run.js +103 -14
  57. package/src/v2/sealed.js +0 -20
  58. package/src/v2/selfcheck.js +190 -13
  59. package/src/v2/ship.js +55 -5
  60. package/src/v2/store.js +67 -1
  61. package/src/v2/types.js +12 -2
  62. package/src/v2/waiver.js +64 -54
  63. package/src/v2/watch/events.js +60 -215
  64. package/src/v2/watch/focus.js +14 -4
  65. package/src/v2/watch/panel.js +167 -17
package/CHANGELOG.md CHANGED
@@ -4,6 +4,435 @@ All notable changes to this project are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the version
5
5
  numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ Nothing yet.
10
+
11
+ ## [0.9.0] — 2026-08-30
12
+
13
+ Found the same way as 0.8.0 and one better: the published build was installed as a stranger
14
+ installs it and used on real throwaway products — a café API, a static site, a library, a
15
+ server that spawns its own child — until it said something untrue. And this time the repo's
16
+ own CI was read, which had been **red for ten releases** while every summary said the tests
17
+ passed. It was red for two real reasons, both below.
18
+
19
+ **The tool was breaking the products it was sent to protect**, and on some shapes it never
20
+ came back at all. Those two are why this release exists.
21
+
22
+ ### Fixed — it broke the product, or itself
23
+
24
+ - **Refusing a connection killed the program that made it.** The refusal was delivered by
25
+ emitting `'error'` on the socket; at that instant nothing is listening, and in Node an
26
+ `'error'` with no listener is a thrown exception. `http.get` on Node 22 — the floor this
27
+ package declares — and a bare `net.connect` on every version died with exit 1, and the run
28
+ then reported the user's product as broken. The refusal is real now rather than simulated,
29
+ so the operating system produces it through Node's own plumbing.
30
+ - **`check` printed its whole answer and never exited.** A start command runs through a
31
+ shell, so the server is a grandchild; killing the shell left it alive holding the output
32
+ pipe, so the event loop never emptied. It also orphaned the server. The shell is started as
33
+ its own process group now and the whole group is signalled.
34
+ - **Two agents shipping at once lost what "working" means.** Six ships, all reporting
35
+ success, four records, and the "already the reference" path never firing. The whole cut is
36
+ one at a time per product now.
37
+ - **Every killed run left a whole copy of the project in the temporary folder, for ever** —
38
+ 777 MB of them on an ordinary machine. Abandoned copies are reclaimed; a copy a live run
39
+ owns is never touched.
40
+
41
+ ### Fixed — it said things that were not true
42
+
43
+ - **The agent was told "everything that worked before still works"** about a run that
44
+ compared nothing at all, while the terminal correctly called it no answer. The engine's
45
+ verdict is the floor for the machine surface now, so this class cannot come back one reason
46
+ at a time.
47
+ - **A sealed money change reached the agent labelled `"ordinary"`** — the human text said
48
+ sealed, `waive` refused it, and the JSON said it was waivable.
49
+ - **The seal that exists because somebody was burned before could never fire.** Guard names
50
+ were never passed to the decision, so that class was empty on every run this tool has ever
51
+ done. And a check on a project with a guard in `.staysfixed/guards` printed the word
52
+ "guard" zero times: not run, not counted, not mentioned.
53
+ - **A guard that asks nothing was reported as holding.** An empty `run()` came back as
54
+ "still holds", and would have said so every day for ever.
55
+ - **The library journey never imported anything.** `init` writes `module: "index.js"`; the
56
+ probe treated that as a package name, failed identically on both builds, and the check said
57
+ "Nothing that worked has changed" for ever.
58
+ - **`ship` blessed a build nothing had looked at.** It matched by git commit, so an
59
+ uncommitted edit resolved to an earlier build that had been checked and was clean.
60
+ - **`doctor` said a check "here" covers things in a folder where a check cannot run at all.**
61
+ - **`--only` printed "everything that worked still works"** about a slice.
62
+ - **`init` and `doctor` both offered `check --paired` as the way to record a reference.** It
63
+ cannot; only shipping cuts one, on purpose. `doctor` also marked it as something the agent
64
+ could do — the single thing an agent must never do.
65
+ - **`status` said nothing had happened** one command after a check and a ship.
66
+ - **It called the person's own browser "a separate application from the browser you use"**,
67
+ which is the one case where that sentence matters and the one case it was false.
68
+
69
+ ### Fixed — it could not see, or would not run
70
+
71
+ - **Full Chrome was invisible on Linux and Windows.** Playwright and Puppeteer both unpack
72
+ into `chrome-linux64` and `chrome-win64`; only macOS uses the names this looked for. On
73
+ Linux, the very command this tool tells people to run left a browser it could not find.
74
+ - **`status`, `walk`, `flake`, `approve`, `mark` and `trace` told a website it had no
75
+ screen** — they only knew version 1's `app:`, and `init` writes version 2's `web:`.
76
+ - **The sign-in example `init` writes used two words the tool does not know**, and a step made
77
+ only of unknown words was skipped in silence — so the form was never filled, every page
78
+ behind the login photographed the login page, and the run came back clean.
79
+ - **A browser's throwaway profile outlived an interrupted run.**
80
+
81
+ ### Fixed — it was noisy or unhelpful
82
+
83
+ - **Renaming one heading came back as five findings**, none of them saying "renamed". A thing
84
+ addressed by its own words is now recognised when it is renamed, and its children travel
85
+ with it.
86
+ - **`doctor` connected to every machine in your ssh config, unasked, on the first run.** Nine
87
+ connections on a brand-new scratch project. It is asked for now, and the machines are still
88
+ listed either way.
89
+ - **Every command-line check wrote two rows** to the log `ship` reads.
90
+ - **The `.gitignore` lines `init` writes matched nothing version 2 writes** — 151 untracked
91
+ files and 1.9 MB of run evidence in `git status` after nine checks.
92
+ - `check --json` now carries `notChecked` and `doorsNeverOpened`, which the README had
93
+ promised and only the MCP reply had.
94
+ - `init` no longer names `staysfixed check --product <name>`, which is not an option.
95
+ - The docs said the self-check builds seventeen products; it builds twenty.
96
+ - `process.alsoWatch` is documented.
97
+
98
+ ## [0.8.0] — 2026-08-30
99
+
100
+ The night this was pointed at itself. Everything below was found the same way:
101
+ the published build was installed the way a stranger installs it, and used on
102
+ throwaway products — a plain-Node server, an Express API, a Flask app, a static
103
+ site, a Vite app, a bare Gradle project, an empty Xcode project, a CLI that
104
+ reaches the internet three ways — until it said something untrue. Reading the
105
+ code found none of it.
106
+
107
+ **Two of them were the tool lying about itself**, which is the worst thing this
108
+ product can do: it told every agent that asked that it could read web pages and
109
+ then could not open one, and it reported a clean run over an entire uncovered
110
+ HTTP surface. Both are closed.
111
+
112
+ ### Changed
113
+
114
+ - **`staysfixed init` now runs version 2's setup.** It ran version 1's picture
115
+ setup until now. Version 2's `init` had been written and tested and was wired
116
+ to nothing — the command table deliberately left it out, on the grounds that
117
+ somebody might have `staysfixed init` in a setup script and would get a
118
+ different file. True, and not a reason: version 1's `init` writes settings for
119
+ photographing screens, which is not what this tool does any more, so a new
120
+ project got the wrong shape of file and was told it was set up. `init --json`
121
+ now answers with the fields `docs/getting-started.md` tells an agent to read —
122
+ `plan.project`, `plan.readiness`, `plan.needs.person`, `plan.journeys`,
123
+ `plan.covers.short`.
124
+
125
+ - **`staysfixed check --watch` opens version 2's panel, not version 1's.** This
126
+ is the one flag whose meaning moved, and it is written down here because
127
+ nothing else said so. The panel draws what a difference engine finds —
128
+ journeys, wobble, findings, a verdict — where version 1's drew approved
129
+ pictures, which this tool no longer has. `--pictures --watch` and `--guards
130
+ --watch` still open version 1's panel over version 1's run, so the only person
131
+ whose command changed meaning is the one who typed `--watch` on its own.
132
+
133
+ - **The scratch copy is cloned, not copied.** Every run works in a throwaway copy
134
+ of the project so it can write anywhere it likes without touching the real one,
135
+ and that copy used to be made byte by byte. On the project this was built
136
+ against — twelve gigabytes, most of it an iOS build folder — that is twelve
137
+ gigabytes of real disk per build and twenty-four per check, before a single
138
+ journey is walked. It now asks the filesystem to clone instead: on a Mac that is
139
+ one call per file that copies no bytes at all and shares the blocks until
140
+ something writes to them, and on Linux the same where the filesystem supports
141
+ it. Measured on that tree: **41.5 seconds and no bytes moved.** It falls back to
142
+ a real copy wherever cloning is not available, so a filesystem without it is
143
+ slower here and never wrong. Never a symlink and never a hardlink — both point
144
+ back at the real project, which is the one thing the copy exists to protect.
145
+
146
+ ### Added
147
+
148
+ - **`process.skip` — folders to leave out of that copy.** A project can name
149
+ folders that are not worth copying, and a copy that takes over twenty seconds
150
+ now says so and points at the setting. **The bar is deliberately high: only
151
+ things regenerated on demand and read by nothing.** Anything skipped that turns
152
+ out to matter makes a run pass for the wrong reason, and a false pass is the one
153
+ failure this whole tool exists to prevent. It can only ever add to the built-in
154
+ list — `.git`, `.staysfixed`, `.turbo`, `.nyc_output`, `coverage`,
155
+ `.pytest_cache`, `__pycache__`, `.DS_Store` — because a setting that could
156
+ switch one of those back on would only ever make runs slower.
157
+
158
+ - **`docs/settings.md` — every option, and what each surface needs on the machine.**
159
+ None of the roughly ninety settings keys the adapters read was written down
160
+ anywhere: the README said the reference "lives with the code it configures",
161
+ which is no help at all to somebody who is not going to read the code. Nor was
162
+ anything the code needs installed: the exact commands for Xcode, the iOS
163
+ runtime, the Android SDK and its emulator, and the two separate ways a browser
164
+ can be missing were all sitting in the source as `howToGet` strings and in none
165
+ of the documentation. It also names the things that quietly cost you coverage —
166
+ an Android Play Store image, which refuses root forever so the files an app
167
+ writes cannot be seen; a `web.start` or `http.start` that hard-codes its port; a
168
+ Windows desktop nobody is signed in to.
169
+
170
+ - **The README now lists the silences that are still open,** beside the table of
171
+ the ones that were found and closed. A table of closed bugs on its own reads as
172
+ though nothing is left. Twelve rows, each with the file that would close it.
173
+
174
+ - **`staysfixed browsers` is reachable.** It was written, tested, given a finished
175
+ command entry with a comment saying "wiring it up is one line" — and that line
176
+ was never written. README.md told people to run `npx staysfixed browsers` and
177
+ `npx staysfixed browsers --clean` to clear up after an interrupted run, and both
178
+ answered *"There is no command called browsers"*. Somebody whose disk was
179
+ filling with abandoned browser profiles had no way to clear them and no reason
180
+ to doubt the page telling them there was.
181
+
182
+ ### Fixed
183
+
184
+ - **`staysfixed ship --version 0.14.0` printed `0.7.2` and shipped nothing.**
185
+ `--version` is a global flag meaning "print the tool's version", and it is also
186
+ one of `ship`'s own options meaning "the release that went out was called this".
187
+ The two lists were concatenated, the parser reads switches before values, so the
188
+ global one won: the release script got the tool's version number on standard
189
+ output, exit code 0, and no reference cut. A command's own flags now win any name
190
+ they share with a global one.
191
+
192
+ - **`staysfixed walk --no-snap` answered "I do not know the option --no-snap".**
193
+ `--no-snap` — leave both windows where they are instead of putting them side by
194
+ side — was read by `walk` and declared by nobody. It is now in `walk`'s options
195
+ and in its help, so `docs/watching.md` saying both commands take every panel flag
196
+ is true again.
197
+
198
+ - **The very first run on a project answered `ok: true` to a machine.** With no
199
+ build on record there is nothing to compare against, so the run proves nothing.
200
+ It said exactly that, in words, in the summary, and the command line exited 2 on
201
+ it — but `--json` and the MCP reply both carried `ok: true`, with no `blocked`
202
+ and none of the `NOTHING WAS ACTUALLY COMPARED` wording. An agent reads the
203
+ fields, not the sentence, so the two interfaces that matter most reported a pass
204
+ over a run that compared nothing. That is the exact failure this tool exists to
205
+ prevent, produced by the tool itself. `comparedNothing` fired only on
206
+ per-journey gaps, which a true cold start never records; it now fires on an
207
+ empty `reference.id` as well, and the rest of the codebase was swept for the
208
+ same shape — `ok` computed from "were there findings" rather than from "was
209
+ anything compared at all".
210
+
211
+ - **The README said a fresh install pulls two packages under a megabyte.** It
212
+ pulls three: `playwright-core` became a runtime dependency, which is the browser
213
+ driver without a browser in it — about 13MB, and still nothing that takes
214
+ minutes. The install section now says what actually happens, including that a
215
+ browser already on the machine is found and used before anything is downloaded,
216
+ and that a project already using the full `playwright` is never asked for a
217
+ second copy of the same driver.
218
+
219
+ - **`staysfixed check --guards-only` does not exist and never did.**
220
+ `docs/guards.md` had told people to run it since it was written. The flag is
221
+ `--guards`.
222
+
223
+ - **`staysfixed rules` and `staysfixed browsers` were both named in the README and
224
+ neither existed.** `browsers` has been wired up; there is no `rules` command, so
225
+ the README now says where the rules actually are — one file,
226
+ `.staysfixed/rules.json`, read like any other file in a pull request.
227
+
228
+ - **`--watch-width 100` is refused, not quietly widened.** `docs/watching.md` said
229
+ anything under 240 or over 900 was brought back to the nearest of the two. Under
230
+ 240 is an error with a message; over 900 is brought down.
231
+
232
+ - **`--profile` prints nothing on a difference-engine check.** It works on
233
+ `staysfixed walk` and on the version 1 picture check. The flag is accepted by the
234
+ new check and the timings are not kept, so `docs/watching.md` no longer claims
235
+ otherwise.
236
+
237
+ - **`staysfixed doctor --json` does not fill the project path into its MCP wiring
238
+ block** — `staysfixed init --json` does. `docs/mcp.md` had said both did.
239
+
240
+ - **`docs/design-v2.md` named four tools that were never used.** It is a plan
241
+ rather than a description and says so at the top, but it is also the only
242
+ per-platform prose in the repository, and it told anybody reading it that
243
+ Android needs Appium, UiAutomator2 and a Java runtime, iOS needs WebDriverAgent
244
+ and Windows needs a .NET probe built on FlaUI. None of those is used. What
245
+ shipped needs none of them, and the section now says so before the list rather
246
+ than after it.
247
+
248
+ - **`--color` was accepted by every command and did nothing.** Colour is settled
249
+ before anything else loads, so by the time a command is parsed the answer is
250
+ already fixed and nothing on the command line could turn it back on. `--no-color`
251
+ works and always did — it is handled first thing, before the logging module
252
+ decides. `--color` has been taken away rather than left as a switch that turns
253
+ nothing on.
254
+
255
+ ### Added — products it could not see before
256
+
257
+ - **A hand-written `node:http` server is read.** A product with `createServer`, a
258
+ chain of `req.url === '/x'` and `.listen(PORT)` was reported as "one thing: the
259
+ `notes` command", found zero routes, left the `http` block commented out, and
260
+ said **"a check here covers the `notes` command in full. Nothing is being left
261
+ out."** over an entire uncovered surface. The same product written with Express
262
+ was handled perfectly, so the machinery worked and the reading stopped at
263
+ frameworks. Eight hand-rolled routing shapes are now read — `req.url === '/x'`,
264
+ `.startsWith`, a `switch` on a pathname, `new URL(req.url, base).pathname`,
265
+ `url.parse`, the `split('?')[0]` variant, `if`/`else if` chains, and a path held
266
+ in a constant — and a method is claimed only where the code checks one, `ANY`
267
+ otherwise, never `GET`.
268
+
269
+ - **Python products are checkable.** A Flask app was told "a Python project is not
270
+ being checked… in a language nothing here drives". The honesty was right and the
271
+ conclusion was wrong: the two adapters that would run it read no source at all.
272
+ The process adapter runs a command and compares what it printed, exited with and
273
+ touched; the http adapter boots a server and asks it for routes. Flask, FastAPI
274
+ (including an `APIRouter` prefix) and Django `urlpatterns` are now read, and
275
+ their path parameters become the same "somebody has to give a real value" flow
276
+ every other route uses. Go, Rust, Ruby and PHP get the boot and the command
277
+ without the routes — and every one of them says, **by name**, that the source
278
+ channel is blind for that language, so none can reach "covered in full".
279
+
280
+ - **`--journeys suite` walks the project's own test suite.** It was written,
281
+ tested and reachable by nothing. Each test file runs twice inside the scratch
282
+ copy with the same frozen clock, seed and watcher as everything else, and every
283
+ check is reported by name, along with why each failure failed and a fingerprint
284
+ of the test file itself — so an edited test says plainly that the change is
285
+ yours. It earns its keep: take the penny-rounding out of a `total()` and the
286
+ product's own output does not move by one character, the discovered journeys say
287
+ nothing has changed, and the harvest names the check that turned red. Off unless
288
+ asked for, held to `suite: { budgetMs }` (90 seconds by default, `0` for none),
289
+ and **every file the budget did not reach is named one by one** rather than
290
+ summarised as "some tests were skipped".
291
+
292
+ - **`keepBuilds` — how much of the record is kept.** The store grew for ever: one
293
+ build folder per commit, in your git history, with the function that would have
294
+ stopped it written and called by nothing. Three tiers now, capped by count and
295
+ not by age, and it refuses to remove anything at all if any record could not be
296
+ read.
297
+
298
+ ### Fixed — the tool being wrong about itself
299
+
300
+ - **It could not open a single web page, and said it could.** `playwright` was
301
+ dropped from the dependencies on the reasonable-looking grounds that nothing
302
+ under `src/` imported it. Nothing does — the web adapter reaches it through
303
+ `await import()`, which no search for an import statement can see. So every
304
+ website check on a fresh install answered "Playwright is not installed, so no
305
+ web page can be opened", while `staysfixed_capabilities` told the calling agent
306
+ that web apps and sites could be checked *here and now*. The dependency is back
307
+ as `playwright-core` — the driver without a browser in it, 13MB and no download,
308
+ because this tool already knows how to find Chrome for Testing, Chrome, Edge or
309
+ Chromium and deliberately prefers one that is not the browser you use. A test
310
+ holds it in place by name, and holds `doctor` and the walk to the same answer.
311
+
312
+ - **An Android project with no APK built was reported as covered in full,** and
313
+ then could not be walked at all. iOS already named the missing build and the
314
+ exact `xcodebuild` line; Android now names the Gradle one. A Tauri app read as
315
+ ready on any machine with no Windows host, because `doctor` returns no Windows
316
+ needs precisely when there is no Windows to ask about.
317
+
318
+ - **Version 1 passed a page with a letter missing from its heading.** The picture
319
+ check's default allowed 0.05% of a picture through, described in a comment as
320
+ "enough for font hinting noise, nowhere near enough to hide a missing
321
+ stylesheet". On a 2880×1800 screenshot that is **2,592 pixels**; taking one
322
+ letter out of an `h1` moves **593**. The replacement was measured rather than
323
+ chosen — ten fresh takes of the same build differ by **zero** pixels — so the
324
+ default is now zero, and a project that sets `tolerance.pixels` is told, per
325
+ screen, how many pixels its setting just swallowed.
326
+
327
+ - **The network boundary let everything out.** The watcher injected into every run
328
+ read the host off the first argument, but Node normalises `Socket.connect` to
329
+ `[options, callback]` — so the host read as an empty string, which was in the
330
+ loopback allow-list. `fetch`, `http.get` and `net.connect` to the open internet
331
+ all succeeded and the report came back empty, while the tool's loudest safety
332
+ claim said every outbound connection had been refused. All three are now refused
333
+ and recorded by host and port.
334
+
335
+ - **`npm run start` died silently** — the default start command `init` writes for
336
+ every Node server. The watcher wrapped `process.env` in a Proxy with only `get`
337
+ and `has` traps, so `process.env.X = 'y'` quietly failed, npm could not pass
338
+ `npm_lifecycle_event` to the script, and it exited 1 without printing a word.
339
+ Every trap now forwards through `Reflect`, with `set` taking the target as the
340
+ receiver.
341
+
342
+ - **Every route, and every command, read as never walked.** A door is addressed
343
+ `route.<VERB>.<url>` and `cli.<the command>`; the adapters wrote `api.<journey>`
344
+ and `cli.<journey name>`. So the coverage ledger — the one part of this tool
345
+ that must never be optimistic — was instead permanently and wrongly pessimistic,
346
+ and a genuinely uncovered door was invisible in the noise.
347
+
348
+ - **A normalisation-rules caveat fired on every run, for ever.** A per-run
349
+ temporary folder was baked into a rule pattern and collided with the real
350
+ project-root rule, so the rule set contained a fresh random pattern every run —
351
+ and the real rule was silently deleted, meaning absolute paths under your
352
+ checkout were never normalised at all.
353
+
354
+ - **A committed change could not be ranked.** "The change" meant the working tree
355
+ and nothing else, so the moment an agent committed its work — which is what an
356
+ agent does at the end of a task — the distance measure went blind and every
357
+ finding carried "nothing in the working tree has changed, so there is no edit to
358
+ measure this against" over a change that was perfectly well known.
359
+
360
+ - **The scratch copy copied every byte.** On the project this was built against —
361
+ twelve gigabytes, most of it an iOS build folder — that is twelve gigabytes of
362
+ real disk per build and twenty-four per check. It now asks the filesystem to
363
+ clone: **41.5 seconds and no bytes moved**, measured on that tree. Never a
364
+ symlink and never a hardlink, because both point back at the project this exists
365
+ to protect.
366
+
367
+ ### Fixed — caps that decided things quietly
368
+
369
+ - **A finding's nearby files were cut to five, and the sealed classes are searched
370
+ in that list.** A finding whose sixth file was `src/billing/refund.js` was
371
+ classified `ordinary` — precisely the class an agent may close on its own.
372
+
373
+ - **A waiver was pinned to the first 40 differences and 20 addresses of a
374
+ finding.** A waiver written over a three-hundred-address finding went on
375
+ covering it after the value at address 41, 150 or 299 became something else.
376
+
377
+ - **A wobble storm needed twelve addresses before it counted,** so ten of eleven
378
+ wobbling was not a storm: ten real differences subtracted, and the run passed.
379
+
380
+ - **Eight address truncations with no fingerprint,** across four files. Two long
381
+ labels, two journey names, two log lines or two checkpoints that agreed for the
382
+ first 60 to 200 characters landed on one address and one of the two answers was
383
+ thrown away at the door. Two of them were screenshot filenames, so the picture
384
+ offered as evidence for a finding could be a photograph of a different screen.
385
+
386
+ - **`trimForStorage` cut in characters against a byte limit.** On box-drawing or
387
+ CJK output, 90,000 bytes in produced 180,061 bytes stored — the whole text twice
388
+ — under a marker claiming 24,464 bytes had been left out of the middle.
389
+
390
+ - **Files over 8MB were compared by a size bracket, not a fingerprint,** so a
391
+ build that wrote a different 40MB file compared equal. They are stream-hashed
392
+ now, and the remaining ceiling is named in the ledger.
393
+
394
+ - **An adapter that did not answer in time made its surface READY.** The race
395
+ resolved an empty needs list, and no needs meant covered — so an Android adapter
396
+ hanging for sixteen seconds produced "Covered against the stored record."
397
+
398
+ ### Changed — words a person reads
399
+
400
+ - **The watch window opened on a fifteen-line paragraph.** Its headline was right
401
+ and the engine's entire summary sat underneath it as one grey block. Nothing is
402
+ dropped — the summary is split at sentence ends, two are shown, and the rest sit
403
+ behind one control.
404
+
405
+ - **A finding titled with the tool's own word.** A renamed route read as
406
+ `"declared" is gone`. Twenty-seven such words are now recognised, so it reads
407
+ `"/notes / declared" is gone` instead.
408
+
409
+ - **A sentence that said a thing was now what it already was.** When both sides of
410
+ a change summarised to the same words, the summary was the wrong thing to print.
411
+ The flagship case now reads: *"GET /invoice / body" now has "line" reading "A
412
+ desk lamp ......... 49.99 GBP" where it read "A desk lamp ......... £49.99"*.
413
+
414
+ - **The first error anybody meets writing a guard was raw JavaScript** —
415
+ `page.goto is not a function`. It now says what the object it was handed
416
+ actually has, in one sentence, and suggests a name only when exactly one is
417
+ close.
418
+
419
+ - **The shipped examples were for a shape the tool no longer uses.** Both said
420
+ "copy this to the root of your project"; doing that today gets you a settings
421
+ file the difference engine rejects. Each now says which half it belongs to, and
422
+ points at `staysfixed init` — which beats any example, because it reads the
423
+ project in front of it.
424
+
425
+ ### Removed
426
+
427
+ - **Fourteen exports nothing called.** Ten were genuinely dead. Two were missing
428
+ features and are now wired: `treeMovedSince`, the one thing `intent.js` promised
429
+ and nothing checked, and `RUNNER_KINDS`, which validated nothing while an unknown
430
+ runner kind was silently treated as posix. Two were kept and wired because
431
+ deleting them would have quietly broken a promise the documentation makes.
432
+
433
+ - **`ios.suite` was advertised in a message telling somebody how to switch
434
+ something on.** Nothing anywhere read it.
435
+
7
436
  ## [0.7.2] — 2026-08-30
8
437
 
9
438
  Found the same way as 0.7.1, one step further along: install from npm into an