synartesis 0.6.22 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +9 -3
- package/SECURITY.md +2 -2
- package/dist/{chunk-JLLAK2QC.js → chunk-FXSKWMNG.js} +51 -2
- package/dist/cli.js +53 -22
- package/dist/proxy.js +23 -12
- package/manifests/filesystem.yaml +0 -3
- package/manifests/memory.yaml +8 -13
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,111 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.7.0 — 2026-09-16
|
|
6
|
+
|
|
7
|
+
A pass over everything, after an audit of the desktop app, the core and the
|
|
8
|
+
release. Most of what it found was not broken code but things that stated
|
|
9
|
+
something untrue.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Every desktop binary since 0.6.12 carried the wrong version.**
|
|
14
|
+
electron-builder reads the version from `app/package.json`, which nothing
|
|
15
|
+
else reads, so it stayed put while the root moved. The nine installers
|
|
16
|
+
attached to v0.6.23 were all named `0.6.12`, byte-identical in name to the
|
|
17
|
+
ones on v0.6.12 — two people on different releases could not tell their
|
|
18
|
+
builds apart. `app-packaging.test.ts` already checked seven agreements
|
|
19
|
+
between the packaging files; the absence of the eighth is why this ran for
|
|
20
|
+
eleven releases.
|
|
21
|
+
|
|
22
|
+
- **A spent approval read as "refused" in `watch` and the console.** `labelFor`
|
|
23
|
+
was written for exactly this and `list` and `show` were taught to use it;
|
|
24
|
+
`plainly` was not — and `plainly` is what the two live screens render, so a
|
|
25
|
+
person was told their own yes had been a no, seconds after giving it.
|
|
26
|
+
|
|
27
|
+
- **`undo --dry-run --force` printed no plan.** It showed what forcing would
|
|
28
|
+
write over and returned, so the flag whose whole job is to print the plan
|
|
29
|
+
printed none. A dry run no longer needs `--yes` either: the two-step ask
|
|
30
|
+
protects a write, and a preview writes nothing.
|
|
31
|
+
|
|
32
|
+
- **A `verify:` read was never validated.** `snapshot` and `inverse` are
|
|
33
|
+
checked against the servers at load; `verify` was not, and it is the one
|
|
34
|
+
whose failure is silent — the proxy turns it into a soft warning and carries
|
|
35
|
+
on, so a typo meant that tool had no drift detection at all while both the
|
|
36
|
+
policy and `check` said it was fine.
|
|
37
|
+
|
|
38
|
+
- **A failed inverse on the lost-answer path was swallowed whole.** An inverse
|
|
39
|
+
reading `$result.` cannot be rebuilt when the answer is what went missing,
|
|
40
|
+
and undo reported "cannot be undone" without saying why — on the path where
|
|
41
|
+
undo matters most.
|
|
42
|
+
|
|
43
|
+
- **`--client` with a typo blamed the machine**, reporting "No MCP client
|
|
44
|
+
config was found on this machine". On `uninstall` it said "Nothing was
|
|
45
|
+
covered, so nothing was changed", which reads as reassurance.
|
|
46
|
+
|
|
47
|
+
- **`undo --to` always exited 1.** A floor makes a run `partial` by
|
|
48
|
+
construction, correctly, and the exit code was read off that — so an undo
|
|
49
|
+
that did exactly what it was told could not be told from one that halted on
|
|
50
|
+
somebody's edit.
|
|
51
|
+
|
|
52
|
+
- **`status` read "when was this server last used" off the newest five hundred
|
|
53
|
+
actions**, so a server whose last use had scrolled out of that window came
|
|
54
|
+
back as never used: "covered, nothing through it yet". Asked exactly now,
|
|
55
|
+
from a covering index — 75ms to 3.5ms on sixty thousand actions.
|
|
56
|
+
|
|
57
|
+
### Fixed — the desktop window
|
|
58
|
+
|
|
59
|
+
- **With no policy the window could not be quit.** The branch that drew the
|
|
60
|
+
no-policy screen returned before the IPC handlers were registered and before
|
|
61
|
+
the quit handlers were attached; on Windows and Linux that left a process
|
|
62
|
+
running with no window. Everything after `whenReady` is now one path in
|
|
63
|
+
`boot.ts`, which imports no Electron — which is also how the key-page
|
|
64
|
+
allowlist became testable.
|
|
65
|
+
|
|
66
|
+
- **Two spurious errors before the no-policy screen.** The window asked the
|
|
67
|
+
engine two questions the moment it mounted, which with no policy came back as
|
|
68
|
+
"No handler registered" and were drawn as errors. The push is now a pull.
|
|
69
|
+
|
|
70
|
+
- **`Bridge` was declared twice and neither copy was connected to the
|
|
71
|
+
preload** — the only one that was true. They had already drifted. The shared
|
|
72
|
+
one survives, the exposed object is annotated rather than inferred, and both
|
|
73
|
+
the compiler and a test now catch a future drift.
|
|
74
|
+
|
|
75
|
+
- **A server that would not start vanished**: no event, no log, nothing in the
|
|
76
|
+
window, and a briefing that went on announcing it as connected. It now
|
|
77
|
+
reaches the transcript with the server's own words, and the model is told not
|
|
78
|
+
to plan around tools that are not there.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- `pnpm check` runs build, typecheck, lint and the suite in one command.
|
|
83
|
+
- The release workflow runs that gate before building installers, builds the
|
|
84
|
+
desktop app in CI, runs the memory demo, and takes its release notes from
|
|
85
|
+
`CHANGELOG.md` instead of a list of commit subjects.
|
|
86
|
+
- `npm publish` runs the whole gate rather than only a build.
|
|
87
|
+
- `list --json` and `show --json` both carry `actionCount`, so one name means
|
|
88
|
+
one thing in both. The existing `actions` shape is untouched.
|
|
89
|
+
- The site points at `/releases/latest` rather than a pinned version.
|
|
90
|
+
- The user guide covers `install`, `uninstall`, `status` and `desktop`, and is
|
|
91
|
+
no longer stamped twenty releases back. `CONTRIBUTING.md` no longer shows a
|
|
92
|
+
manifest syntax that throws on load, or asks for a C toolchain the project
|
|
93
|
+
disables.
|
|
94
|
+
|
|
95
|
+
## 0.6.23 — 2026-09-16
|
|
96
|
+
|
|
97
|
+
### Docs
|
|
98
|
+
|
|
99
|
+
- **What to do with a tool that does two things at once.** A call that both
|
|
100
|
+
writes something recoverable and does something that is not -- saves a file
|
|
101
|
+
and posts to an API, updates a record and sends the email about it -- takes
|
|
102
|
+
the class of its least recoverable part, because a rule matches a tool name
|
|
103
|
+
and gives it one class. The guide now says so, says why the alternative is
|
|
104
|
+
worse (undo puts the file back, reports `rolled_back`, and says nothing about
|
|
105
|
+
the request still out there), points at `move_file` as the shipped example of
|
|
106
|
+
the same shape, and shows the one way out: a `compensable` rule whose inverse
|
|
107
|
+
neutralises the unrecoverable half. Asked often enough to be worth writing
|
|
108
|
+
down rather than answering again.
|
|
109
|
+
|
|
5
110
|
## 0.6.22 — 2026-09-16
|
|
6
111
|
|
|
7
112
|
### Added
|
package/README.md
CHANGED
|
@@ -385,11 +385,17 @@ pnpm test
|
|
|
385
385
|
```
|
|
386
386
|
|
|
387
387
|
```bash
|
|
388
|
-
pnpm
|
|
388
|
+
pnpm check
|
|
389
389
|
```
|
|
390
390
|
|
|
391
|
-
Every push runs
|
|
392
|
-
and the
|
|
391
|
+
Every push runs that on Linux and macOS across Node 22 and 24, plus both demos,
|
|
392
|
+
the installer, and a build of the desktop app.
|
|
393
|
+
|
|
394
|
+
**Windows is built and not tested.** The release attaches a Windows installer,
|
|
395
|
+
and no CI job compiles or exercises it — the test matrix is Linux and macOS. It
|
|
396
|
+
is expected to work, the code has no platform-specific paths outside
|
|
397
|
+
`src/locate.ts` and `src/install/clients.ts`, and nobody has proved it. If you
|
|
398
|
+
run Windows and something is wrong there, that is worth an issue.
|
|
393
399
|
|
|
394
400
|
## Licence
|
|
395
401
|
|
package/SECURITY.md
CHANGED
|
@@ -325,6 +325,20 @@ CREATE INDEX IF NOT EXISTS actions_unresolved ON actions(run_id, server, tool)
|
|
|
325
325
|
-- ordering it asks for, so the index both filters and sorts.
|
|
326
326
|
CREATE INDEX IF NOT EXISTS actions_writes ON actions(run_id, seq)
|
|
327
327
|
WHERE class <> 'readonly';
|
|
328
|
+
|
|
329
|
+
-- Covering, like actions_run_status and for the same reason. status asks when
|
|
330
|
+
-- each server was last used; without this the group-by walks the table, and
|
|
331
|
+
-- the table carries the snapshots, so the cost of drawing a connection list
|
|
332
|
+
-- grew with the size of the data those connections had touched rather than
|
|
333
|
+
-- with how many there were. Both columns the query reads are here, so sqlite
|
|
334
|
+
-- never reaches into a row -- the plan says COVERING INDEX, and it is the
|
|
335
|
+
-- covering half that does the work. Measured on sixty thousand actions across
|
|
336
|
+
-- twelve servers with two-kilobyte snapshots: 75ms to 3.5ms.
|
|
337
|
+
--
|
|
338
|
+
-- Added the same way as the ones above and for the same reason: no row
|
|
339
|
+
-- changes, no meaning changes, IF NOT EXISTS makes it idempotent, and an older
|
|
340
|
+
-- build opening the same file afterwards neither notices nor cares.
|
|
341
|
+
CREATE INDEX IF NOT EXISTS actions_seen ON actions(server, ts);
|
|
328
342
|
`;
|
|
329
343
|
|
|
330
344
|
// src/journal/journal.ts
|
|
@@ -336,6 +350,7 @@ var runSchema = z.object({
|
|
|
336
350
|
ended_at: z.string().nullable(),
|
|
337
351
|
status: z.enum(["active", "complete", "rolled_back", "partial"])
|
|
338
352
|
});
|
|
353
|
+
var seenSchema = z.object({ server: z.string(), ts: z.string() });
|
|
339
354
|
var tallySchema = z.object({
|
|
340
355
|
run_id: z.string(),
|
|
341
356
|
actions: z.number(),
|
|
@@ -847,6 +862,33 @@ var SqliteJournal = class {
|
|
|
847
862
|
return raw === void 0 ? void 0 : toAction(raw);
|
|
848
863
|
});
|
|
849
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* When each server was last used, over everything, not over a window.
|
|
867
|
+
*
|
|
868
|
+
* This was a scan of the newest five hundred actions, which answers a
|
|
869
|
+
* different question: on a busy journal a server whose last use had fallen
|
|
870
|
+
* out of that window came back as never used at all, and `status` reported
|
|
871
|
+
* "covered, nothing through it yet" about a server somebody uses daily.
|
|
872
|
+
* Wrong, and reassuringly so -- it reads as though the connection were
|
|
873
|
+
* merely new.
|
|
874
|
+
*
|
|
875
|
+
* Exact instead, and answered entirely from actions_seen: the query reads
|
|
876
|
+
* only the two columns that index holds, so it never touches the rows
|
|
877
|
+
* carrying snapshots.
|
|
878
|
+
*/
|
|
879
|
+
lastSeenPerServer() {
|
|
880
|
+
return this.#run("lastSeenPerServer", () => {
|
|
881
|
+
const rows = this.#db.prepare("SELECT server, MAX(ts) AS ts FROM actions GROUP BY server").all();
|
|
882
|
+
const seen = /* @__PURE__ */ new Map();
|
|
883
|
+
for (const row of rows) {
|
|
884
|
+
const read2 = seenSchema.safeParse(row);
|
|
885
|
+
if (read2.success) {
|
|
886
|
+
seen.set(read2.data.server, read2.data.ts);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return seen;
|
|
890
|
+
});
|
|
891
|
+
}
|
|
850
892
|
recentActions(limit) {
|
|
851
893
|
return this.#run(
|
|
852
894
|
"recentActions",
|
|
@@ -1272,6 +1314,13 @@ function validate(source, manifest) {
|
|
|
1272
1314
|
}
|
|
1273
1315
|
checkCall(source, [...path, "inverse"], policy.inverse, servers, allowed);
|
|
1274
1316
|
}
|
|
1317
|
+
if (policy.verify !== void 0) {
|
|
1318
|
+
const allowed = ["$.", "$result."];
|
|
1319
|
+
if (policy.snapshot !== void 0) {
|
|
1320
|
+
allowed.push("$snapshot.");
|
|
1321
|
+
}
|
|
1322
|
+
checkCall(source, [...path, "verify"], policy.verify, servers, allowed);
|
|
1323
|
+
}
|
|
1275
1324
|
});
|
|
1276
1325
|
}
|
|
1277
1326
|
function withGate(policy) {
|
|
@@ -1561,7 +1610,7 @@ function describeStanding(entry) {
|
|
|
1561
1610
|
return "no claim either way";
|
|
1562
1611
|
}
|
|
1563
1612
|
}
|
|
1564
|
-
var LIVE_IS_NOT_RECOVERY = "`live` means the policy has met its server, not that undo has been round-tripped against it.
|
|
1613
|
+
var LIVE_IS_NOT_RECOVERY = "`live` means the policy has met its server, not that undo has been round-tripped against it. Whether it puts anything back is a separate question, and only a test against the real server answers it.";
|
|
1565
1614
|
function warnUntested(servers) {
|
|
1566
1615
|
const names = servers.join(", ");
|
|
1567
1616
|
const these = servers.length === 1 ? "this policy has" : "these policies have";
|
|
@@ -1944,4 +1993,4 @@ export {
|
|
|
1944
1993
|
observeState,
|
|
1945
1994
|
connectStdioUpstream
|
|
1946
1995
|
};
|
|
1947
|
-
//# sourceMappingURL=chunk-
|
|
1996
|
+
//# sourceMappingURL=chunk-FXSKWMNG.js.map
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
verifyAgainstServers,
|
|
39
39
|
warnUntested,
|
|
40
40
|
wasRefused
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-FXSKWMNG.js";
|
|
42
42
|
import {
|
|
43
43
|
DriftConflict,
|
|
44
44
|
ManifestError,
|
|
@@ -1076,7 +1076,7 @@ function plainly(action) {
|
|
|
1076
1076
|
case "rolling_back":
|
|
1077
1077
|
return { text: "undoing", needs: false };
|
|
1078
1078
|
case "denied":
|
|
1079
|
-
return { text: "refused", needs: false };
|
|
1079
|
+
return labelFor(action) === "used" ? { text: "used", needs: false } : { text: "refused", needs: false };
|
|
1080
1080
|
case "failed":
|
|
1081
1081
|
return { text: "failed", needs: false };
|
|
1082
1082
|
case "approved":
|
|
@@ -1526,6 +1526,12 @@ var LABELS = {
|
|
|
1526
1526
|
cursor: "Cursor",
|
|
1527
1527
|
codex: "Codex"
|
|
1528
1528
|
};
|
|
1529
|
+
var CLIENT_IDS = Object.keys(LABELS).filter(
|
|
1530
|
+
(name) => name in LABELS
|
|
1531
|
+
);
|
|
1532
|
+
function isClientId(value) {
|
|
1533
|
+
return CLIENT_IDS.some((known) => known === value);
|
|
1534
|
+
}
|
|
1529
1535
|
function claudeDesktopPath() {
|
|
1530
1536
|
const home = homedir();
|
|
1531
1537
|
switch (platform()) {
|
|
@@ -1946,14 +1952,7 @@ function applyUninstall(sites, manifestPath) {
|
|
|
1946
1952
|
// src/install/connections.ts
|
|
1947
1953
|
var ACTIVE_WITHIN_MS = 2 * 60 * 1e3;
|
|
1948
1954
|
function lastSeenByServer(journal) {
|
|
1949
|
-
|
|
1950
|
-
for (const action of journal.recentActions(500)) {
|
|
1951
|
-
const known = seen.get(action.server);
|
|
1952
|
-
if (known === void 0 || action.ts > known) {
|
|
1953
|
-
seen.set(action.server, action.ts);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
return seen;
|
|
1955
|
+
return journal.lastSeenPerServer();
|
|
1957
1956
|
}
|
|
1958
1957
|
function commandMissing(command) {
|
|
1959
1958
|
if (command === void 0) {
|
|
@@ -2848,7 +2847,8 @@ Most commands end by naming the one thing worth doing next, worked out from
|
|
|
2848
2847
|
what is actually in the journal rather than from what was typed. Set
|
|
2849
2848
|
SYNARTESIS_NO_HINTS to turn that off; --json never carries it.
|
|
2850
2849
|
|
|
2851
|
-
Exit codes: 0
|
|
2850
|
+
Exit codes: 0 did what was asked, 1 stopped or left something in place,
|
|
2851
|
+
2 bad usage or configuration.
|
|
2852
2852
|
`;
|
|
2853
2853
|
var UsageError = class extends Error {
|
|
2854
2854
|
/**
|
|
@@ -2940,6 +2940,20 @@ async function runPin(argv) {
|
|
|
2940
2940
|
out("");
|
|
2941
2941
|
return 0;
|
|
2942
2942
|
}
|
|
2943
|
+
function namedClient(argv) {
|
|
2944
|
+
const typed = flag(argv, "--client");
|
|
2945
|
+
if (typed === void 0) {
|
|
2946
|
+
return void 0;
|
|
2947
|
+
}
|
|
2948
|
+
if (!isClientId(typed)) {
|
|
2949
|
+
const near = didYouMean(typed, CLIENT_IDS);
|
|
2950
|
+
throw new UsageError(
|
|
2951
|
+
`--client ${typed} is not a client this knows${near === void 0 ? "" : `; did you mean ${near}?`}
|
|
2952
|
+
It knows: ${CLIENT_IDS.join(", ")}`
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
return typed;
|
|
2956
|
+
}
|
|
2943
2957
|
async function runCheck(argv) {
|
|
2944
2958
|
const path = findManifest(flag(argv, "--manifest"));
|
|
2945
2959
|
const manifest = loadManifest(path);
|
|
@@ -3037,7 +3051,7 @@ async function runCheck(argv) {
|
|
|
3037
3051
|
}
|
|
3038
3052
|
async function runInstall(argv) {
|
|
3039
3053
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3040
|
-
const only =
|
|
3054
|
+
const only = namedClient(argv);
|
|
3041
3055
|
const dryRun = argv.includes("--dry-run");
|
|
3042
3056
|
const printOnly = argv.includes("--print");
|
|
3043
3057
|
const sites = discover(process.cwd()).filter(
|
|
@@ -3122,7 +3136,7 @@ async function runInstall(argv) {
|
|
|
3122
3136
|
}
|
|
3123
3137
|
async function runUninstall(argv) {
|
|
3124
3138
|
const manifestPath = findManifest(flag(argv, "--manifest"));
|
|
3125
|
-
const only =
|
|
3139
|
+
const only = namedClient(argv);
|
|
3126
3140
|
const sites = discover(process.cwd()).filter(
|
|
3127
3141
|
(site) => only === void 0 || site.client === only
|
|
3128
3142
|
);
|
|
@@ -3396,7 +3410,14 @@ function runList(journal, asJson, journalPath) {
|
|
|
3396
3410
|
const tally2 = journal.tallyRuns();
|
|
3397
3411
|
const counted = (id) => tally2.get(id) ?? { actions: 0, unknown: 0, waiting: 0, applied: 0 };
|
|
3398
3412
|
if (asJson) {
|
|
3399
|
-
out(
|
|
3413
|
+
out(
|
|
3414
|
+
JSON.stringify(
|
|
3415
|
+
runs.map((run) => {
|
|
3416
|
+
const actions = counted(run.id).actions;
|
|
3417
|
+
return { ...run, actions, actionCount: actions };
|
|
3418
|
+
})
|
|
3419
|
+
)
|
|
3420
|
+
);
|
|
3400
3421
|
return 0;
|
|
3401
3422
|
}
|
|
3402
3423
|
if (runs.length === 0) {
|
|
@@ -3452,6 +3473,9 @@ async function runShow(argv, journal, asJson) {
|
|
|
3452
3473
|
JSON.stringify({
|
|
3453
3474
|
run,
|
|
3454
3475
|
actions: journal.getActions(runId),
|
|
3476
|
+
// The same name means the same thing in `list --json`, where `actions`
|
|
3477
|
+
// has always been a count and cannot change.
|
|
3478
|
+
actionCount: journal.getActions(runId).length,
|
|
3455
3479
|
...inspection === void 0 ? {} : { live: inspection.resources }
|
|
3456
3480
|
})
|
|
3457
3481
|
);
|
|
@@ -3907,7 +3931,7 @@ function report(result, alreadyForcing = false, as = "") {
|
|
|
3907
3931
|
needs: ["manifest", "journal"]
|
|
3908
3932
|
});
|
|
3909
3933
|
}
|
|
3910
|
-
return result.
|
|
3934
|
+
return result.halted === void 0 && permanent.length === 0 ? 0 : 1;
|
|
3911
3935
|
}
|
|
3912
3936
|
async function withUpstreams(manifestPath, use, only) {
|
|
3913
3937
|
const manifest = loadManifest(manifestPath);
|
|
@@ -4025,6 +4049,8 @@ async function runUndo(argv, journal) {
|
|
|
4025
4049
|
if (said && !forcing) {
|
|
4026
4050
|
process.stderr.write("synartesis: --yes only means anything with --force; ignoring it\n");
|
|
4027
4051
|
}
|
|
4052
|
+
const dryRun = argv.includes("--dry-run");
|
|
4053
|
+
const forcePlan = forcing && (said || dryRun);
|
|
4028
4054
|
if (forcing && !said) {
|
|
4029
4055
|
const over = (await withUpstreams(
|
|
4030
4056
|
manifestPath,
|
|
@@ -4048,25 +4074,30 @@ async function runUndo(argv, journal) {
|
|
|
4048
4074
|
}
|
|
4049
4075
|
}
|
|
4050
4076
|
out("");
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4077
|
+
if (!dryRun) {
|
|
4078
|
+
out(` ${style.quiet("nothing has been written. To go ahead and lose that:")}`);
|
|
4079
|
+
out(` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`);
|
|
4080
|
+
out("");
|
|
4081
|
+
return 1;
|
|
4082
|
+
}
|
|
4055
4083
|
}
|
|
4056
4084
|
}
|
|
4057
4085
|
const replan = argv.includes("--replan");
|
|
4058
4086
|
return report(
|
|
4059
4087
|
await performUndo(manifestPath, journal, runId, {
|
|
4060
|
-
dryRun
|
|
4088
|
+
dryRun,
|
|
4061
4089
|
...toSeq === void 0 ? {} : { toSeq },
|
|
4062
4090
|
replan,
|
|
4063
|
-
...
|
|
4091
|
+
...forcePlan ? { force: true } : {}
|
|
4064
4092
|
}),
|
|
4065
4093
|
forcing,
|
|
4066
4094
|
// --to is deliberately absent, and cannot reach here: a floor leaves
|
|
4067
4095
|
// actions below it alone, which makes the result `partial`, and the hint
|
|
4068
4096
|
// is only offered on `rolled_back`.
|
|
4069
|
-
|
|
4097
|
+
// forcePlan, not `forcing && said`: on a dry run the command that does
|
|
4098
|
+
// this for real is the forced one, and offering it without --force would
|
|
4099
|
+
// hand back something that halts on the drift the preview just showed.
|
|
4100
|
+
`${replan ? " --replan" : ""}${forcePlan ? " --force --yes" : ""}`
|
|
4070
4101
|
);
|
|
4071
4102
|
}
|
|
4072
4103
|
var KNOWN_COMMANDS = [
|
package/dist/proxy.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
verifyAgainstServers,
|
|
26
26
|
warnUntested,
|
|
27
27
|
withIdempotencyKey
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-FXSKWMNG.js";
|
|
29
29
|
import {
|
|
30
30
|
SnapshotError,
|
|
31
31
|
UpstreamError,
|
|
@@ -399,6 +399,18 @@ function compatible(tool, seen) {
|
|
|
399
399
|
}
|
|
400
400
|
return { ...tool, outputSchema: withoutDialect(tool["outputSchema"], seen) };
|
|
401
401
|
}
|
|
402
|
+
function recoverInverse(policy, captured, noPriorState) {
|
|
403
|
+
if (policy.inverse === void 0 || noPriorState) {
|
|
404
|
+
return {};
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
return { inverse: planInverse(policy.inverse, { ...captured, result: void 0 }) };
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return {
|
|
410
|
+
warning: `and its inverse could not be resolved without that answer: ${describe(error)}`
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
402
414
|
function createProxyServer(options) {
|
|
403
415
|
const { upstreams, manifest, journal } = options;
|
|
404
416
|
const router = createRouter(upstreams, manifest);
|
|
@@ -842,19 +854,19 @@ function createProxyServer(options) {
|
|
|
842
854
|
} else if (settled === "unknown") {
|
|
843
855
|
journal.markUnknown(pending.actionId, describe(error));
|
|
844
856
|
} else {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
recovered = void 0;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
857
|
+
const recovered = recoverInverse(
|
|
858
|
+
policy,
|
|
859
|
+
{ args, snapshot },
|
|
860
|
+
missingPriorState !== void 0
|
|
861
|
+
);
|
|
853
862
|
journal.markApplied(pending.actionId, {
|
|
854
863
|
result: void 0,
|
|
855
|
-
...recovered === void 0 ? {} : { inverse: recovered },
|
|
864
|
+
...recovered.inverse === void 0 ? {} : { inverse: recovered.inverse },
|
|
856
865
|
...verify === void 0 ? {} : { verify },
|
|
857
|
-
warning:
|
|
866
|
+
warning: [
|
|
867
|
+
`the call applied but its answer never arrived: ${describe(error)}`,
|
|
868
|
+
...recovered.warning === void 0 ? [] : [recovered.warning]
|
|
869
|
+
].join("; ")
|
|
858
870
|
});
|
|
859
871
|
}
|
|
860
872
|
}
|
|
@@ -1199,7 +1211,6 @@ async function main() {
|
|
|
1199
1211
|
upstreams,
|
|
1200
1212
|
manifest,
|
|
1201
1213
|
journal,
|
|
1202
|
-
gateTimeoutMs: argv.gateTimeoutMs,
|
|
1203
1214
|
logger: log,
|
|
1204
1215
|
// Absolute, because whoever approves may be in any directory at all.
|
|
1205
1216
|
approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`
|
|
@@ -88,9 +88,6 @@ tools:
|
|
|
88
88
|
path: "$.path"
|
|
89
89
|
content: "$snapshot.content"
|
|
90
90
|
|
|
91
|
-
# Reversible from its arguments alone: no pre-read could say anything the
|
|
92
|
-
# arguments do not already. Drift cannot be checked for the same reason, so
|
|
93
|
-
# undo reports it as unverified.
|
|
94
91
|
# Only reversible when the destination did not exist. Moving onto a file
|
|
95
92
|
# that did overwrites it, and moving back afterwards restores the source and
|
|
96
93
|
# leaves nothing where the destination's contents were: undo reports success
|
package/manifests/memory.yaml
CHANGED
|
@@ -42,19 +42,14 @@ tools:
|
|
|
42
42
|
# undoing by argument would delete a Grace who was there long before the
|
|
43
43
|
# agent ran. The result carries only what was actually created.
|
|
44
44
|
#
|
|
45
|
-
# $result.entities, not $result
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# the
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# disagree: the text is a bare list of entities while structuredContent wraps
|
|
54
|
-
# it as {"entities": [...]}. The structured block is the machine-readable
|
|
55
|
-
# contract, so that is the one a path walks. Writing $result[].name here is
|
|
56
|
-
# the natural mistake, and it was the first thing this policy got wrong
|
|
57
|
-
# against the live server.
|
|
45
|
+
# $result.entities, not $result. $result is the structured block, not the
|
|
46
|
+
# text one, and here the two disagree: the text is a bare list of entities
|
|
47
|
+
# while structuredContent wraps it as {"entities": [...]}. The structured
|
|
48
|
+
# block is the machine-readable contract, so that is the one a path walks.
|
|
49
|
+
# Writing $result[].name is the natural mistake, and it was the first thing
|
|
50
|
+
# this policy got wrong against the live server -- the same mistake the
|
|
51
|
+
# filesystem policy made, and the reason neither can be trusted until it has
|
|
52
|
+
# met one.
|
|
58
53
|
- match: "memory.create_entities"
|
|
59
54
|
class: compensable
|
|
60
55
|
inverse:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synartesis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "An undo layer for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"app:dist": "pnpm app:build && node app/build/pack.mjs",
|
|
58
58
|
"demo": "tsup --silent && ./demo/filesystem-demo.sh",
|
|
59
59
|
"demo:memory": "tsup --silent && ./demo/memory-demo.sh",
|
|
60
|
-
"
|
|
60
|
+
"check": "pnpm build && pnpm typecheck && pnpm lint && pnpm test",
|
|
61
|
+
"prepublishOnly": "pnpm check"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
64
|
"@modelcontextprotocol/sdk": "^1.30.0",
|