synartesis 0.5.1 → 0.5.2

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 (3) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +105 -609
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.5.2 — 2026-09-10
6
+
7
+ ### Changed
8
+
9
+ - **The README is a third of its former length** — 714 lines to 210. It had
10
+ grown a full walkthrough and a manifest-authoring guide, both of which already
11
+ exist in the user guide, so it was three documents pretending to be one and
12
+ the first screen was a long way from `npm install`. What is left is the
13
+ argument, the two real terminal shots, install, the four classes, the commands,
14
+ and the limits; the walkthrough and manifest authoring are one link away.
15
+
16
+ It also documents what has shipped since it was last written: `show --live`,
17
+ `show --full`, `undo --force [--yes]`, `undo --replan`, the `l` key and the
18
+ rest of the screen's keys, `SYNARTESIS_SYNC`, and which policies are actually
19
+ proven against a real server rather than merely checked for tool existence.
20
+
21
+ No code changed. npm cannot refresh a package page without a version, and this
22
+ also carries 0.5.1's latency fix to anyone installing from npm.
23
+
5
24
  ## 0.5.1 — 2026-09-10
6
25
 
7
26
  ### Changed
package/README.md CHANGED
@@ -23,59 +23,23 @@ tells you `update_customer` ran forty times, not what the values were before.
23
23
 
24
24
  ## What it looks like
25
25
 
26
- Two commands, both real. The output below was produced by
27
- [`./demo/filesystem-demo.sh`](demo/filesystem-demo.sh) against the official
28
- filesystem MCP server and pasted, not typeset.
29
-
30
- An agent overwrote a file and tried to move another. One command puts the
31
- first back and reports that the second never happened:
26
+ Both shots are real output from [`./demo/filesystem-demo.sh`](demo/filesystem-demo.sh),
27
+ pasted rather than typeset. An agent overwrote a file and tried to move another.
28
+ One command puts the first back and reports that the second never happened:
32
29
 
33
30
  ![synartesis undo, reverting a write and skipping a gated move](https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/brand/synartesis-undo.png)
34
31
 
35
- `skip` is the interesting row. `move_file` is irreversible on that server, so
36
- it was never applied in the first place — there is nothing to undo.
32
+ `skip` is the interesting row. `move_file` is irreversible on that server, so it
33
+ was never applied in the first place — there is nothing to undo.
37
34
 
38
35
  Now the same damage, except a colleague edited the file before you got to the
39
36
  undo. Writing the old contents back would destroy their work, so it does not:
40
37
 
