synartesis 0.4.0 → 0.4.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 CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.4.1 — 2026-09-09
6
+
7
+ ### Added
8
+
9
+ - **`synartesis show <session> --live`**, and `l` in the screen. Synartesis
10
+ records what an agent does, not what happens to a file — nothing you do by
11
+ hand goes through the proxy. That is what makes the drift check work, and it
12
+ meant the commonest question about this tool had no answer: has somebody
13
+ edited that file since? You found out by attempting an undo and having it
14
+ refuse.
15
+
16
+ `--live` reads every resource the session touched as it is now and says
17
+ which of them still match what the run left. Nothing is written, no
18
+ reversing call is sent, and no row changes status. Unlike `undo --dry-run`
19
+ it does not stop at the first conflict: a session with five writes reports
20
+ on all five.
21
+
22
+ An earlier write to a resource is reported as superseded rather than
23
+ changed. Undo walks backwards, so each older write is checked against a
24
+ state the one above it restores; measuring them all against the world as it
25
+ is now would call every write but the last one changed.
26
+
27
+ ### Fixed
28
+
29
+ - **Undo acted on a session nobody was looking at.** One cursor serves four
30
+ views, and in the held-calls and connections lists its number counts
31
+ something else entirely — but `u`, `p` and `l` read it against the sessions
32
+ regardless. `g`, `j`, `u`, `y`, four keys pressed while looking at the list
33
+ of held calls, undid a session that was never on screen. That is the failure
34
+ this tool exists to prevent, committed by the tool. Those keys now do
35
+ nothing outside the session views and say why.
36
+
37
+ - **`--force` showed one conflict and overwrote several.** The ask ran a
38
+ dry-run rollback, and a rollback halts at the first drift — so with two
39
+ people's edits underneath it, one diff was printed, `--force --yes` was
40
+ typed, and both were written over. It now reads every conflict and prints
41
+ each one.
42
+
43
+ - **One broken server stopped you undoing anything at all.** `install` covers
44
+ every AI on the machine with one policy, so a manifest routinely names
45
+ servers that have nothing to do with the session in hand — and undo started
46
+ all of them. An entry whose command is not there made every session
47
+ unreadable and unundoable. Undo and `--live` now start only the servers the
48
+ session actually went to, and skip with a reported reason any that still
49
+ will not start. `--replan` still starts everything, since it re-resolves
50
+ inverses from the current policy.
51
+
52
+ - **`--force` ignored `--to`.** A change below the floor — an action the undo
53
+ would not touch — refused the whole command.
54
+
55
+ - The overwrite warning in the screen now expires with the diff that
56
+ justified it, so the second `u` cannot be answered after the evidence has
57
+ scrolled away. `--yes` without `--force` says it is being ignored rather
58
+ than being silently dropped, and `--live` no longer starts every server to
59
+ inspect a session that recorded nothing.
60
+
5
61
  ## 0.4.0 — 2026-09-08
6
62
 
7
63
  ### Added
package/README.md CHANGED
@@ -526,11 +526,13 @@ was refused with the file untouched.
526
526
  | `init <server> -- <cmd>` | Introspect a server and draft a manifest |
527
527
  | `list` | Every recorded run |
528
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 |
529
530
  | `gates` | What is waiting for a decision |
530
531
  | `approve <actionId>` | Allow a suspended call |
531
532
  | `deny <actionId>` | Refuse one |
532
533
  | `undo <runId>` | Reverse a run, newest action first |
533
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 |
534
536
  | `check` | Load a manifest and verify it against the servers it names |
535
537
  | `prune` | Delete runs older than 30 days and reclaim the space |
536
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 |
@@ -598,6 +600,39 @@ protocol traffic.
598
600
  is baked into every run made under it. `undo --replan` rebuilds them from a
599
601
  corrected manifest using the state already captured, which is the way out.
