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.
- package/CHANGELOG.md +207 -0
- package/README.md +104 -43
- package/docs/design-v2.md +275 -0
- package/docs/getting-started.md +295 -0
- package/docs/guards.md +226 -0
- package/docs/how-it-stays-stable.md +315 -0
- package/docs/how-v2-works.md +403 -0
- package/docs/mcp.md +286 -0
- package/docs/running-it-in-ci.md +306 -0
- package/docs/watching.md +190 -0
- package/package.json +3 -3
- package/src/cli/index.js +34 -7
- package/src/core/config.js +12 -2
- package/src/v2/adapters/isolate.js +89 -0
- package/src/v2/cause.js +151 -12
- package/src/v2/check.js +345 -3
- package/src/v2/cli.js +71 -12
- package/src/v2/cluster.js +20 -3
- package/src/v2/coverage.js +40 -5
- package/src/v2/detect.js +1413 -20
- package/src/v2/doctor.js +191 -35
- package/src/v2/init.js +480 -57
- package/src/v2/mcp/tools.js +124 -11
- package/src/v2/normalise.js +54 -7
- package/src/v2/observation.js +56 -10
- package/src/v2/rank.js +212 -43
- package/src/v2/run.js +216 -31
- package/src/v2/selfcheck.js +312 -7
- package/src/v2/store.js +269 -45
- package/src/v2/watch/index.js +96 -17
- package/src/v2/watch/window.js +138 -20
|
@@ -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.
|