41
38
  ![synartesis undo halting on drift, showing the human's line](https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/brand/synartesis-drift.png)
42
39
 
43
- It stops at the record that moved and exits non-zero. Anything it had already
44
- put back stays put back; `synartesis show` tells you what is left.
45
-
46
- ## What it can and cannot do
47
-
48
- Every tool gets one of four classifications, which you write down in a manifest:
49
-
50
- | Class | Meaning | Example | What happens |
51
- |---|---|---|---|
52
- | `readonly` | Changes nothing | `get_customer` | Recorded, forwarded |
53
- | `reversible` | Prior state can be restored exactly | `update_customer` | State captured before the write; written back on undo |
54
- | `compensable` | Cannot be reversed, but can be offset | `create_charge` | A different call neutralises it |
55
- | `irreversible` | Neither | `send_email` | **Suspended until a human approves it** |
56
-
57
- A tool your manifest does not mention is treated as `irreversible`. That is
58
- deliberate: silently forwarding an unknown destructive call is the one failure
59
- worth avoiding most.
60
-
61
- ## Requirements
62
-
63
- | Tool | Version | Check with |
64
- |---|---|---|
65
- | Node | 22 or newer | `node --version` |
66
- | pnpm | 9 or newer | `pnpm --version` |
67
- | A C toolchain | any | `cc --version` |
68
-
69
- `pnpm` comes with Node via corepack:
70
-
71
- ```bash
72
- corepack enable pnpm
73
- ```
74
-
75
- The C toolchain is only needed if you build from source: npm ships a prebuilt
76
- SQLite binding for common platforms and installs without one. To build from a
77
- clone on macOS run `xcode-select --install`; on Debian or Ubuntu,
78
- `apt install build-essential`.
40
+ It stops at the record that moved and exits non-zero. Anything already put back
41
+ stays put back, and it prints the three ways on: leave it, restore the resource
42
+ and `--replan`, or `--force` to overwrite deliberately.
79
43
 
80
44
  ## Install
81
45
 
@@ -89,612 +53,144 @@ Then, from anywhere:
89
53
  synartesis install
90
54
  ```
91
55
 
92
- That finds what Claude Code, Claude Desktop, Cursor or Codex already list, writes one
93
- policy covering all of it, and points each entry at the proxy. Servers it
94
- recognises get the policy that ships for them and work immediately; the rest
95
- are drafted with every tool held until you say how to undo it. Your config is
96
- copied aside first, `synartesis uninstall` puts it back, and `synartesis
97
- status` says what is covered.
56
+ That finds what Claude Code, Claude Desktop, Cursor or Codex already list,
57
+ writes one policy covering all of it, and points each entry at the proxy.
58
+ Servers it recognises get the policy that ships for them and work immediately;
59
+ the rest are drafted with every tool held until you say how to undo it. Your
60
+ config is copied aside first, `synartesis uninstall` puts it back, and
61
+ `synartesis status` says what is covered.
98
62
 
99
- Each server keeps its own entry and its own proxy, selected with `--server`,
100
- so **no tool is renamed** — the agent sees exactly the names it saw before.
63
+ Each server keeps its own entry and its own proxy, so **no tool is renamed** —
64
+ the agent sees exactly the names it saw before. **Your agent needs nothing
65
+ installed.**
101
66
 
102
- That is for you: the screen, `undo`, `watch`, `approve`. **Your agent needs
103
- nothing installed** — the config block below fetches the proxy on demand.
67
+ Needs Node 22 or newer. npm ships a prebuilt SQLite binding, so no toolchain is
68
+ required unless you build from a clone.
104
69
 
105
- Or the script, which builds from source and links the same two commands:
70
+ Then just:
106
71
 
107
72
  ```bash
108
- curl -fsSL https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/install.sh | bash
73
+ synartesis
109
74
  ```
110
75
 
111
- Or from a clone, if you would rather read it first:
76
+ One screen: what agents have done, what is held for approval, every AI on the
77
+ machine, and undo — all on the arrow keys.
112
78
 
113
- ```bash
114
- git clone https://github.com/ArhaanDev24/Synartesis.git && cd Synartesis && ./install.sh
115
- ```
116
-
117
- The script checks your Node version, builds, and links `synartesis` and
118
- `synartesis-proxy` into the first writable directory already on your PATH. It
119
- edits no shell profile and needs no sudo. Pass `--no-link` to build only.
120
-
121
- ```bash
122
- synartesis --help
123
- ```
124
-
125
- If nothing could be linked, nothing breaks: every command Synartesis prints
126
- spells itself out in whichever form actually runs on your machine.
127
-
128
- ## Walkthrough
129
-
130
- This uses a toy CRM that ships with the repo, so you can see the whole loop
131
- without pointing anything at real data. Run it from a scratch directory.
132
-
133
- ```bash
134
- mkdir -p /tmp/synartesis-demo && cd /tmp/synartesis-demo
135
- ```
136
-
137
- ### 1. Write a policy
138
-
139
- `init` starts a server, asks it what tools it has, and writes a manifest.
140
- Replace `SYNARTESIS` with the path you cloned into.
141
-
142
- ```bash
143
- node SYNARTESIS/dist/cli.js init crm -- node SYNARTESIS/dist/toy-crm.js --state ./crm.json
144
- ```
145
-
146
- For the servers most people start with — files, memory, git, github — `init`
147
- recognises the server and uses the finished policy bundled here, after checking
148
- every rule against the tools that server actually advertises. If a rule names a
149
- snapshot or an inverse the server does not have, the whole policy is dropped
150
- and you get the TODOs instead: a policy whose inverses cannot be called is
151
- worse than none, because it looks done.
152
-
153
- The toy CRM below is deliberately not one it knows, so this walkthrough shows
154
- the manual path.
155
-
156
- `init` prints the path it wrote to. Unless a policy already sits above the
157
- directory you are standing in, that is `~/.synartesis/synartesis.yaml`. Open it:
158
- every tool that isn't a self-declared read starts as `irreversible` with a
159
- `TODO`. **Working through those TODOs is the job.** A finished policy for this
160
- fixture ships in the repo, so copy it into this directory rather than typing it
161
- out — a policy here takes precedence over the one in your home:
162
-
163
- ```bash
164
- cp SYNARTESIS/manifests/toy-crm.yaml ./synartesis.yaml
165
- ```
166
-
167
- Then edit the `args` line so it points at your clone and keeps its data in
168
- this directory. The copy ships with `args: ["dist/toy-crm.js"]`; it needs both
169
- the path to your clone and the `--state` file:
170
-
171
- ```yaml
172
- servers:
173
- crm:
174
- command: node
175
- args: ["SYNARTESIS/dist/toy-crm.js", "--state", "./crm.json"]
176
- ```
177
-
178
- ### 2. Point your agent at the proxy
179
-
180
- Wherever your MCP client lists servers, replace the entry for the server you
181
- want covered with the proxy. For Claude Desktop or Claude Code that is a
182
- `mcpServers` block:
183
-
184
- ```json
185
- {
186
- "mcpServers": {
187
- "crm": {
188
- "command": "node",
189
- "args": ["SYNARTESIS/dist/proxy.js", "--manifest", "/tmp/synartesis-demo/synartesis.yaml"]
190
- }
191
- }
192
- }
193
- ```
194
-
195
- That spells out the clone, because this walkthrough runs from one. Outside it,
196
- there is nothing to install first and nothing to point at:
197
-
198
- ```json
199
- {
200
- "mcpServers": {
201
- "synartesis": {
202
- "command": "npx",
203
- "args": ["-y", "synartesis", "proxy", "--manifest", "/Users/you/.synartesis/synartesis.yaml"]
204
- }
205
- }
206
- }
207
- ```
208
-
209
- The agent sees the same tools with the same names and the same results. That is
210
- the point: nothing about your agent changes.
211
-
212
- For this walkthrough you do not need a real agent. This does the same thing:
213
-
214
- ```bash
215
- printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo-agent","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"update_customer","arguments":{"id":"c_001","plan":"free","notes":"wrong edit"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"delete_customer","arguments":{"id":"c_002"}}}' | node SYNARTESIS/dist/proxy.js --manifest ./synartesis.yaml --journal ./journal.db > /dev/null
216
- ```
217
-
218
- Look at the damage:
219
-
220
- ```bash
221
- cat crm.json
222
- ```
223
-
224
- Ada is on the wrong plan with the wrong notes, and Grace is gone.
225
-
226
- ### 3. See what it did
227
-
228
- ```bash
229
- node SYNARTESIS/dist/cli.js list --journal ./journal.db
230
- ```
231
-
232
- ```bash
233
- node SYNARTESIS/dist/cli.js show RUN_ID --journal ./journal.db
234
- ```
235
-
236
- `show` prints each call with its class, its status, and the exact call that
237
- would undo it, already resolved to literal values.
238
-
239
- ### 4. Undo it
240
-
241
- Look before you leap:
242
-
243
- ```bash
244
- node SYNARTESIS/dist/cli.js undo RUN_ID --dry-run --journal ./journal.db
245
- ```
246
-
247
- Then do it:
248
-
249
- ```bash
250
- node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.db
251
- ```
252
-
253
- ```bash
254
- cat crm.json
255
- ```
256
-
257
- Grace is back and Ada is on her original plan, with her original notes.
258
-
259
- ### 5. Watch it refuse
260
-
261
- Undo is not a blunt instrument. If something else changed a record after the
262
- agent touched it, writing the old value back would destroy that work, so
263
- Synartesis stops and shows you both values.
264
-
265
- Run the damage command from step 2 again. That creates a second run, so take
266
- the run id from the top of `list`, which is ordered most recent first. Then
267
- edit the record by hand:
268
-
269
- ```bash
270
- node -e 'const f="./crm.json",s=JSON.parse(require("fs").readFileSync(f));s.customers.c_001.notes="a human wrote this";require("fs").writeFileSync(f,JSON.stringify(s,null,2))'
271
- ```
272
-
273
- ```bash
274
- node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.db
275
- ```
276
-
277
- It halts at the record that moved, prints the expected and actual state, and
278
- exits non-zero. It is not all-or-nothing: undo works newest first, so anything
279
- it had already put back before reaching the drifted record stays put back —
280
- here `delete_customer` is reversed and `c_002` comes back, and then it stops.
281
- `show` afterwards tells you which actions are still outstanding. Once you have
282
- resolved the conflict yourself, `undo --replan` rebuilds the plan against the
283
- world as it now is and carries on.
284
-
285
- ## Approving what cannot be undone
286
-
287
- `send_email` is classified `irreversible`, so the agent cannot send one on its
288
- own. The call is **refused immediately** with an action id and the command that
289
- would approve it. The agent tells you, you decide, and it tries again.
290
-
291
- It does not hold the call open while waiting. That was the first design and it
292
- does not survive contact with a real client: every useful window for a person
293
- to notice, open a terminal and decide is longer than a client will wait for a
294
- tool, so the two cannot be reconciled by picking a better timeout.
295
-
296
- Approval also does not happen on the terminal the agent is using: the proxy
297
- talks MCP over stdin and stdout, so there is nothing there to prompt on, and a
298
- desktop client has no terminal at all. The request goes to the journal, and you
299
- answer it from anywhere:
300
-
301
- ```bash
302
- node SYNARTESIS/dist/cli.js gates --journal ./journal.db
303
- ```
304
-
305
- ```bash
306
- node SYNARTESIS/dist/cli.js approve ACTION_ID --by your-name --journal ./journal.db
307
- ```
308
-
309
- ```bash
310
- node SYNARTESIS/dist/cli.js deny ACTION_ID --by your-name --reason "not this one" --journal ./journal.db
311
- ```
312
-
313
- An approval is **single use** and expires after an hour, so it covers the retry
314
- it was granted for and cannot quietly authorise the same call tomorrow. It is
315
- not tied to one session, because people restart their client and an approval
316
- stranded in a dead session would be no approval at all.
79
+ ## What it can and cannot do
317
80
 
318
- Nothing is ever approved by silence. An unanswered request simply stays
319
- unanswered, visible in `synartesis gates` until someone decides.
81
+ Every tool gets one of four classifications, written down in a manifest:
320
82
 
321
- The agent is told all of this when it connects, so it can explain itself rather
322
- than reporting an opaque failure.
83
+ | Class | Meaning | Example | What happens |
84
+ |---|---|---|---|
85
+ | `readonly` | Changes nothing | `get_customer` | Recorded, forwarded |
86
+ | `reversible` | Prior state can be restored exactly | `update_customer` | State captured before the write; written back on undo |
87
+ | `compensable` | Cannot be reversed, but can be offset | `create_charge` | A different call neutralises it |
88
+ | `irreversible` | Neither | `send_email` | **Suspended until a human approves it** |
323
89
 
324
- ## Real servers
90
+ A tool your manifest does not mention is treated as `irreversible`. That is
91
+ deliberate: silently forwarding an unknown destructive call is the one failure
92
+ worth avoiding most.
325
93
 
326
- Synartesis has nothing to do with email in particular. It sits on the MCP
327
- protocol, so its subject is whatever the servers you have connected can do:
328
- your files, your repositories, your database, your tickets, your agent's own
329
- memory. What it can undo depends entirely on what those servers expose, and
330
- each manifest below says plainly where that runs out.
94
+ ## Has anybody touched it since?
331
95
 
332
- | Manifest | Server | State it governs |
333
- | --- | --- | --- |
334
- | [`filesystem.yaml`](manifests/filesystem.yaml) | `@modelcontextprotocol/server-filesystem` | real files on disk |
335
- | [`memory.yaml`](manifests/memory.yaml) | `@modelcontextprotocol/server-memory` | the knowledge graph an agent keeps about you |
336
- | [`git.yaml`](manifests/git.yaml) | `mcp-server-git` | a real repository's index and history |
337
- | [`github.yaml`](manifests/github.yaml) | `github/github-mcp-server` | issues, pull requests, file contents |
338
- | [`toy-crm.yaml`](manifests/toy-crm.yaml) | the fixture in this repo, not in the published package | the worked example of every class |
96
+ Synartesis records what an agent does, not what happens to a file. Nothing you
97
+ do by hand goes through the proxy — which is exactly what makes the drift check
98
+ work: when undo reads a file and finds bytes it never recorded, it knows
99
+ somebody else has been there.
339
100
 
340
- Every one of those but `github.yaml` was checked against the server actually
341
- running. Two demos run the whole loop for real:
101
+ To ask before you find out the hard way:
342
102
 
343
103
  ```bash