600
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
+ ```
616
+
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
+
601
636
  ## Watching it work
602
637
 
603
638
  Synartesis is not a daemon and cannot be one. An MCP client spawns a stdio
package/dist/cli.js CHANGED
@@ -553,11 +553,11 @@ function describeStep(action) {
553
553
  return { seq: action.seq, server: action.server, tool: action.tool };
554
554
  }
555
555
  function overwriteText(current, action) {
556
- const intended = intendedAfterInverse(action);
557
- if (intended === void 0) {
556
+ const intended2 = intendedAfterInverse(action);
557
+ if (intended2 === void 0) {
558
558
  return "";
559
559
  }
560
- return changedLines(current, intended);
560
+ return changedLines(current, intended2);
561
561
  }
562
562
  function intendedAfterInverse(action) {
563
563
  return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
@@ -595,6 +595,98 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
595
595
  return { ok: true };
596
596
  }
597
597
 
598
+ // src/rollback/inspect.ts
599
+ import { z as z3 } from "zod";
600
+ var inversePlan2 = z3.object({
601
+ server: z3.string(),
602
+ tool: z3.string(),
603
+ args: z3.record(z3.string(), z3.unknown())
604
+ });
605
+ var observation2 = z3.union([
606
+ z3.object({ present: z3.literal(true), value: z3.unknown() }),
607
+ z3.object({ present: z3.literal(false) })
608
+ ]);
609
+ function sameState2(a, b) {
610
+ return canonical(a) === canonical(b);
611
+ }
612
+ function intended(action) {
613
+ return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
614
+ }
615
+ function resourceKey(plan) {
616
+ return canonical({ server: plan.server, tool: plan.tool, args: plan.args });
617
+ }
618
+ var SETTLED = /* @__PURE__ */ new Set(["failed", "denied", "gated", "approved"]);
619
+ async function inspect(options) {
620
+ const { journal, router, runId } = options;
621
+ const signal = options.signal ?? new AbortController().signal;
622
+ const actions = journal.getActions(runId);
623
+ const resources = [];
624
+ const seen = /* @__PURE__ */ new Set();
625
+ for (const action of [...actions].reverse()) {
626
+ const at = { seq: action.seq, server: action.server, tool: action.tool };
627
+ if (action.class === "readonly") {
628
+ continue;
629
+ }
630
+ if (SETTLED.has(action.status)) {
631
+ resources.push({ ...at, condition: "not-applied", note: action.status });
632
+ continue;
633
+ }
634
+ if (action.status === "rolled_back") {
635
+ resources.push({ ...at, condition: "restored" });
636
+ continue;
637
+ }
638
+ const verify = inversePlan2.safeParse(action.verify);
639
+ const post = observation2.safeParse(action.postSnapshot);
640
+ if (!verify.success || !post.success) {
641
+ resources.push({
642
+ ...at,
643
+ condition: "unknowable",
644
+ note: action.inverse === void 0 ? "nothing was captured to restore" : "no pre-read was declared"
645
+ });
646
+ continue;
647
+ }
648
+ const key = resourceKey(verify.data);
649
+ if (seen.has(key)) {
650
+ resources.push({ ...at, condition: "superseded" });
651
+ continue;
652
+ }
653
+ seen.add(key);
654
+ let current;
655
+ try {
656
+ current = await observeState(router, verify.data, signal);
657
+ } catch (error) {
658
+ resources.push({ ...at, condition: "unknowable", note: `could not read it: ${describe(error)}` });
659
+ continue;
660
+ }
661
+ if (sameState2(current, post.data)) {
662
+ resources.push({ ...at, condition: "unchanged" });
663
+ continue;
664
+ }
665
+ const back = intended(action);
666
+ if (back !== void 0 && sameState2(current, back)) {
667
+ resources.push({ ...at, condition: "restored", note: "put back outside this journal" });
668
+ continue;
669
+ }
670
+ resources.push({
671
+ ...at,
672
+ condition: "changed",
673
+ diff: changedLines(post.data, current)
674
+ });
675
+ }
676
+ return { runId, resources: resources.reverse() };
677
+ }
678
+ function verdict(inspection) {
679
+ const changed = inspection.resources.filter((r) => r.condition === "changed").length;
680
+ const undoable = inspection.resources.filter((r) => r.condition === "unchanged").length;
681
+ if (changed > 0) {
682
+ return `${String(changed)} changed since this ran; undoing would write over ${changed === 1 ? "it" : "them"}`;
683
+ }
684
+ if (undoable > 0) {
685
+ return `nothing has been touched since; all ${String(undoable)} would undo cleanly`;
686
+ }
687
+ return "nothing here is still applied";
688
+ }
689
+
598
690
  // src/watch.ts
599
691
  import { existsSync as existsSync2 } from "fs";
600
692
 
@@ -1868,13 +1960,14 @@ function footer(screen, options) {
1868
1960
  return ["", ` ${style.accent(what)} ${keyHint2("y", "yes")} ${keyHint2("n", "no")}`];
1869
1961
  }
1870
1962
  const keys = screen.mode === "connections" ? [keyHint2("enter", "connect"), keyHint2("a", "connect all"), keyHint2("r", "rescan"), keyHint2("j/k", "move"), keyHint2("h", "back")] : screen.mode === "gates" ? [keyHint2("a", "approve"), keyHint2("d", "deny"), keyHint2("j/k", "move"), keyHint2("r", "runs")] : screen.mode === "run" ? [
1963
+ keyHint2("l", "check now"),
1871
1964
  keyHint2("p", "preview undo"),
1872
1965
  keyHint2("u", "undo"),
1873
- keyHint2("esc", "back"),
1874
- keyHint2("g", "held")
1966
+ keyHint2("f", "expand"),
1967
+ keyHint2("esc", "back")
1875
1968
  ] : [
1876
1969
  keyHint2("enter", "open"),
1877
- keyHint2("p", "preview undo"),
1970
+ keyHint2("l", "check now"),
1878
1971
  keyHint2("u", "undo"),
1879
1972
  keyHint2("j/k", "move"),
1880
1973
  keyHint2("g", "held")
@@ -1962,6 +2055,7 @@ async function openConsole(options) {
1962
2055
  ""
1963
2056
  ].join("\n");
1964
2057
  };
2058
+ const onASession = () => screen.mode === "runs" || screen.mode === "run";
1965
2059
  const selectedRun = (ready) => {
1966
2060
  if (screen.mode === "run" && screen.openRun !== void 0) {
1967
2061
  return ready.getRun(screen.openRun);
@@ -2049,6 +2143,28 @@ async function openConsole(options) {
2049
2143
  screen.busy = void 0;
2050
2144
  }
2051
2145
  };
2146
+ const look = async (runId) => {
2147
+ if (options.check === void 0) {
2148
+ say("no way to read the current state was configured");
2149
+ return;
2150
+ }
2151
+ screen.busy = "reading how things are now...";
2152
+ try {
2153
+ const found = await options.check(runId);
2154
+ const changed = found.resources.filter((one) => one.condition === "changed");
2155
+ say(
2156
+ [
2157
+ verdict(found),
2158
+ ...changed.length === 0 ? [] : ["", ...changed.flatMap((one) => [`${String(one.seq)} ${one.server}.${one.tool}`, ...(one.diff ?? "").split("\n")])]
2159
+ ].join("\n"),
2160
+ changed.length === 0 ? NOTICE_TICKS2 : READING_TICKS
2161
+ );
2162
+ } catch (error) {
2163
+ say(error instanceof Error ? error.message : "could not read the current state");
2164
+ } finally {
2165
+ screen.busy = void 0;
2166
+ }
2167
+ };
2052
2168
  const rescan = () => {
2053
2169
  screen.groups = options.scan?.() ?? [];
2054
2170
  };
@@ -2160,7 +2276,26 @@ async function openConsole(options) {
2160
2276
  decide(false);
2161
2277
  }
2162
2278
  return;
2279
+ case "l": {
2280
+ if (!onASession()) {
2281
+ return;
2282
+ }
2283
+ if (screen.busy !== void 0) {
2284
+ say("still working on the last one");
2285
+ return;
2286
+ }
2287
+ const ready = open();
2288
+ const run = ready === void 0 ? void 0 : selectedRun(ready);
2289
+ if (ready === void 0 || run === void 0) {
2290
+ return;
2291
+ }
2292
+ void look(run.id);
2293
+ return;
2294
+ }
2163
2295
  case "p": {
2296
+ if (!onASession()) {
2297
+ return;
2298
+ }
2164
2299
  const ready = open();
2165
2300
  const run = ready === void 0 ? void 0 : selectedRun(ready);
2166
2301
  if (ready === void 0 || run === void 0) {
@@ -2175,6 +2310,10 @@ async function openConsole(options) {
2175
2310
  return;
2176
2311
  }
2177
2312
  case "u": {
2313
+ if (!onASession()) {
2314
+ say("undo works on a session; r for the list");
2315
+ return;
2316
+ }
2178
2317
  if (screen.busy !== void 0) {
2179
2318
  say("still working on the last one");
2180
2319
  return;
@@ -2241,6 +2380,7 @@ async function openConsole(options) {
2241
2380
  for (; !screen.stop; tick += 1) {
2242
2381
  if (screen.notice !== "" && tick >= screen.noticeUntil) {
2243
2382
  screen.notice = "";
2383
+ screen.warned = void 0;
2244
2384
  }
2245
2385
  options.write(clear + frame());
2246
2386
  if (options.maxTicks !== void 0 && tick + 1 >= options.maxTicks) {
@@ -2289,7 +2429,7 @@ var COMMANDS = `
2289
2429
  synartesis init <server> -- <command> [args...] [--manifest <path>]
