staysfixed 0.6.2 → 0.7.1

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.
@@ -0,0 +1,403 @@
1
+ # How version 2 works
2
+
3
+ *The difference engine. What it does, in the order it does it, and the exact
4
+ shapes the pieces hand each other.*
5
+
6
+ Version 1 photographed screens and compared pixels. That answers one question —
7
+ *does it still look the same* — and it asks a person to approve every answer.
8
+ Version 2 asks a bigger one: **has anything that already worked changed?** It
9
+ answers it without a person, and it reports only the differences nobody asked
10
+ for.
11
+
12
+ ---
13
+
14
+ ## The loop
15
+
16
+ The agent is the user. It goes like this, and the owner of the product is not in
17
+ it:
18
+
19
+ 1. The agent changes some code.
20
+ 2. The agent calls `staysfixed_check` over MCP.
21
+ 3. Everything unchanged is skipped and never reaches the agent at all.
22
+ 4. What comes back is what changed. The agent already knows what it *meant* to
23
+ change, so the targets are the differences it did **not** intend.
24
+ 5. It fixes those and runs again.
25
+
26
+ The only things that reach a person are the ones no agent may wave through:
27
+ money, signing in, lost data, a crash, or a bug that was already reported once.
28
+
29
+ ---
30
+
31
+ ## The three ideas it rests on
32
+
33
+ ### 1. Measure the wobble. Never guess a tolerance.
34
+
35
+ Every product disagrees with itself a little between runs — a timestamp, an
36
+ animation frame, an id. So the tool runs the **new build twice**. Anything that
37
+ differs between two runs of the same build was not caused by your change: it is
38
+ the product's own wobble, and it is subtracted arithmetically.
39
+
40
+ There are no tolerance settings in version 2. Version 1's `tolerance` block is
41
+ gone. Tolerance knobs are how tools like this die — too loose to catch the real
42
+ thing, too tight to leave switched on.
43
+
44
+ It also catches something no screenshot tool has ever caught. A path that was
45
+ **steady** in the reference and **wobbles** now is itself a finding: the change
46
+ made something unpredictable. That is `newlyUnstable`, and it is reported even
47
+ though no value technically "changed".
48
+
49
+ ### 2. Cheap suspicion, expensive proof.
50
+
51
+ Comparing against the stored record is fast and needs no rebuild, so that runs
52
+ first. Every path that then looks different gets the old build **booted live, on
53
+ this machine, in the same minute**, and walked again. Only differences that
54
+ survive that live re-run are reported.
55
+
56
+ `--paired` skips straight to the expensive half: old build live from the start.
57
+ That is for pre-release, and for the first run on a product with no stored
58
+ record.
59
+
60
+ ### 3. Sequential, never simultaneous.
61
+
62
+ Two builds at the same instant fight over ports, single-instance locks, user data
63
+ directories, databases and relay slots. The value was never in the same *second*
64
+ — it is in the same machine, the same fonts, the same operating system, the same
65
+ data, minutes apart. Runs are sequential with a full state reset between them,
66
+ interleaved journey by journey so drift cannot accumulate.
67
+
68
+ ---
69
+
70
+ ## The seven channels
71
+
72
+ Everything observed is flattened to one shape: **a path, a channel, and a
73
+ value**. One comparison engine then serves every platform.
74
+
75
+ | Channel | What it holds |
76
+ | --- | --- |
77
+ | `meaning` | What the interface says a control is and does — its role, its name, whether it is on, off or disabled. Not the underlying markup, because markup changes when nothing did. |
78
+ | `effects` | What the product sent out into the world: calls made, files written, processes started, things saved. |
79
+ | `complaints` | What the product complained about: console messages, errors, crashes, the code it exited with. |
80
+ | `results` | What the product gave back: what it printed, what it answered, what it offers other code. |
81
+ | `contract` | The doors the source code says exist: routes, exported functions, message channels. Read without running anything. Free, and exact. |
82
+ | `counters` | Rough counts — files written, calls made, doors answered, names exported. Compared exactly. |
83
+ | `pixels` | What it looked like. Used to show a person a problem another channel already found. |
84
+
85
+ ### The address space
86
+
87
+ A **path** is segments joined with dots, read left to right from the widest
88
+ thing to the narrowest: surface, then place, then thing, then the property of
89
+ it. The first segment names the surface, so paths from a phone and paths from a
90
+ terminal sit in one list without colliding.
91
+
92
+ ```
93
+ api.GET./users.status
94
+ cli.build.exit
95
+ ipc.session:create.registered
96
+ screen.home.tree.button:Save.enabled
97
+ ```
98
+
99
+ **How long something took is deliberately NOT in this address space.** It is
100
+ recorded, it is printed in the sentence a person reads beside the address, and it
101
+ is never differenced. A stopwatch on a shared machine measures how busy the
102
+ machine is at least as much as it measures the product — measured here, thirty
103
+ runs of the same one-line program on an idle Mac spread from 48ms to 96ms against
104
+ a bucket boundary at 100ms, so any load at all crossed it and invented a
105
+ difference nobody caused. Every duration goes out through `howLongItTook()` in
106
+ `src/v2/adapters/contract.js`, carries a fixed value on every run, and counts as
107
+ missing coverage rather than as a pass. A build that *hangs* is still caught: it
108
+ is stopped for taking too long, and how it finished is compared exactly.
109
+
110
+ A dot inside one segment is written `%2E` and a literal percent `%25` — use
111
+ `joinPath()` and you never have to think about it. Escaping rather than
112
+ stripping matters: `v1.2` and `v12` are different names, and a stripping scheme
113
+ would quietly merge two different buttons into one address.
114
+
115
+ The full grammar is `PATH_RULES` in `src/v2/observation.js`, and it is printed to
116
+ whoever is wiring the tool up rather than left in a document.
117
+
118
+ ## Where the steps come from
119
+
120
+ Ranked, because this is the real workload question:
121
+
122
+ 1. **Read the code.** Free and exact. Routes, exports, IPC channels.
123
+ 2. **Run the project's own test suite under instrumentation.** Most projects
124
+ already have hundreds of journeys sitting there, walked for a different reason.
125
+ 3. **Recorded real sessions.**
126
+ 4. **The agent exploring one named gap** and freezing it into a replayable file.
127
+ 5. Never a person clicking through an app.
128
+
129
+ `--journeys <source>` picks between them — and **two of those five are written and
130
+ not wired.** What a run actually walks today is (1), plus a journeys file you name:
131
+ each adapter reads your source and offers what it finds there, and `--journeys
132
+ <file>` names steps by hand. The suite harvest and session replay live in
133
+ `src/v2/journeys/` with tests around them, and nothing on the check path calls
134
+ them; ask for `--journeys suite` or `--journeys recorded` and you are told that by
135
+ name rather than handed a clean result about steps something else chose. Saying so
136
+ is the point: a feature that exists in the repository and not in the run is not a
137
+ feature you have.
138
+
139
+ ---
140
+
141
+ ## Four layers of noise control
142
+
143
+ Before anything reaches the agent:
144
+
145
+ 1. **Normalise** volatile shapes by rules kept in git, so a version bump in a
146
+ footer reports zero differences instead of five hundred.
147
+ 2. **Cluster** by signature, so one cause is one finding.
148
+ 3. **Rank by distance from the changed code.** A difference *far* from your edit
149
+ sorts to the **top** — that is the definition of a side effect.
150
+ 4. **Prove causation** by reverting the suspect hunk and running again. That is a
151
+ proof, not a heuristic, and it is cheap.
152
+
153
+ ### What normalising deliberately hides
154
+
155
+ Every normalisation rule is a trade. It buys quiet by making some real
156
+ differences invisible, and pretending otherwise would be dishonest. So the rules
157
+ are **data**, not code: they live in git next to the project's settings, they get
158
+ reviewed like any other change, and every one of them carries a **`wouldHide`**
159
+ field written in plain English — the real change this rule would wrongly cover
160
+ up. Read that field before switching a rule on.
161
+
162
+ The rule that quietens a wobbling clock also hides a genuinely wrong date. The
163
+ rule that quietens a build time also hides a real ten-times slowdown. The rule
164
+ that strips terminal colour also hides green success turning red. None of that is
165
+ a bug; all of it has to be written down.
166
+
167
+ Two things keep it honest:
168
+
169
+ - **`explain()` returns exactly what was replaced, where, by which rule, and what
170
+ that rule admits it might be hiding.** A difference the tool decided not to show
171
+ has to be answerable for.
172
+ - **The test suite asserts each blind spot exists.** `test/v2/normalise.test.js`
173
+ fails if a rule ships without a `wouldHide`, and fails again if a recorded blind
174
+ spot silently stops being one — because that means the trade changed and nobody
175
+ wrote it down again.
176
+
177
+ ## Where the approval line sits
178
+
179
+ The word "approve" was hiding four different decisions.
180
+
181
+ 1. **What counts as working** — the owner, and only the owner. But never by
182
+ opening this tool. The reference is cut by an act already performed: saying
183
+ ship. Approval happens in bulk, retrospectively, by shipping.
184
+ 2. **Is this difference real or is it noise** — the machine, arithmetically, from
185
+ running the new build twice. No judgement, nobody's opinion.
186
+ 3. **Did my own edit cause this** — the agent. That is a *causal* claim, which is
187
+ checkable: revert the suspect hunk, run again, and if the difference survives
188
+ the revert the agent was wrong and it escalates.
189
+ 4. **Is an unintended difference acceptable anyway** — the owner. This is the
190
+ only thing that reaches a person, and it should be a handful of items a month.
191
+
192
+ **An agent cannot write a reference.** It can only write a waiver, through four
193
+ machine-checked gates: sealed classes are unwaivable; the waiver must agree with
194
+ an intent the agent sealed **before** the run, so it has to say what it meant to
195
+ change before it sees what broke; five waivers per change and no more; and every
196
+ waiver is fingerprinted to one exact difference and expires when the reference
197
+ moves.
198
+
199
+ ---
200
+
201
+ ## The shapes
202
+
203
+ These are the seams between the modules. The authoritative versions are the
204
+ JSDoc typedefs in `src/v2/types.js` and `src/v2/run.js`; this is the short read.
205
+
206
+ ### An observation
207
+
208
+ One fact seen during a run.
209
+
210
+ ```js
211
+ { path: 'screen.settings.tree.button:Save.enabled', channel: 'meaning', value: true }
212
+ ```
213
+
214
+ Values are JSON-safe: strings, numbers, booleans, null, and arrays or plain
215
+ objects of those. `makeObservation()` refuses anything else, and it refuses a
216
+ path that breaks the grammar, so a bad address can never reach the store.
217
+
218
+ ### A capture
219
+
220
+ One journey, walked once, against one build.
221
+
222
+ ```js
223
+ {
224
+ id: '20260829-013245-a-3f9c1a',
225
+ journey: 'the shop opens',
226
+ build: { /* fingerprint */ },
227
+ run: 'a', // 'a' and 'b' are the two runs of the same build
228
+ startedAt: '2026-08-29T01:32:45.000Z',
229
+ durationMs: 4120,
230
+ observations: [ /* … */ ],
231
+ coverage: { /* what it did NOT manage to look at */ },
232
+ complete: true, // false when the file was read back torn
233
+ }
234
+ ```
235
+
236
+ ### A difference
237
+
238
+ ```js
239
+ {
240
+ path: 'cli.build.exit',
241
+ channel: 'results',
242
+ kind: 'changed', // 'changed' | 'appeared' | 'vanished'
243
+ reference: 0,
244
+ candidate: 1,
245
+ journey: 'the shop opens',
246
+ distance: 1, // how far apart the two values are, not how far from your edit
247
+ }
248
+ ```
249
+
250
+ `vanished` is the one no screenshot comparison has ever noticed: a door that
251
+ closed.
252
+
253
+ ### Wobble
254
+
255
+ What one build disagrees with itself about, measured by running it twice.
256
+
257
+ ```js
258
+ {
259
+ buildId: 'abc1234',
260
+ journey: 'the shop opens',
261
+ runs: ['…-a-…', '…-b-…'],
262
+ entries: [ /* WobbleEntry per unsteady path */ ],
263
+ unstable: ['counters.boot.ms'],
264
+ steady: 417,
265
+ measured: true, // false when the build was only run once — and it says so
266
+ }
267
+ ```
268
+
269
+ `subtractWobble(differences, wobble, { referenceWobble })` returns
270
+ `{ real, noise, newlyUnstable, couldTellNewlyUnstable, couldNotTell, note }`. The
271
+ rule is set subtraction and nothing cleverer: if a path will not sit still between
272
+ two runs of the same build, a difference at that path proves nothing, whatever its
273
+ size. Any "but it changed by MORE than the wobble did" rule is a tolerance wearing
274
+ a disguise.
275
+
276
+ `couldNotTell` is the guard on the one way that rule can go catastrophically
277
+ wrong. If the second run of the new build falls over half way, or the product
278
+ writes hash-named files, or stamps a fresh id on every line it prints, then most
279
+ of its addresses are unsteady, nearly every difference is dropped as noise, and
280
+ what is left is not an answer — but it reads exactly like a clean run. So
281
+ `wobbleStorm(wobble)` looks at the share: a build that disagrees with itself about
282
+ more than half of its own addresses did not wobble, something went wrong with the
283
+ run. The verdict is then **not** ok, the summary opens with `NO ANSWER FROM THIS
284
+ RUN`, and the reason is in the coverage as a hole. This is not a tolerance — no
285
+ number here decides whether any single difference is real. It decides one thing:
286
+ whether this run has earned the right to use the word clean.
287
+
288
+ ### A finding
289
+
290
+ A cluster of differences that share a cause, which is what the agent reads:
291
+
292
+ ```js
293
+ {
294
+ id: 'f3a91c', // stable while the cause persists, so a waiver can pin to it
295
+ signature: 'meaning:changed:button-name',
296
+ channel: 'meaning',
297
+ kind: 'changed',
298
+ what: 'Every Save button is now called Store.',
299
+ count: 37,
300
+ journeys: ['the shop opens'],
301
+ paths: [ /* a handful, for orientation */ ],
302
+ examples: [ /* Difference */ ],
303
+ where: 'src/ui/Button.jsx',
304
+ provenAgainst: 'the old build, run live',
305
+ distance: 6, // steps through imports from the nearest file you changed
306
+ sealed: null, // or one of the five classes
307
+ waivable: true,
308
+ why: 'Why it sorted where it did, in plain English.',
309
+ }
310
+ ```
311
+
312
+ ### A verdict
313
+
314
+ What `staysfixed check --json` prints and what the MCP tool returns:
315
+
316
+ ```js
317
+ {
318
+ ok: false,
319
+ compared: true, // false when there was nothing to compare against yet
320
+ headline: 'Three things changed that nobody asked for.',
321
+ findings: [ /* most suspicious first */ ],
322
+ reference: { kind: 'marker', label: 'v0.13.0', bootable: true },
323
+ provenAgainst: 'the old build, run live',
324
+ notes: [ /* warnings a reader must not miss */ ],
325
+ missingCoverage: [ /* what was refused rather than run twice */ ],
326
+ counts: { unchanged: 4118, wobble: 412, differences: 44, droppedAtProof: 39, findings: 3 },
327
+ journeys: ['the shop opens'],
328
+ youChanged: ['src/ui/Settings.jsx'],
329
+ tookMs: { total: 41200, firstPass: 12000, secondPass: 11800, proof: 17400 },
330
+ at: '2026-08-29T01:32:45.000Z',
331
+ }
332
+ ```
333
+
334
+ `compared: false` is not a pass. A run with nothing to compare against has
335
+ proved nothing, and the command line exits with an error rather than letting a
336
+ release through on the strength of it.
337
+
338
+ ### The modules
339
+
340
+ | Module | What it holds |
341
+ | --- | --- |
342
+ | `src/v2/types.js` | Every shape, as JSDoc typedefs. Nothing else. |
343
+ | `src/v2/observation.js` | The address space, the channels, `diffCaptures`, `measureWobble`, `subtractWobble`. |
344
+ | `src/v2/normalise.js` | The rules, as data, each carrying `wouldHide` in plain English, and `explain()` so a normalisation can be answered for. |
345
+ | `src/v2/cluster.js` | Many differences, one cause, one finding. |
346
+ | `src/v2/rank.js` | `rankFindings` — furthest from the edit first, sealed classes above everything — plus `sealOf`, `whatChanged`, `importGraph`. |
347
+ | `src/v2/cause.js` | `proveCause` — revert the suspect hunk, run again, and find out. |
348
+ | `src/v2/store.js` | The append-only capture files, build records, references, and reading a torn file without losing the rest. |
349
+ | `src/v2/run.js` | `runCheck` — the loop, over any `CheckEngine`. |
350
+ | `src/v2/check.js` | `check` — the assembled front door. The command line, the MCP server and the self-check corpus all look for it here, on purpose: if they found the engine in different places they would be checking different things and reporting it as one. |
351
+ | `src/v2/selfcheck.js` | The corpus of deliberately broken builds, and `runSelfcheck`. |
352
+ | `src/v2/adapters/` | One per surface. The only place that knows what a browser or a child process is. |
353
+ | `src/v2/mcp/` | The tools an agent calls. |
354
+ | `src/v2/cli.js` | `V2_COMMANDS`, `run`, `doctorRun`, `checkOptions`, `report`. |
355
+ | `src/v2/doctor.js` | `capabilities()`, `describeCapabilities()`, `CHANNELS`, `onPath`, `reachableHosts`. |
356
+
357
+ ## The tool describes itself
358
+
359
+ Nothing about wiring this up should require a human to read documentation —
360
+ including this page. `staysfixed doctor --json`, and `staysfixed_capabilities`
361
+ over MCP, return the same object, and it is the first call an agent should make.
362
+ It carries:
363
+
364
+ - what it can check on this machine right now, per kind of product, and which of
365
+ the seven channels are reachable for each
366
+ - what is missing, why it matters, and the exact command that would fix it —
367
+ marked with whether the tool can do it itself or a person has to
368
+ - which other machines it can already reach, **detected by dialling them**, so a
369
+ working SSH host is never presented as something to go and set up
370
+ - the shape of its own results, so an agent can act on them without being taught
371
+ - what it will never be able to see, on any machine
372
+
373
+ ---
374
+
375
+ ## Honest limits
376
+
377
+ - **Anything irreversible is watched at the moment it is asked for** — the same
378
+ charge, the same amount, the same place — and never allowed to happen. If a bug
379
+ only appears after the payment settles or the email lands, this tool is blind
380
+ to it, by design and permanently.
381
+ - **A migration that destroys data is refused, not run twice.** The refusal is
382
+ reported as missing coverage, never as a pass.
383
+ - **A run whose wobble swallowed the comparison has no verdict.** It says so in
384
+ those words rather than passing. See `couldNotTell` above.
385
+ - **Two facts at one address lose one of them.** Every index keeps the first, so
386
+ an adapter that gives one name to two things makes the second invisible. Each
387
+ walk is checked and each clash is named in the coverage — but the check finds
388
+ it, it does not fix it. The adapter has to give the two things two names.
389
+ - **Subtracting the wobble hides intermittent bugs.** A race that already existed
390
+ and got worse will not show. Running the new build twice recovers half of this
391
+ by flagging anything newly unstable. Only half. This is the sharpest weakness
392
+ in the architecture.
393
+ - **"Deep" means every door the code exposes and every journey it was given.** Not every possible state — nothing can enumerate that, and any tool
394
+ claiming otherwise is lying. The coverage ledger names the doors it never
395
+ opened, so the hole is visible instead of pretended away.
396
+ - **Real phones cannot be paired.** No paired run is possible on a device in your
397
+ hand; those fall back to the stored record and say so.
398
+ - **Native Windows cannot run two builds at once, even in principle,** because
399
+ Windows shows one desktop at a time.
400
+ - **If the old build no longer compiles**, comparison falls back to the stored
401
+ record. That reintroduces every cross-day difference the paired design exists
402
+ to remove, and it announces itself in those words on every run rather than
403
+ degrading quietly.
package/docs/mcp.md ADDED
@@ -0,0 +1,286 @@
1
+ # Using Stays Fixed from an AI coding agent
2
+
3
+ Stays Fixed speaks the Model Context Protocol, so an agent can check its own work
4
+ the moment it finishes editing — before it tells you it is done.
5
+
6
+ This is where the tool earns its keep. An agent that has just changed twenty
7
+ files has no way of knowing whether it broke the settings page, because it never
8
+ opened the settings page and does not know the settings page exists. Now it can
9
+ ask: it runs your product through the same steps twice, compares that against the
10
+ build you last shipped, subtracts whatever your product disagrees with itself
11
+ about, and hands the agent **only the differences nobody asked for**. Everything
12
+ unchanged is skipped and never reaches its context.
13
+
14
+ **An agent can check, and it can waive within limits. It can never decide what
15
+ "working" means.** That is cut by shipping, by a person, and there is no tool on
16
+ this surface that could move it — not refused, not on the list.
17
+
18
+ ---
19
+
20
+ ## Wiring it up
21
+
22
+ The server runs over stdin and stdout:
23
+
24
+ ```
25
+ npx -y staysfixed mcp
26
+ ```
27
+
28
+ It works out where your project starts from the folder it was launched in,
29
+ walking up parent folders the same way the CLI does — so an editor that starts
30
+ the server in a subfolder still gets answers about the whole project. It answers
31
+ `staysfixed_capabilities` even when the project has no settings file and half the
32
+ engine is missing, because that is the call an agent makes to find out what is
33
+ wrong.
34
+
35
+ ### Claude Code
36
+
37
+ ```
38
+ claude mcp add staysfixed -- npx -y staysfixed mcp
39
+ ```
40
+
41
+ Add `-s project` to write it into the repository's `.mcp.json` so everybody on
42
+ the team gets it, instead of only you.
43
+
44
+ ### Cursor
45
+
46
+ `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for all projects:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "staysfixed": {
52
+ "command": "npx",
53
+ "args": ["-y", "staysfixed", "mcp"],
54
+ "cwd": "/absolute/path/to/your/project"
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Gemini CLI
61
+
62
+ Same shape, in `~/.gemini/settings.json` (or `.gemini/settings.json` in the
63
+ project):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "staysfixed": {
69
+ "command": "npx",
70
+ "args": ["-y", "staysfixed", "mcp"],
71
+ "cwd": "/absolute/path/to/your/project"
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Codex
78
+
79
+ Codex keeps the same fields in TOML, in `~/.codex/config.toml`:
80
+
81
+ ```toml
82
+ [mcp_servers.staysfixed]
83
+ command = "npx"
84
+ args = ["-y", "staysfixed", "mcp"]
85
+ cwd = "/absolute/path/to/your/project"
86
+ ```
87
+
88
+ ### Anything else
89
+
90
+ Every client that speaks MCP over stdio wants the same three things — a command,
91
+ its arguments, and somewhere to run it. The JSON block above is the shape almost
92
+ all of them use; translate it into whatever your client's config file looks like.
93
+
94
+ `staysfixed doctor --json` returns that block already filled in, under
95
+ `wiring.mcp`, and so does `staysfixed_capabilities` with `detail: "full"` — so an
96
+ agent setting this up for somebody never has to copy it out of this page.
97
+
98
+ ### If you have it installed locally
99
+
100
+ Skip `npx` and point straight at it, which is faster to start:
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "staysfixed": {
106
+ "command": "node",
107
+ "args": ["./node_modules/staysfixed/bin/staysfixed.js", "mcp"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## The loop
116
+
117
+ 1. **`staysfixed_capabilities`** — once per session, first. What can be checked on
118
+ this machine, what cannot and why, what is missing and who has to fix it, and
119
+ the exact shape of every reply. After this call an agent should not need to
120
+ read any documentation about this tool, including this page.
121
+ 2. **`staysfixed_intent`** — say what you *meant* to change, before you run
122
+ anything.
123
+ 3. Change the code.
124
+ 4. **`staysfixed_check`** — run it. Only what you did not account for comes back.
125
+ 5. **`staysfixed_explain`** on the two or three you intend to act on, and
126
+ **`staysfixed_prove`** where you want to test whether your own edit really
127
+ caused one.
128
+ 6. Fix those. **`staysfixed_waive`** anything you genuinely meant, within the
129
+ gates. Then check again.
130
+
131
+ ## The tools
132
+
133
+ | Tool | What it does | When the agent should reach for it |
134
+ | --- | --- | --- |
135
+ | `staysfixed_capabilities` | What this machine can check right now, what it cannot and why, what is missing that would unlock more, which other machines it can already reach, and the shape of every reply. Runs nothing. | **First**, once per session, before anything else. |
136
+ | `staysfixed_intent` | Seals what you meant to change: one plain sentence, the files or areas you expect to touch, and the differences you expect. | **Before the check**, and it has to be before — an intent sealed after a run cannot justify anything in it. |
137
+ | `staysfixed_check` | Runs your product through the same steps twice, compares against the last shipped build, and returns only the differences you did not account for, ranked with the ones furthest from your edit at the top. | **After editing and before saying it is done.** This is the one that matters. |
138
+ | `staysfixed_explain` | One finding in depth: every address that moved, both values in full, what class it is in, how far from your edit, the evidence. | On the two or three findings you intend to act on. Never on all of them. |
139
+ | `staysfixed_prove` | Puts the files you suspect back to the reference, runs again, and says whether the difference went away. Nothing is left reverted. | When you are about to fix something and want to know it is yours. |
140
+ | `staysfixed_waive` | Records that a difference was intended. Four gates, and a refusal is final. | Rarely. It is not approval and it makes nothing the new normal. |
141
+ | `staysfixed_coverage` | What was **not** checked: doors no journey has opened, surfaces out of reach, surfaces this copy has no adapter for, anything refused for being irreversible, and what it can never see anywhere. | Before telling anybody a change is safe. |
142
+
143
+ Each of those also carries a short title and the protocol's own flags for what it
144
+ does to the machine, so a client can tell a question from an action without being
145
+ told: `staysfixed_capabilities`, `staysfixed_explain` and `staysfixed_coverage`
146
+ are read-only and idempotent; `staysfixed_check` and `staysfixed_prove` open your
147
+ product, so they are neither; and none of the seven reaches the outside world,
148
+ because none of them does.
149
+
150
+ ### Aiming a check
151
+
152
+ `staysfixed_check` takes `surface` and `at` to point it at one kind of product:
153
+
154
+ ```
155
+ { "surface": "web", "at": "http://localhost:3000" }
156
+ { "surface": "electron", "at": "./release/mac-arm64/YourApp.app" }
157
+ { "surface": "android", "at": "./app/build/outputs/apk/release/app.apk" }
158
+ { "surface": "ios", "at": "./build/YourApp.app" }
159
+ ```
160
+
161
+ Aim it at a kind of product this project does not contain, or that this copy of
162
+ the tool has no adapter for, and **it refuses by name** — it never falls back to
163
+ checking whatever else was lying around and reporting that as your answer. A run
164
+ that did go where it was aimed says so.
165
+
166
+ It also takes `paired` (boot the old build live from the start — slower, much
167
+ stronger, and the right thing before a release), `against` (compare with a named
168
+ marker or commit), `only` (a list of journey names), `limit` and `offset` (paging
169
+ through the *last* run without running anything again), and `format: "json"`.
170
+
171
+ `journeys` names where the steps come from. Today that is the default — what each
172
+ adapter reads out of your source — or a path to a journeys file. `"suite"` and
173
+ `"recorded"` are written in `src/v2/journeys/` and not yet wired into a run: ask
174
+ for either and you are told so by name, rather than given a clean result about
175
+ steps something quietly chose instead.
176
+
177
+ ---
178
+
179
+ ## What comes back
180
+
181
+ Every reply is trimmed hard on purpose. Nothing heavy is volunteered: values,
182
+ evidence and pictures are fetched through `staysfixed_explain`, and each reply
183
+ ends by naming what was withheld and the call that fetches it.
184
+
185
+ - **The headline** — `NOTHING UNACCOUNTED FOR`, or how many differences you did
186
+ not account for and how many of those are sealed.
187
+ - **The arithmetic** — how many ways in were walked, how many differences were
188
+ subtracted as the product's own wobble, how many were already recorded as
189
+ intended. This is what makes the silence legible: "nothing changed" and
190
+ "nothing ran" read identically without it.
191
+ - **What was not checked** — directly under the headline, on clean runs as loudly
192
+ as on dirty ones, and on a clean run also what a clean result *on this machine*
193
+ actually means.
194
+ - **The findings**, worst first. Furthest from your edit sorts to the top,
195
+ because that is what a side effect looks like.
196
+ - **Newly unpredictable addresses**, listed separately. Those were the same every
197
+ run before your change and disagree with themselves now. Nothing looks broken,
198
+ which is exactly why that kind of bug survives for months. A run with any of
199
+ these is not a pass, and they cannot be waived — it is not a difference, it is
200
+ a loss of determinism.
201
+ - **An escalation block**, when something needs a person, written for the person
202
+ and marked to be pasted into your closing summary word for word.
203
+
204
+ Three results are neither a pass nor a failure and must never be reported as one:
205
+
206
+ - **`BLOCKED`** — the check could not be completed. No answer at all.
207
+ - **`NOTHING WAS ACTUALLY COMPARED`** — every journey walked on the new build,
208
+ nothing on record from the old one. Arithmetically clean, and it would let a
209
+ real regression through.
210
+ - **`NO ANSWER FROM THIS RUN`** — the build disagreed with itself about most of
211
+ its own addresses, so almost everything was dropped before it could be
212
+ compared. Fix it by writing a normalisation rule for whatever is moving, not by
213
+ trusting the clean-looking run underneath it.
214
+
215
+ ---
216
+
217
+ ## The waiver gates
218
+
219
+ An agent's only door is a waiver, and it passes four machine-checked gates:
220
+
221
+ 1. **Sealed classes are unwaivable** — money, signing in, losing data, a crash, or
222
+ a difference touching a named guard. Whatever the reason, whoever is asking.
223
+ 2. **The waiver has to agree with an intent sealed *before* the run.** Sealing one
224
+ afterwards is refused, and the refusal says so in those words.
225
+ 3. **Five waivers between one ship and the next.** Sealing another intent does not
226
+ buy five more.
227
+ 4. **Every waiver is fingerprinted to one exact difference** and dies the moment
228
+ the reference moves.
229
+
230
+ Every waiver is counted out loud in the reply. "Nothing changed", "nothing ran"
231
+ and "everything was waived" read identically otherwise, and two of those three
232
+ are a safety net quietly announcing success.
233
+
234
+ `staysfixed_approve` is not merely refused — **it is not on the tool list at
235
+ all**, so an agent never sees a door to push on. An agent that could bless its own
236
+ results would edit the code, notice something moved, approve it, and report
237
+ success, and your safety net would have become a rubber stamp.
238
+
239
+ ---
240
+
241
+ ## Notes
242
+
243
+ **Nothing is guessed about where you are.** The server walks up from the folder it
244
+ was started in to find your project, the same way the CLI does. If it still lands
245
+ somewhere wrong, set `cwd` in the config block or pass `--config`.
246
+
247
+ **Tool calls run one at a time.** Two at once would mean two copies of your app
248
+ fighting over ports, locks and data folders — which is the exact failure this
249
+ whole design rejects. The queue is not optional.
250
+
251
+ **Failures come back as results, not protocol errors.** A tool that fails returns
252
+ its explanation as content with `isError` set, because the agent is supposed to
253
+ read the failure and act on it — a JSON-RPC error is swallowed by the client
254
+ before the agent ever sees the words. A check that found differences is also
255
+ returned with `isError` set, deliberately: it is the flag every client puts in
256
+ front of the agent, and an agent skimming past a real regression is the failure
257
+ this tool exists to prevent.
258
+
259
+ **Nothing is left running.** Anything the tool opened is closed on the way out —
260
+ clean finish, error or interrupt — and it never closes anything it did not start.
261
+ `staysfixed browsers --clean` tidies up after a run that was killed.
262
+
263
+ **Stdout is the protocol.** Every human-readable word goes to stderr. If you are
264
+ debugging a guard, `console.log` in it is safe: the server diverts stdout writes
265
+ to stderr so a stray log cannot corrupt the stream.
266
+
267
+ ---
268
+
269
+ ## The version 1 tools, for anybody who wired them up
270
+
271
+ Before the difference engine, Stays Fixed was a picture checker, and its MCP
272
+ surface was a different set of tools: `staysfixed_screens`, `staysfixed_capture`,
273
+ `staysfixed_status`, `staysfixed_trace`, a picture-shaped `staysfixed_check`, and
274
+ `staysfixed_approve` / `staysfixed_mark` behind explicit opt-ins
275
+ (`mcp.allowApprove`, `mcp.allowMark`, both `false` by default).
276
+
277
+ They still work, unchanged, and they are served by:
278
+
279
+ ```
280
+ npx -y staysfixed mcp --v1
281
+ ```
282
+
283
+ Nobody who wired that up has to change anything. Everything version 1's picture
284
+ check did is also still reachable from the command line — `staysfixed check
285
+ --pictures`, `staysfixed approve` — and pictures still require a person to
286
+ approve them there, for the same reason they always did.