344
- ./demo/filesystem-demo.sh
345
- ./demo/memory-demo.sh
346
- ```
347
-
348
- The filesystem demo overwrites a file and moves another, restores both, then
349
- shows undo refusing when a human edited the file in between, and the gate
350
- refusing to create a directory this server has no way to remove.
351
-
352
- The memory demo is the sharper one. The agent adds two people to the graph, one
353
- of whom was already there, and the server quietly ignores the duplicate. Undo
354
- therefore has to remove exactly one of them: the inverse is built from what the
355
- server said it created, not from what the agent asked for, so the person who
356
- was there first survives being undone. The same session then tries to delete an
357
- entity and is held, because deleting an entity also deletes every relation
358
- touching it and one inverse call cannot put back both.
359
-
360
- ### Where each one runs out
361
-
362
- The limits are the interesting part, and they are properties of the servers
363
- rather than of Synartesis.
364
-
365
- - **filesystem**: `move_file` is reversible from its arguments alone, so no
366
- pre-read is declared and drift cannot be checked for it. `create_directory`
367
- is `irreversible` not because directories are precious but because this
368
- server exposes no way to remove one.
369
- - **memory**: `add_observations` and `delete_observations` are exact opposites
370
- that disagree about what to call the same field. A path can read a field and
371
- cannot rename one, so that inverse cannot be written at all and the call is
372
- gated instead.
373
- - **git**: nearly every read this server offers answers in prose meant for a
374
- person, so almost nothing can be inverted from a captured state however
375
- reversible the underlying git operation is. Commits are gated because this
376
- server exposes no reset, no revert, and no way to move a branch.
377
-
378
- Two things worth knowing if you write your own, both found by running these
379
- against live servers rather than by reading documentation:
380
-
381
- `$result` is the structured block, and it need not match the text one. The
382
- memory server answers `create_entities` with a bare list in its text block and
383
- `{"entities": [...]}` in `structuredContent`. Synartesis walks the structured
384
- one, because that is the machine-readable contract.
385
-
386
- And `synartesis check` proves a tool exists, not that a path resolves. It
387
- cannot: no call has been made, so there is no result to walk. Run the thing
388
- once and read `synartesis show` before you rely on an inverse.
389
-
390
- ## Writing a manifest
391
-
392
- The manifest is the whole product. It should take fifteen minutes for an API
393
- you know.
394
-
395
- ```yaml
396
- version: 1
397
-
398
- servers:
399
- crm:
400
- command: node
401
- args: ["./crm-server.js"]
402
-
403
- tools:
404
- - match: "crm.get_customer"
405
- class: readonly
406
-
407
- # Read the record before overwriting it, then write that record back.
408
- - match: "crm.update_customer"
409
- class: reversible
410
- snapshot:
411
- tool: "crm.get_customer"
412
- args:
413
- id: "$.id"
414
- inverse:
415
- tool: "crm.update_customer"
416
- args:
417
- id: "$.id"
418
- name: "$snapshot.name"
419
- plan: "$snapshot.plan"
420
-
421
- # Nothing to read beforehand; the id only exists once the call returns.
422
- - match: "crm.create_customer"
423
- class: compensable
424
- inverse:
425
- tool: "crm.delete_customer"
426
- args:
427
- id: "$result.id"
428
-
429
- - match: "crm.send_*"
430
- class: irreversible
431
- gate: always
432
- ```
433
-
434
- There are exactly three things a value can refer to:
435
-
436
- | Prefix | Refers to | Available in |
437
- |---|---|---|
438
- | `$.` | the arguments the agent sent | `snapshot` and `inverse` |
439
- | `$snapshot.` | what the pre-read captured | `inverse` |
440
- | `$result.` | what the forward call returned | `inverse` |
441
-
442
- Anything else is a literal. A reference can stand alone, in which case the
443
- value keeps its type, or sit inside a sentence, in which case it is substituted
444
- as text:
445
-
446
- ```yaml
447
- sha: "$result.content.sha" # the value itself
448
- message: "Revert agent change to $.path" # text with the path substituted
449
- ```
450
-
451
- Write `$$` for a literal dollar sign. There are no expressions, conditionals or
452
- functions, and there will not be: the moment this becomes a language it stops
453
- being something you can write in fifteen minutes.
454
-
455
- Paths can index a list with `[0]` and read one field from every element with
456
- `[]`:
457
-
458
- ```yaml
459
- labels: "$snapshot.labels[].name" # [{name: "bug"}, ...] becomes ["bug", ...]
460
- ```
461
-
462
- That covers the common case where an API hands a field back richer than it
463
- takes it, which is what GitHub does with issue labels. `[]` reads the same key
464
- from each element and nothing else: it is still a path, not a transform. A
465
- reference copies values, it cannot compute them, so an API needing a genuinely
466
- different shape is one the inverse should leave that field out of, and say so.
467
-
468
- Other things to know:
469
-
470
- - `match` supports `*`, which matches within one segment: `crm.send_*` matches
471
- `crm.send_email` but not `crm.a.b`. The most specific pattern wins regardless
472
- of the order rules are written in.
473
- - The inverse of a patch should restore **every** field, not re-apply a patch.
474
- If the same record is edited twice in one run, a partial inverse leaves the
475
- fields the second edit touched behind.
476
- - `gate: on_write` is a heuristic for tools like a raw SQL runner, where
477
- destructiveness cannot be read from the tool name. Anything it cannot
478
- confidently read as a single read statement is gated. Use `gate: always`
479
- wherever certainty matters.
480
- - A malformed manifest stops the proxy from starting, with the file and line to
481
- fix. It will never run with a policy it could not understand.
482
-
483
- ### Saying what absence looks like
484
-
485
- A pre-read that fails tells you nothing about *why*. MCP has no code for "not
486
- found", so a snapshot that comes back with an error could mean the resource was
487
- never there — an agent creating a file — or that it exists and could not be
488
- read. Those need opposite answers: the first is a call to ask a person about,
489
- the second is a call to refuse outright.
490
-
491
- Tell the policy what its server says when a thing is not there:
492
-
493
- ```yaml
494
- - match: "fs.write_file"
495
- class: reversible
496
- snapshot:
497
- tool: "fs.read_text_file"
498
- args:
499
- path: "$.path"
500
- absent_when: ["ENOENT", "no such file"]
104
+ synartesis show <session> --live
501
105
  ```
502
106
 
503
- Anything else the pre-read reports is then a failed snapshot, and the write is
504
- refused rather than offered for approval as a creation. Without `absent_when`,
505
- every error has to be read as absence, which is the safe reading but a blunt
506
- one. Every policy bundled here declares it.
107
+ It reads every resource the session touched as it is now and says which still
108
+ match. Nothing is written, no reversing call is sent, and unlike
109
+ `undo --dry-run` it does not stop at the first conflict — five writes get five
110
+ answers. `l` in the screen does the same.
507
111
 
508
- ### The size of what you can snapshot
509
-
510
- Past a few megabytes a resource cannot be read back through a stdio MCP
511
- connection — the reply is too large to carry, and the connection closes.
512
- Synartesis reconnects and says so, and refuses the write rather than applying a
513
- change it could not capture. That is the right answer, but it does mean
514
- **writes to very large files are refused, not undone**. Nothing is lost; the
515
- call simply does not go through.
516
-
517
- Where the ceiling falls depends on the server and its transport, so no exact
518
- figure here would be true of every one. Measured against the bundled
519
- filesystem policy: a 4 MB file was captured and restored intact; 8 MB and above
520
- was refused with the file untouched.
112
+ If you decide the recorded value is the one worth keeping, `undo --force` prints
113
+ every line it would write over and stops; `--force --yes` goes ahead.
521
114
 
522
115
  ## Commands
523
116
 
524
117
  | Command | Does |
525
118
  |---|---|
119
+ | `synartesis` | The screen. Everything below can be done from it |
120
+ | `install` / `uninstall` / `status` | Cover the clients on this machine, put them back, say what is covered |
526
121
  | `init <server> -- <cmd>` | Introspect a server and draft a manifest |
527
- | `list` | Every recorded run |
528
- | `show <runId>` | One run's timeline, with the undo for each step |
529
- | `show <runId> --live` | The same, plus what has changed in the world since |
530
- | `gates` | What is waiting for a decision |
531
- | `approve <actionId>` | Allow a suspended call |
532
- | `deny <actionId>` | Refuse one |
533
- | `undo <runId>` | Reverse a run, newest action first |
534
- | `undo <runId> --replan` | Same, but rebuild each undo from the current manifest |
535
- | `undo <runId> --force` | Print every change it would write over; `--yes` goes ahead |
536
122
  | `check` | Load a manifest and verify it against the servers it names |
537
- | `prune` | Delete runs older than 30 days and reclaim the space |
538
- | `close [runId]` | End a run a killed proxy left open. Nothing guesses at this: several proxies can share one journal, so a run left active is indistinguishable from one still being worked on |
539
-
540
- `--manifest` and `--journal` are found rather than typed. Both are looked for
541
- from the current directory upwards, the way a version control tool finds its
542
- root, so inside a project that has a `synartesis.yaml` every command works with
543
- no flags at all. When there is nothing above you either, both come from
544
- `~/.synartesis` — most of what anyone guards is not part of a project, and
545
- should not need a directory of its own. `SYNARTESIS_HOME` moves that. A journal
546
- that does not exist yet is placed beside the policy, so the proxy that creates
547
- it and the CLI that reads it agree without either being told.
548
-
549
- Other flags: `--dry-run`, `--to <seq>` and `--replan` on `undo`, `--all` on
550
- `approve` and `deny`, `--json` on `list`, `show` and `gates`, `--older-than
551
- <days>` and `--dry-run` on `prune`. `synartesis --version` prints the version
552
- and nothing else.
553
-
554
- Exit codes: `0` succeeded, `1` halted or refused, `2` bad usage or
555
- configuration.
556
-
557
- ### Serving over HTTP
558
-
559
- Most clients start the proxy themselves over stdio. ChatGPT's connectors do
560
- not: they take a remote HTTPS endpoint and will not run a process on your
561
- machine. For those, serve it:
562
-
563
- ```bash
564
- synartesis proxy --manifest ~/.synartesis/synartesis.yaml --http 9123 --token "$SYNARTESIS_TOKEN"
565
- ```
566
-
567
- The endpoint is `/mcp` and every request needs `Authorization: Bearer <token>`.
568
- A token of at least 16 characters is **required** — it refuses to start without
569
- one — and it binds to `127.0.0.1` unless `--http-host` says otherwise, which
570
- warns when it is not loopback. A session that goes quiet for half an hour is
571
- closed; `--http-idle <seconds>` changes that.
572
-
573
- Reaching it from the internet means putting a tunnel in front of it. That is
574
- deliberately your decision and not a flag: what is on the other end can write
575
- through every server in your policy, so treat the token like a password and
576
- prefer a tunnel that does its own authentication.
577
-
578
- The proxy takes `--manifest`, `--journal` and `--log-level`. It also still
579
- accepts `--gate-timeout <seconds>`, which does nothing: a held call is refused
580
- immediately rather than holding the connection open, so there is no wait to cut
581
- short. Passing it logs a warning saying as much. It logs structured JSON to stderr; stdout is reserved for
582
- protocol traffic.
123
+ | `list` | Every recorded session |
124
+ | `show <id>` | One session's timeline, with the undo for each step |
125
+ | `show <id> --live` | The same, plus what has changed in the world since |
126
+ | `show <id> --full` | Every argument, snapshot and inverse, nothing elided |
127
+ | `gates` / `approve <id>` / `deny <id>` | What is waiting, and answering it |
128
+ | `undo <id>` | Reverse a session, newest action first |
129
+ | `undo <id> --dry-run` | Plan it and change nothing |
130
+ | `undo <id> --replan` | Rebuild each undo from the current manifest |
131
+ | `undo <id> --force [--yes]` | Print what it would write over; `--yes` goes ahead |
132
+ | `watch` | Live activity, with approvals answerable in place |
133
+ | `prune` | Delete sessions older than 30 days and reclaim the space |
134
+ | `close [id]` | End a session a killed proxy left open |
135
+
136
+ In the screen: `enter` opens, `u` undoes, `p` previews, `l` checks the world
137
+ now, `f` expands, `c` shows every AI on the machine, `g` shows what is held.
138
+
139
+ `--manifest` and `--journal` are found rather than typed, from the current
140
+ directory upwards the way a version control tool finds its root, then from
141
+ `~/.synartesis`. `SYNARTESIS_HOME` moves that. `--json` works on `list`, `show`
142
+ and `gates`. Exit codes: `0` succeeded, `1` halted or refused, `2` bad usage.
143
+
144
+ **Full walkthrough, writing a manifest, and serving over HTTP for clients that
145
+ cannot start a process:** see the [user guide](docs/synartesis-user-guide.md).
583
146
 
584
147
  ## What it does not do
585
148
 
586
149
  - **It cannot un-send what has been seen.** An email that has been read, a
587
150
  posted message, a file deleted with no backup. This is why the gate exists.
588
151
  - **Compensable actions cannot be checked for drift.** They declare no pre-read,
589
- so undo compensates them and marks them `[unverified]` in its report.
152
+ so undo compensates them and marks them `[unverified]`.
590
153
  - **Undo halts on uncertainty, and steps over the merely permanent.** Drift, an
591
- unknown outcome, or a failed reversing call stop it, because continuing past
592
- those could destroy something. An action that simply cannot be undone, like a
593
- sent email, is reported and left in place while everything else is reverted:
594
- no amount of stopping un-sends it, and stopping would only leave the rest
595
- wrong too. Either way the run is marked `partial`.
596
- - **A call interrupted mid-flight is recorded as unknown**, not as failed. Undo
597
- refuses to walk past it, because whether it applied cannot be determined.
154
+ unknown outcome, or a failed reversing call stop it. An action that simply
155
+ cannot be undone is reported and left in place while everything else is
156
+ reverted. Either way the session is marked `partial`.
157
+ - **An error is not proof that nothing happened.** A timeout or a tool-level
158
+ error after a write leaves the outcome *unknown*, not failed, and undo will
159
+ not step past it. Where a pre-read exists it is consulted to settle the
160
+ question instead of guessing.
598
161
  - **An undo is only as good as the policy that recorded it.** Inverses are
599
- resolved when the call happens, not when you undo, so a mistake in a manifest
600
- is baked into every run made under it. `undo --replan` rebuilds them from a
601
- corrected manifest using the state already captured, which is the way out.
602
-
603
- ## Has anybody touched it since?
604
-
605
- Synartesis records what an agent does, not what happens to a file. Nothing you
606
- do by hand goes through the proxy, so an edit of your own is invisible to the
607
- journal — and that is what makes the drift check work: when undo reads a file
608
- and finds bytes it never recorded, it knows somebody else has been there.
609
-
610
- The cost used to be that you found out by walking into it. You ran the undo,
611
- and it refused. To ask first:
612
-
613
- ```bash
614
- synartesis show <session> --live
615
- ```
162
+ resolved when the call happens, so a mistake in a manifest is baked into every
163
+ run made under it. `undo --replan` rebuilds them from a corrected one.
616
164
 
617
- It reads every resource the session touched, as it is now, and says which of
618
- them still match what the run left:
619
-
620
- ```
621
- 1 ← reversible applied filesystem.write_file changed since
622
- at line 13:
623
- + A HUMAN WAS HERE
624
- 0 removed, 1 added.
625
-
626
- 1 changed since this ran; undoing would write over it
627
- ```
628
-
629
- Nothing is written, no reversing call is sent, and no row changes status.
630
- Unlike `undo --dry-run`, it does not stop at the first conflict — a session
631
- with five writes reports on all five. `l` in the screen does the same thing.
632
-
633
- If you decide the recorded value is the one worth keeping, `undo --force`
634
- prints every line it would write over and stops; `--force --yes` goes ahead.
635
-
636
- ## Watching it work
637
-
638
- Synartesis is not a daemon and cannot be one. An MCP client spawns a stdio
639
- server itself and owns its lifetime, so nothing long-running could sit in
640
- between and see those calls. What a person wants from a daemon is usually the
641
- reassurance that it is there and doing something, and that needs somewhere to
642
- look rather than a background process:
643
-
644
- ```bash
645
- synartesis watch
646
- ```
647
-
648
- It redraws as the agent works: what has been called, what class each call was,
649
- and anything waiting on a decision, with the command to approve it. Ctrl-C
650
- stops it. Piped rather than run in a terminal, it prints the state once and
651
- exits.
165
+ The bundled **filesystem** policy is tested against the real server: exact
166
+ byte-for-byte restoration, drift refusal, and absence told apart from a read
167
+ that failed. The **memory, git and github** policies are checked only for tool
168
+ existence — their recovery guarantees are not yet proven.
652
169
 
653
170
  ## Trust
654
171
 
655
172
  A manifest names commands and Synartesis runs them. Treat one you did not write
656
173
  the way you would treat a shell script from the same source: read it first.
657
- There is no sandbox here, and there is not meant to be.
658
-
659
- ### What the journal holds
660
-
661
- Putting a file back means having kept what was in it, so the journal is not a
662
- log of what happened. It is a copy of the data: the contents of every resource
663
- before it was written, the arguments of every call, and what each server sent
664
- back. A key that was sitting in a file your agent touched is in there in plain
665
- text. That is not a leak to be closed — it is the thing that makes undo work.
666
-
667
- So it is treated as private data. The journal is created `0600` and the
668
- directory Synartesis makes for it `0700`, and `0600` is re-applied every time a
669
- journal is opened, which closes one written by an older version the first time
670
- a newer one touches it. A directory that already existed is left alone, because
671
- a journal can sit beside a policy inside a project and silently making your
672
- project directory `0700` would be the worse surprise. If your `~/.synartesis`
673
- predates 0.3.0, `chmod 700 ~/.synartesis` is worth doing once by hand.
674
-
675
- Nothing is encrypted. Full-disk encryption answers a stolen laptop; file
676
- permissions answer another account on a machine you share.
677
-
678
- ### How fast it grows
679
-
680
- Every write keeps the resource as it was, the resource as it became, and the
681
- call that did it, so the journal grows at roughly **four times the bytes your
682
- agent writes** and never shrinks on its own. Thirty edits of one 200 kB file
683
- came to 24 MB.
684
-
685
- ```bash
686
- synartesis prune # runs finished more than 30 days ago
687
- synartesis prune --older-than 7 --dry-run
688
- ```
689
-
690
- It deletes whole runs and then `VACUUM`s, since deleting rows leaves a SQLite
691
- file exactly the size it was. It will not touch a run that is still active, or
692
- one holding a call that is waiting on a person or whose outcome is unknown —
693
- age is not an answer to a question nobody answered. A pruned run cannot be
694
- undone afterwards, which is the whole of the trade.
695
174
 
696
- Nothing prunes on a timer. A tool for undoing things should not discard the
697
- record of what was done unless it is asked to.
175
+ **The journal is a copy of your data, not a log.** Putting a file back means
176
+ having kept what was in it, so the contents of every resource before it was
177
+ written are in there in plain text — including any key that was sitting in a
178
+ file your agent touched. That is not a leak to be closed; it is the thing that
179
+ makes undo work. It is created `0600` in a `0700` directory, and nothing is
180
+ encrypted: full-disk encryption answers a stolen laptop, permissions answer
181
+ another account on a machine you share.
182
+
183
+ **It grows at roughly four times the bytes your agent writes** and never shrinks
184
+ on its own — thirty edits of one 200 kB file came to 24 MB. `synartesis prune`
185
+ deletes whole sessions and `VACUUM`s. It will not touch one still active, or one
186
+ holding a call waiting on a person, or one whose undo halted on a conflict. A
187
+ pruned session cannot be undone afterwards, which is the whole of the trade.
188
+ Nothing prunes on a timer.
189
+
190
+ **Durability.** The journal runs `synchronous = NORMAL`. A crash of the process
191
+ or of the CLI mid-undo loses nothing; only the machine losing power can cost the
192
+ tail of the write-ahead log. `SYNARTESIS_SYNC=full` asks for an fsync per commit
193
+ instead — worth it where fsync is cheap, and measurably not where it is not.
698
194
 
699
195
  ## Development
700
196
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synartesis",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "An undo layer for AI agents.",
5
5
  "type": "module",
6
6
  "private": false,