2290
2430
  synartesis check [--manifest <path>]
2291
2431
  synartesis list [--journal <path>]
2292
- synartesis show <runId> [--full] [--journal <path>]
2432
+ synartesis show <runId> [--full] [--live] [--journal <path>]
2293
2433
  synartesis gates [--journal <path>]
2294
2434
  synartesis close [runId] [--journal <path>]
2295
2435
  synartesis prune [--older-than <days>] [--dry-run] [--journal <path>]
@@ -2329,6 +2469,7 @@ resource back as the run left it and --replan, or --force to overwrite.
2329
2469
  --client claude-code, claude-desktop, cursor or codex; all by default
2330
2470
  --print show the entries install would write, and write nothing
2331
2471
  --full show every argument, snapshot and inverse in full, nothing elided
2472
+ --live read each resource as it is now and say what has changed since
2332
2473
  --server serve one server from the manifest, keeping its tool names
2333
2474
  --manifest synartesis.yaml, looked for here and upwards, then in the home
2334
2475
  --journal beside the manifest, or the one in the home
@@ -2743,13 +2884,24 @@ function runList(journal, asJson, journalPath) {
2743
2884
  }
2744
2885
  return 0;
2745
2886
  }
2746
- function runShow(argv, journal, asJson) {
2887
+ async function runShow(argv, journal, asJson) {
2747
2888
  const full = argv.includes("--full");
2748
2889
  const runs = [...journal.listRuns()].reverse();
2749
2890
  const run = pick(runs, positional(argv)[1], RUN, true);
2750
2891
  const runId = run.id;
2892
+ const inspection = argv.includes("--live") && journal.getActions(runId).length > 0 ? await withUpstreams(
2893
+ findManifest(flag(argv, "--manifest")),
2894
+ async (router) => await inspect({ journal, router, runId }),
2895
+ serversUsedBy(journal, runId)
2896
+ ) : void 0;
2751
2897
  if (asJson) {
2752
- out(JSON.stringify({ run, actions: journal.getActions(runId) }));
2898
+ out(
2899
+ JSON.stringify({
2900
+ run,
2901
+ actions: journal.getActions(runId),
2902
+ ...inspection === void 0 ? {} : { live: inspection.resources }
2903
+ })
2904
+ );
2753
2905
  return 0;
2754
2906
  }
2755
2907
  out("");
@@ -2772,10 +2924,17 @@ function runShow(argv, journal, asJson) {
2772
2924
  out(` ${style.label("timeline")}`);
2773
2925
  out(` ${rule(72)}`);
2774
2926
  out("");
2927
+ const live = new Map((inspection?.resources ?? []).map((found) => [found.seq, found]));
2775
2928
  for (const action of actions) {
2929
+ const now = live.get(action.seq);
2776
2930
  out(
2777
- ` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}`
2931
+ ` ${style.quiet(String(action.seq).padStart(3))} ${badgeOf(action)} ${statusOf(action)} ${style.strong(`${action.server}.${action.tool}`)}` + (now === void 0 ? "" : ` ${conditionOf(now)}`)
2778
2932
  );
2933
+ if (now?.diff !== void 0) {
2934
+ for (const line2 of now.diff.split("\n")) {
2935
+ out(` ${style.quiet(line2)}`);
2936
+ }
2937
+ }
2779
2938
  if (full) {
2780
2939
  out(` ${style.quiet("arguments")}`);
2781
2940
  for (const line2 of block(action.args)) {
@@ -2812,9 +2971,40 @@ function runShow(argv, journal, asJson) {
2812
2971
  }
2813
2972
  out("");
2814
2973
  out(` ${summarise2(actions)}`);
2974
+ if (inspection !== void 0) {
2975
+ out("");
2976
+ const spoiled = inspection.resources.some((found) => found.condition === "changed");
2977
+ out(` ${spoiled ? style.accent(verdict(inspection)) : style.quiet(verdict(inspection))}`);
2978
+ if (spoiled) {
2979
+ out(
2980
+ ` ${style.quiet("undo stops at the first of them; ")}${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force`)}${style.quiet(" shows what it would write")}`
2981
+ );
2982
+ }
2983
+ } else {
2984
+ out("");
2985
+ out(
2986
+ ` ${style.quiet("has anything changed since? ")}${style.strong(`${cliCommand()} show ${runId.slice(0, 8)} --live`)}`
2987
+ );
2988
+ }
2815
2989
  out("");
2816
2990
  return 0;
2817
2991
  }
2992
+ function conditionOf(found) {
2993
+ switch (found.condition) {
2994
+ case "changed":
2995
+ return style.accent("changed since");
2996
+ case "unchanged":
2997
+ return style.quiet("unchanged");
2998
+ case "restored":
2999
+ return style.quiet(found.note === void 0 ? "back to before" : `back to before, ${found.note}`);
3000
+ case "superseded":
3001
+ return style.quiet("older write to the same thing");
3002
+ case "not-applied":
3003
+ return style.quiet(`never applied (${found.note ?? "settled"})`);
3004
+ default:
3005
+ return style.quiet(found.note ?? "cannot tell");
3006
+ }
3007
+ }
2818
3008
  var CLASS_MARK = {
2819
3009
  readonly: "\xB7",
2820
3010
  reversible: "\u2190",
@@ -3105,45 +3295,63 @@ function report(result, alreadyForcing = false) {
3105
3295
  out("");
3106
3296
  return result.status === "rolled_back" ? 0 : 1;
3107
3297
  }
3108
- async function performUndo(manifestPath, journal, runId, options) {
3298
+ async function withUpstreams(manifestPath, use, only) {
3109
3299
  const manifest = loadManifest(manifestPath);
3110
3300
  const upstreams = [];
3301
+ const missing = [];
3111
3302
  try {
3112
3303
  for (const [name, spec] of Object.entries(manifest.servers)) {
3113
- upstreams.push(
3114
- await connectStdioUpstream({
3115
- name,
3116
- command: spec.command,
3117
- args: spec.args,
3118
- stderr: "capture",
3119
- ...spec.env === void 0 ? {} : { env: spec.env }
3120
- })
3121
- );
3122
- }
3123
- const base = {
3124
- journal,
3125
- router: createRouter(upstreams, manifest),
3126
- runId,
3127
- ...options.toSeq === void 0 ? {} : { toSeq: options.toSeq },
3128
- ...options.replan === true ? { replanWith: manifest } : {}
3129
- };
3130
- if (options.preflight === true) {
3131
- const seen = await rollback({ ...base, dryRun: true });
3132
- if (seen.halted?.conflict === true) {
3133
- return seen;
3304
+ if (only !== void 0 && !only.has(name)) {
3305
+ continue;
3306
+ }
3307
+ try {
3308
+ upstreams.push(
3309
+ await connectStdioUpstream({
3310
+ name,
3311
+ command: spec.command,
3312
+ args: spec.args,
3313
+ stderr: "capture",
3314
+ ...spec.env === void 0 ? {} : { env: spec.env }
3315
+ })
3316
+ );
3317
+ } catch (error) {
3318
+ missing.push(`${name}: ${describe(error)}`);
3134
3319
  }
3135
3320
  }
3136
- return await rollback({
3137
- ...base,
3138
- dryRun: options.dryRun,
3139
- ...options.force === true ? { force: true } : {}
3140
- });
3321
+ if (upstreams.length === 0 && missing.length > 0) {
3322
+ throw new ManifestError(`no server could be started. ${missing.join("; ")}`);
3323
+ }
3324
+ for (const why of missing) {
3325
+ process.stderr.write(`synartesis: ${why}; anything through it cannot be reached
3326
+ `);
3327
+ }
3328
+ return await use(createRouter(upstreams, manifest), manifest);
3141
3329
  } finally {
3142
3330
  for (const upstream of upstreams) {
3143
3331
  await upstream.close();
3144
3332
  }
3145
3333
  }
3146
3334
  }
3335
+ function serversUsedBy(journal, runId) {
3336
+ return new Set(journal.getActions(runId).map((action) => action.server));
3337
+ }
3338
+ async function performUndo(manifestPath, journal, runId, options) {
3339
+ return await withUpstreams(
3340
+ manifestPath,
3341
+ async (router, manifest) => await rollback({
3342
+ journal,
3343
+ router,
3344
+ runId,
3345
+ ...options.toSeq === void 0 ? {} : { toSeq: options.toSeq },
3346
+ ...options.replan === true ? { replanWith: manifest } : {},
3347
+ dryRun: options.dryRun,
3348
+ ...options.force === true ? { force: true } : {}
3349
+ }),
3350
+ // A replan re-resolves inverses from the current policy, which may name a
3351
+ // server this run never used; everything else needs only what it touched.
3352
+ options.replan === true ? void 0 : serversUsedBy(journal, runId)
3353
+ );
3354
+ }
3147
3355
  async function runUndo(argv, journal) {
3148
3356
  const given = positional(argv)[1];
3149
3357
  const chosen = pick([...journal.listRuns()].reverse(), given, RUN, true);
@@ -3195,21 +3403,48 @@ async function runUndo(argv, journal) {
3195
3403
  }
3196
3404
  const forcing = argv.includes("--force");
3197
3405
  const said = argv.includes("--yes");
3198
- const result = await performUndo(findManifest(flag(argv, "--manifest")), journal, runId, {
3199
- dryRun: argv.includes("--dry-run"),
3200
- ...toSeq === void 0 ? {} : { toSeq },
3201
- replan: argv.includes("--replan"),
3202
- ...forcing ? { force: said, preflight: !said } : {}
3203
- });
3204
- const code = report(result, forcing);
3205
- if (forcing && !said && result.halted?.conflict === true) {
3206
- out(` ${style.quiet("nothing has been written. To go ahead and lose that change:")}`);
3207
- out(
3208
- ` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`
3406
+ const manifestPath = findManifest(flag(argv, "--manifest"));
3407
+ if (said && !forcing) {
3408
+ process.stderr.write("synartesis: --yes only means anything with --force; ignoring it\n");
3409
+ }
3410
+ if (forcing && !said) {
3411
+ const over = (await withUpstreams(
3412
+ manifestPath,
3413
+ async (router) => await inspect({ journal, router, runId }),
3414
+ serversUsedBy(journal, runId)
3415
+ )).resources.filter(
3416
+ // Below --to nothing is undone, so a change down there is not something
3417
+ // this command would write over and must not stand in its way.
3418
+ (one) => one.condition === "changed" && (toSeq === void 0 || one.seq >= toSeq)
3209
3419
  );
3210
- out("");
3420
+ if (over.length > 0) {
3421
+ out("");
3422
+ out(
3423
+ ` ${style.accent(`${String(over.length)} changed since this ran`)} ` + style.quiet(`undoing would write over ${over.length === 1 ? "it" : "them"}`)
3424
+ );
3425
+ for (const one of over) {
3426
+ out("");
3427
+ out(` ${style.quiet(String(one.seq).padStart(3))} ${style.strong(`${one.server}.${one.tool}`)}`);
3428
+ for (const line2 of (one.diff ?? "").split("\n")) {
3429
+ out(` ${style.quiet(line2)}`);
3430
+ }
3431
+ }
3432
+ out("");
3433
+ out(` ${style.quiet("nothing has been written. To go ahead and lose that:")}`);
3434
+ out(` ${style.strong(`${cliCommand()} undo ${runId.slice(0, 8)} --force --yes`)}`);
3435
+ out("");
3436
+ return 1;
3437
+ }
3211
3438
  }
3212
- return code;
3439
+ return report(
3440
+ await performUndo(manifestPath, journal, runId, {
3441
+ dryRun: argv.includes("--dry-run"),
3442
+ ...toSeq === void 0 ? {} : { toSeq },
3443
+ replan: argv.includes("--replan"),
3444
+ ...forcing && said ? { force: true } : {}
3445
+ }),
3446
+ forcing
3447
+ );
3213
3448
  }
3214
3449
  var FLAGS = /* @__PURE__ */ new Set([
3215
3450
  // Two lists have to agree about a flag: this one decides whether it is
@@ -3219,6 +3454,7 @@ var FLAGS = /* @__PURE__ */ new Set([
3219
3454
  "--client",
3220
3455
  "--print",
3221
3456
  "--full",
3457
+ "--live",
3222
3458
  "--manifest",
3223
3459
  "--journal",
3224
3460
  "--to",
@@ -3292,6 +3528,18 @@ ${COMMANDS}`);
3292
3528
  write: (text) => process.stdout.write(text),
3293
3529
  live: process.stdout.isTTY,
3294
3530
  decideAs: flag(argv, "--by") ?? process.env["USER"] ?? process.env["LOGNAME"] ?? "unknown",
3531
+ check: async (runId) => {
3532
+ const journal2 = openJournal(journalPath2, { mustExist: true });
3533
+ try {
3534
+ return await withUpstreams(
3535
+ manifestPath,
3536
+ async (router) => await inspect({ journal: journal2, router, runId }),
3537
+ serversUsedBy(journal2, runId)
3538
+ );
3539
+ } finally {
3540
+ journal2.close();
3541
+ }
3542
+ },
3295
3543
  undo: async (runId, dryRun, force) => {
3296
3544
  const journal2 = openJournal(journalPath2, { mustExist: true });
3297
3545
  try {
@@ -3356,7 +3604,7 @@ ${COMMANDS}`);
3356
3604
  case "list":
3357
3605
  return runList(journal, asJson, journalPath);
3358
3606
  case "show":
3359
- return runShow(argv, journal, asJson);
3607
+ return await runShow(argv, journal, asJson);
3360
3608
  case "close":
3361
3609
  return runClose(argv, journal);
3362
3610
  case "prune":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synartesis",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "An undo layer for AI agents.",
5
5
  "type": "module",
6
6
  "private": false,