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 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 typecheck && pnpm lint
388
+ pnpm check
389
389
  ```
390
390
 
391
- Every push runs those on Linux and macOS across Node 22 and 24, plus the demo
392
- and the installer.
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
@@ -20,8 +20,8 @@ patches to older ones.
20
20
 
21
21
  | Version | Supported |
22
22
  |---|---|
23
- | 0.3.x | yes |
24
- | < 0.3 | no |
23
+ | 0.6.x | yes |
24
+ | < 0.6 | no |
25
25
 
26
26
  ## What the journal holds, and why it matters
27
27
 
@@ -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. Of the policies that ship, filesystem and memory have been; git and github have not.";
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-JLLAK2QC.js.map
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-JLLAK2QC.js";
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
- const seen = /* @__PURE__ */ new Map();
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 complete, 1 halted or partial, 2 bad usage or configuration.
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 = flag(argv, "--client");
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 = flag(argv, "--client");
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(JSON.stringify(runs.map((run) => ({ ...run, actions: counted(run.id).actions }))));
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.status === "rolled_back" ? 0 : 1;
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
- out(` ${style.quiet("nothing has been written. To go ahead and lose that:")}`);
4052
- out(` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`);
4053
- out("");
4054
- return 1;
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: argv.includes("--dry-run"),
4088
+ dryRun,
4061
4089
  ...toSeq === void 0 ? {} : { toSeq },
4062
4090
  replan,
4063
- ...forcing && said ? { force: true } : {}
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
- `${replan ? " --replan" : ""}${forcing && said ? " --force --yes" : ""}`
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-JLLAK2QC.js";
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
- let recovered;
846
- if (policy.inverse !== void 0 && missingPriorState === void 0) {
847
- try {
848
- recovered = planInverse(policy.inverse, { args, snapshot, result: void 0 });
849
- } catch {
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: `the call applied but its answer never arrived: ${describe(error)}`
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
@@ -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: this server answers with both a text block
46
- # holding a bare array and a structuredContent object wrapping it, and
47
- # structured data wins over re-parsed text. Written against the text block,
48
- # the first version of this policy resolved to nothing and undo halted. It is
49
- # the same mistake the filesystem policy made, and the reason neither of them
50
- # can be trusted until it has met a real server.
51
- #
52
- # $result is the structured block, not the text one, and here the two
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.6.22",
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
- "prepublishOnly": "tsup"
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",